From 0d8b9fe91f8b3de59dd880bfc4d165591ffb70c6 Mon Sep 17 00:00:00 2001 From: tpz Date: Tue, 5 Dec 2023 13:05:52 +0100 Subject: [PATCH] feat: added new order constraint EntityPrimaryKeyNatural, added option to QueryValidator to to switch running profiles between demo and local evita instance, moved structures sorting logic to tests only - we only need it to be sorted for comparing in documentation tests, added documentation to query constraints and commonly used model classes, fixed serialization of ComplexDataObject when inner object is present fixed bugs found thanks to the new documentation tests --- .../ComplexDataObjectToJsonConverter.cs | 7 +- .../Converters/Models/Data/EntityConverter.cs | 77 +- .../Converters/Models/ResponseConverter.cs | 29 +- EvitaDB.Client/EvitaClient.cs | 158 +++- EvitaDB.Client/EvitaClientSession.cs | 693 ++++++++++++++---- .../Models/Data/AssociatedDataKey.cs | 4 +- EvitaDB.Client/Models/Data/IAssociatedData.cs | 93 ++- EvitaDB.Client/Models/Data/IAttributes.cs | 102 ++- .../Models/Data/IDevelopmentConstants.cs | 7 + EvitaDB.Client/Models/Data/IDroppable.cs | 10 +- EvitaDB.Client/Models/Data/IEntity.cs | 53 +- .../Models/Data/IEntityClassifier.cs | 22 +- .../Data/IEntityClassifierWithParent.cs | 14 +- EvitaDB.Client/Models/Data/IPrices.cs | 58 +- EvitaDB.Client/Models/Data/IVersioned.cs | 11 +- .../Reference/InsertReferenceMutation.cs | 2 +- .../Models/Data/Structure/AssociatedData.cs | 24 +- .../Models/Data/Structure/Attributes.cs | 30 +- .../Models/Data/Structure/Entity.cs | 68 +- .../Models/Data/Structure/EntityAttributes.cs | 12 +- .../Models/Data/Structure/EntityReference.cs | 6 +- .../Data/Structure/ExistingEntityBuilder.cs | 4 +- .../Data/Structure/ExistingPricesBuilder.cs | 4 +- .../Data/Structure/InitialEntityBuilder.cs | 4 +- .../Data/Structure/InitialPricesBuilder.cs | 6 +- .../AssociatedDataValuePredicate.cs | 4 +- .../Predicates/AttributeValuePredicate.cs | 4 +- .../Models/Data/Structure/Prices.cs | 18 +- .../Models/Data/Structure/Reference.cs | 39 +- .../Data/Structure/ReferenceAttributes.cs | 14 +- EvitaDB.Client/Models/EvitaRequest.cs | 96 ++- .../Models/ExtraResults/FacetSummary.cs | 109 ++- .../Models/ExtraResults/Histogram.cs | 3 +- .../Models/ExtraResults/IHistogram.cs | 22 +- .../Models/ExtraResults/IPrettyPrintable.cs | 10 + .../Builders/InternalEntitySchemaBuilder.cs | 6 +- .../Models/Schemas/Dtos/EntitySchema.cs | 35 +- .../Models/Schemas/EntitySchemaDecorator.cs | 6 +- .../Models/Schemas/IEntitySchema.cs | 4 +- EvitaDB.Client/Queries/Filter/And.cs | 21 +- .../Queries/Filter/AttributeBetween.cs | 40 +- .../Queries/Filter/AttributeContains.cs | 20 +- .../Queries/Filter/AttributeEndsWith.cs | 20 +- .../Queries/Filter/AttributeEquals.cs | 21 +- .../Queries/Filter/AttributeGreaterThan.cs | 15 +- .../Filter/AttributeGreaterThanEquals.cs | 16 +- .../Queries/Filter/AttributeInRange.cs | 26 +- .../Queries/Filter/AttributeInSet.cs | 21 +- EvitaDB.Client/Queries/Filter/AttributeIs.cs | 16 +- .../Queries/Filter/AttributeLessThan.cs | 15 +- .../Queries/Filter/AttributeLessThanEquals.cs | 16 +- .../Queries/Filter/AttributeSpecialValue.cs | 8 +- .../Queries/Filter/AttributeStartsWith.cs | 20 +- EvitaDB.Client/Queries/Filter/EntityHaving.cs | 17 +- .../Queries/Filter/EntityLocaleEquals.cs | 30 +- .../Queries/Filter/EntityPrimaryKeyInSet.cs | 10 +- EvitaDB.Client/Queries/Filter/FacetHaving.cs | 20 +- EvitaDB.Client/Queries/Filter/FilterBy.cs | 17 +- .../Queries/Filter/FilterGroupBy.cs | 22 +- .../Queries/Filter/HierarchyDirectRelation.cs | 50 +- .../Queries/Filter/HierarchyExcluding.cs | 73 +- .../Queries/Filter/HierarchyExcludingRoot.cs | 58 +- .../Queries/Filter/HierarchyHaving.cs | 83 ++- .../Queries/Filter/HierarchyWithin.cs | 57 +- .../Queries/Filter/HierarchyWithinRoot.cs | 54 +- EvitaDB.Client/Queries/Filter/Not.cs | 30 +- EvitaDB.Client/Queries/Filter/Or.cs | 20 +- EvitaDB.Client/Queries/Filter/PriceBetween.cs | 15 +- .../Queries/Filter/PriceInCurrency.cs | 13 +- .../Queries/Filter/PriceInPriceLists.cs | 23 +- EvitaDB.Client/Queries/Filter/PriceValidIn.cs | 13 +- .../Queries/Filter/ReferenceHaving.cs | 26 +- EvitaDB.Client/Queries/Filter/UserFilter.cs | 19 +- EvitaDB.Client/Queries/Head/Collection.cs | 10 +- EvitaDB.Client/Queries/IQueryConstraints.cs | 515 ++++++++++++- .../Queries/Order/AttributeNatural.cs | 39 +- .../Queries/Order/AttributeSetExact.cs | 21 +- .../Queries/Order/AttributeSetInFilter.cs | 26 +- .../Queries/Order/EntityGroupProperty.cs | 69 +- .../Queries/Order/EntityPrimaryKeyExact.cs | 21 +- .../Queries/Order/EntityPrimaryKeyInFilter.cs | 25 +- .../Queries/Order/EntityPrimaryKeyNatural.cs | 34 + .../Queries/Order/EntityProperty.cs | 38 +- EvitaDB.Client/Queries/Order/OrderBy.cs | 27 +- .../Queries/Order/OrderDirection.cs | 5 +- EvitaDB.Client/Queries/Order/OrderGroupBy.cs | 46 +- EvitaDB.Client/Queries/Order/PriceNatural.cs | 19 +- EvitaDB.Client/Queries/Order/Random.cs | 11 +- .../Queries/Order/ReferenceProperty.cs | 67 +- EvitaDB.Client/Queries/Query.cs | 22 +- .../Queries/Requires/AssociatedDataContent.cs | 18 +- .../Queries/Requires/AttributeContent.cs | 20 +- .../Queries/Requires/AttributeHistogram.cs | 17 +- .../Queries/Requires/DataInLocales.cs | 22 +- .../EmptyHierarchicalEntityBehaviour.cs | 6 +- .../Queries/Requires/EntityFetch.cs | 30 +- .../Queries/Requires/EntityGroupFetch.cs | 35 +- .../Requires/FacetGroupsConjunction.cs | 40 + .../Requires/FacetGroupsDisjunction.cs | 40 + .../Queries/Requires/FacetGroupsNegation.cs | 30 + .../Queries/Requires/FacetStatisticsDepth.cs | 12 +- .../Queries/Requires/FacetSummary.cs | 75 +- .../Requires/FacetSummaryOfReference.cs | 72 +- .../Queries/Requires/HierarchyChildren.cs | 57 +- .../Queries/Requires/HierarchyContent.cs | 21 +- .../Queries/Requires/HierarchyDistance.cs | 37 +- .../Queries/Requires/HierarchyFromNode.cs | 80 +- .../Queries/Requires/HierarchyFromRoot.cs | 61 +- .../Queries/Requires/HierarchyLevel.cs | 31 +- .../Queries/Requires/HierarchyNode.cs | 40 +- .../Queries/Requires/HierarchyOfReference.cs | 33 +- .../Queries/Requires/HierarchyOfSelf.cs | 30 +- .../Queries/Requires/HierarchyParents.cs | 56 +- .../Queries/Requires/HierarchySiblings.cs | 65 +- .../Queries/Requires/HierarchyStatistics.cs | 36 +- .../Queries/Requires/HierarchyStopAt.cs | 14 +- EvitaDB.Client/Queries/Requires/Page.cs | 20 +- .../Queries/Requires/PriceContent.cs | 24 +- .../Queries/Requires/PriceContentMode.cs | 5 +- .../Queries/Requires/PriceHistogram.cs | 18 +- EvitaDB.Client/Queries/Requires/PriceType.cs | 19 +- .../Queries/Requires/QueryPriceMode.cs | 5 +- .../Queries/Requires/QueryTelemetry.cs | 10 +- .../Queries/Requires/ReferenceContent.cs | 83 ++- EvitaDB.Client/Queries/Requires/Require.cs | 16 +- .../Queries/Requires/StatisticsBase.cs | 6 +- .../Queries/Requires/StatisticsType.cs | 6 +- EvitaDB.Client/Queries/Requires/Strip.cs | 20 +- .../Queries/Visitor/PrettyPrintingVisitor.cs | 4 +- EvitaDB.QueryValidator/Program.cs | 62 +- .../Json/Converters/DecimalConverter.cs | 25 + .../Json/Converters/EntitySerializer.cs | 47 +- .../Markdown/MarkdownConverter.cs | 42 +- ...te.txt => evita-csharp-query-template.txt} | 5 +- EvitaDB.Test/Tests/EvitaDataTypesTest.cs | 13 - 135 files changed, 4717 insertions(+), 479 deletions(-) create mode 100644 EvitaDB.Client/Models/Data/IDevelopmentConstants.cs create mode 100644 EvitaDB.Client/Models/ExtraResults/IPrettyPrintable.cs create mode 100644 EvitaDB.Client/Queries/Order/EntityPrimaryKeyNatural.cs create mode 100644 EvitaDB.QueryValidator/Serialization/Json/Converters/DecimalConverter.cs rename EvitaDB.QueryValidator/{csharp_query_template.txt => evita-csharp-query-template.txt} (88%) diff --git a/EvitaDB.Client/Converters/DataTypes/ComplexDataObjectToJsonConverter.cs b/EvitaDB.Client/Converters/DataTypes/ComplexDataObjectToJsonConverter.cs index d4bcc93..b63955c 100644 --- a/EvitaDB.Client/Converters/DataTypes/ComplexDataObjectToJsonConverter.cs +++ b/EvitaDB.Client/Converters/DataTypes/ComplexDataObjectToJsonConverter.cs @@ -72,8 +72,9 @@ public void Visit(DataItemMap mapItem) var stackNode = _stack.Peek(); if (stackNode is JObject objectNode) { - objectNode.Add(_propertyNameStack.Peek()); - _stack.Push(objectNode); + JObject newObject = new(); + objectNode.Add(_propertyNameStack.Peek(), newObject); + _stack.Push(newObject); } else if (stackNode is JArray arrayNode) { @@ -233,4 +234,4 @@ private void WriteNull() throw new InvalidOperationException($"Unexpected type of node on stack: {theNode?.GetType()}"); } } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Converters/Models/Data/EntityConverter.cs b/EvitaDB.Client/Converters/Models/Data/EntityConverter.cs index 81635d4..b571f58 100644 --- a/EvitaDB.Client/Converters/Models/Data/EntityConverter.cs +++ b/EvitaDB.Client/Converters/Models/Data/EntityConverter.cs @@ -60,7 +60,16 @@ public static T ToEntity(Func entitySc parentEntity = parent; } - return (T) Entity.InternalBuild( + List references = grpcEntity.References + .Select(it => ToReference(entitySchema, entitySchemaProvider, it, evitaRequest)) + .ToList(); + + if (IDevelopmentConstants.IsTestRun) + { + references.Sort((x, y) => x.ReferenceKey.CompareTo(y.ReferenceKey)); + } + + return (T)Entity.InternalBuild( grpcEntity.PrimaryKey, grpcEntity.Version, entitySchema, @@ -99,26 +108,37 @@ public static T ToEntity(Func entitySc ); } - private static ICollection ToAttributeValues( + private static IDictionary ToAttributeValues( IDictionary globalAttributesMap, IDictionary localizedAttributesMap ) { - List result = new(globalAttributesMap.Count + localizedAttributesMap.Count); + AttributeValue[] attributeValueTuples = new AttributeValue[globalAttributesMap.Count + localizedAttributesMap.Values.Select(x => x.Attributes.Count).Sum()]; + int index = 0; foreach (var (key, localizedAttributeSet) in localizedAttributesMap) { CultureInfo locale = new CultureInfo(key); foreach (KeyValuePair attributeEntry in localizedAttributeSet.Attributes) { - result.Add( - ToAttributeValue(new AttributeKey(attributeEntry.Key, locale), attributeEntry.Value) - ); + attributeValueTuples[index++] = + ToAttributeValue(new AttributeKey(attributeEntry.Key, locale), attributeEntry.Value); } } foreach (var (attributeName, value) in globalAttributesMap) { - result.Add(ToAttributeValue(new AttributeKey(attributeName), value)); + attributeValueTuples[index++] = ToAttributeValue(new AttributeKey(attributeName), value); + } + + if (IDevelopmentConstants.IsTestRun) + { + Array.Sort(attributeValueTuples, (x, y) => x.Key.CompareTo(y.Key)); + } + + IDictionary result = new Dictionary(attributeValueTuples.Length); + foreach (var attributeValue in attributeValueTuples) + { + result.Add(attributeValue.Key, attributeValue); } return result; @@ -133,37 +153,38 @@ private static AttributeValue ToAttributeValue(AttributeKey attributeKey, GrpcEv EvitaDataTypesConverter.ToEvitaValue(attributeValue) ); } - - private static ICollection ToAssociatedDataValues( + + private static IDictionary ToAssociatedDataValues( IDictionary globalAssociatedDataMap, IDictionary localizedAssociatedDataMap ) { - List result = new(globalAssociatedDataMap.Count + localizedAssociatedDataMap.Count); - + AssociatedDataValue[] associatedDataValueTuples = new AssociatedDataValue[globalAssociatedDataMap.Count + localizedAssociatedDataMap.Values.Select(x => x.AssociatedData.Count).Sum()]; + int index = 0; foreach (var (key, localizedAssociatedDataSet) in localizedAssociatedDataMap) { CultureInfo locale = new CultureInfo(key); - foreach (KeyValuePair associatedDataEntry in - localizedAssociatedDataSet.AssociatedData) + foreach (KeyValuePair associatedDataEntry in localizedAssociatedDataSet.AssociatedData) { - result.Add( - ToAssociatedDataValue( - new AssociatedDataKey(associatedDataEntry.Key, locale), - associatedDataEntry.Value - ) - ); + associatedDataValueTuples[index++] = + ToAssociatedDataValue(new AssociatedDataKey(associatedDataEntry.Key, locale), associatedDataEntry.Value); } } - foreach (var (associatedDataName, value) in globalAssociatedDataMap) + foreach (var (attributeName, value) in globalAssociatedDataMap) { - result.Add( - ToAssociatedDataValue( - new AssociatedDataKey(associatedDataName), - value - ) - ); + associatedDataValueTuples[index++] = ToAssociatedDataValue(new AssociatedDataKey(attributeName), value); + } + + if (IDevelopmentConstants.IsTestRun) + { + Array.Sort(associatedDataValueTuples, (x, y) => x.Key.CompareTo(y.Key)); + } + + IDictionary result = new Dictionary(associatedDataValueTuples.Length); + foreach (var associatedDataValue in associatedDataValueTuples) + { + result.Add(associatedDataValue.Key, associatedDataValue); } return result; @@ -206,7 +227,7 @@ associatedDataValue.PrimitiveValue is not null grpcPrice.Version ); } - + private static Reference ToReference( ISealedEntitySchema entitySchema, Func entitySchemaProvider, @@ -255,4 +276,4 @@ private static Reference ToReference( : ToEntity(entitySchemaProvider, grpcReference.GroupReferencedEntity, evitaRequest) ); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Converters/Models/ResponseConverter.cs b/EvitaDB.Client/Converters/Models/ResponseConverter.cs index f9de3fb..a36dd7b 100644 --- a/EvitaDB.Client/Converters/Models/ResponseConverter.cs +++ b/EvitaDB.Client/Converters/Models/ResponseConverter.cs @@ -51,7 +51,7 @@ public static IDataChunk ConvertToDataChunk(GrpcQueryResponse grpcResponse public static IEvitaResponseExtraResult[] ToExtraResults( Func entitySchemaFetcher, - EvitaRequest evitaRequest, + EvitaRequest evitaRequest, GrpcExtraResults? extraResults) { Query query = evitaRequest.Query; @@ -101,7 +101,7 @@ extraResults.SelfHierarchy is not null evitaRequest, hierarchyConstraints .OfType() - .First(it => it.ReferenceNames.Any(name => name == x.Key)), + .First(it => it.ReferenceNames.Any(name => name == x.Key)), x.Value) ) ) @@ -155,8 +155,11 @@ GrpcFacetGroupStatistics grpcFacetGroupStatistics return new FacetGroupStatistics( grpcFacetGroupStatistics.ReferenceName, grpcFacetGroupStatistics.GroupEntity is not null - ? EntityConverter.ToEntity(entitySchemaFetcher, grpcFacetGroupStatistics.GroupEntity, evitaRequest) - : EntityConverter.ToEntityReference(grpcFacetGroupStatistics.GroupEntityReference), + ? EntityConverter.ToEntity(entitySchemaFetcher, grpcFacetGroupStatistics.GroupEntity, + evitaRequest.DeriveCopyWith(grpcFacetGroupStatistics.GroupEntity.EntityType, entityGroupFetch!)) + : grpcFacetGroupStatistics.GroupEntityReference is not null + ? EntityConverter.ToEntityReference(grpcFacetGroupStatistics.GroupEntityReference) + : null, grpcFacetGroupStatistics.Count, grpcFacetGroupStatistics.FacetStatistics .Select(x => ToFacetStatistics(entitySchemaFetcher, evitaRequest, entityFetch, x)) @@ -176,12 +179,12 @@ grpcFacetStatistics.FacetEntity is not null ? EntityConverter.ToEntity( entitySchemaFetcher, grpcFacetStatistics.FacetEntity, - evitaRequest + evitaRequest.DeriveCopyWith(grpcFacetStatistics.FacetEntity.EntityType, entityFetch!) ) : EntityConverter.ToEntityReference(grpcFacetStatistics.FacetEntityReference), grpcFacetStatistics.Requested, grpcFacetStatistics.Count, - grpcFacetStatistics is {Impact: not null, MatchCount: not null} + grpcFacetStatistics is { Impact: not null, MatchCount: not null } ? new RequestImpact( grpcFacetStatistics.Impact.Value, grpcFacetStatistics.MatchCount.Value @@ -206,7 +209,8 @@ GrpcHierarchy grpcHierarchy cnt => cnt is IHierarchyRequireConstraint hrc && x.Key == hrc.OutputName ); EntityFetch? entityFetch = QueryUtils.FindConstraint(hierarchyConstraint!); - return x.Value.LevelInfos.Select(y => ToLevelInfo(entitySchemaFetcher, evitaRequest, entityFetch, y)).ToList(); + return x.Value.LevelInfos.Select(y => ToLevelInfo(entitySchemaFetcher, evitaRequest, entityFetch, y)) + .ToList(); }); } @@ -219,7 +223,14 @@ GrpcLevelInfo grpcLevelInfo { return new LevelInfo( grpcLevelInfo.Entity is not null - ? EntityConverter.ToEntity(entitySchemaFetcher, grpcLevelInfo.Entity, evitaRequest) + ? EntityConverter.ToEntity( + entitySchemaFetcher, + grpcLevelInfo.Entity, + evitaRequest.DeriveCopyWith( + grpcLevelInfo.Entity.EntityType, + entityFetch! + ) + ) : EntityConverter.ToEntityReference(grpcLevelInfo.EntityReference), grpcLevelInfo.Requested, grpcLevelInfo.QueriedEntityCount, @@ -259,4 +270,4 @@ private static Bucket ToBucket(GrpcHistogram.Types.GrpcBucket grpcBucket) grpcBucket.Requested ); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/EvitaClient.cs b/EvitaDB.Client/EvitaClient.cs index f430727..c85f8eb 100644 --- a/EvitaDB.Client/EvitaClient.cs +++ b/EvitaDB.Client/EvitaClient.cs @@ -29,6 +29,15 @@ namespace EvitaDB.Client; public delegate void EvitaSessionTerminationCallback(EvitaClientSession session); +/// +/// Evita is a specialized database with easy-to-use API for e-commerce systems. Purpose of this research is creating fast +/// and scalable engine that handles all complex tasks that e-commerce systems has to deal with on daily basis. Evita should +/// operate as a fast secondary lookup / search index used by application frontends. We aim for order of magnitude better +/// latency (10x faster or better) for common e-commerce tasks than other solutions based on SQL or NoSQL databases on the +/// same hardware specification. Evita should not be used for storing and handling primary data, and we don't aim for ACID +/// properties nor data corruption guarantees. Evita "index" must be treated as something that could be dropped any time and +/// built up from scratch easily again. +/// public partial class EvitaClient : IClientContext, IDisposable { private static readonly ISchemaMutationConverter @@ -86,6 +95,12 @@ void TerminationCallback() _terminationCallback = TerminationCallback; } + /// + /// This method is used for registering a callback that is invoked any system event, like catalog creation or its + /// top level mutation occurs. + /// + /// request for subscribing to system events + /// an observable collection that receives updates about changes in database public IObservable RegisterSystemChangeCapture(ChangeSystemCaptureRequest request) { return ExecuteWithStreamingEvitaService(stub => @@ -100,16 +115,38 @@ public IObservable RegisterSystemChangeCapture(ChangeSystem ); } + /// + /// Creates for querying and altering the database. + /// Don't forget to or when your work with Evita is finished. + /// + /// EvitaClientSession is not thread safe! + /// + /// name of the catalog on which the session should be created + /// created read-only session public EvitaClientSession CreateReadOnlySession(string catalogName) { return CreateSession(new SessionTraits(catalogName)); } + /// + /// Creates for querying and altering the database. + /// Don't forget to or when your work with Evita is finished. + /// + /// EvitaClientSession is not thread safe! + /// + /// name of the catalog on which the session should be created + /// created read-write session public EvitaClientSession CreateReadWriteSession(string catalogName) { return CreateSession(new SessionTraits(catalogName, SessionFlags.ReadWrite)); } + /// + /// Method returns active session by its unique id or NULL if such session is not found. + /// + /// name of the catalog + /// id of requested session + /// returns existing active session specified by params public EvitaClientSession? GetSessionById(string catalogName, Guid sessionId) { AssertActive(); @@ -121,12 +158,19 @@ public EvitaClientSession CreateReadWriteSession(string catalogName) return null; } + /// + /// Terminates existing . When this method is called no additional calls to this EvitaSession + /// is accepted and all will terminate with . + /// public void TerminateSession(EvitaClientSession session) { AssertActive(); (this as IClientContext).ExecuteWithClientId(Configuration.ClientId, Close); } + /// + /// Returns complete listing of all catalogs known to the Evita instance. + /// public ISet GetCatalogNames() { AssertActive(); @@ -137,6 +181,12 @@ public ISet GetCatalogNames() ); } + /// + /// Creates new catalog of particular name if it doesn't exist. The schema of the catalog (should it was created or + /// not) is returned to the response. + /// + /// name of the catalog + /// a builder for applying more catalog mutations public ICatalogSchemaBuilder DefineCatalog(string catalogName) { AssertActive(); @@ -153,6 +203,16 @@ public ICatalogSchemaBuilder DefineCatalog(string catalogName) }); } + /// + /// Renames existing catalog to a new name. The `newCatalogName` must not clash with any existing catalog name, + /// otherwise exception is thrown. If you need to rename catalog to a name of existing catalog use + /// the method instead. + /// + /// In case exception occurs the original catalog (`catalogName`) is guaranteed to be untouched, + /// and the `newCatalogName` will not be present. + /// + /// name of the catalog that will be renamed + /// new name of the catalog public void RenameCatalog(string catalogName, string newCatalogName) { AssertActive(); @@ -170,6 +230,16 @@ public void RenameCatalog(string catalogName, string newCatalogName) } } + /// + /// Replaces existing catalog of particular with the contents of the another catalog. When this method is + /// successfully finished, the catalog `catalogNameToBeReplacedWith` will be known under the name of the + /// `catalogNameToBeReplaced` and the original contents of the `catalogNameToBeReplaced` will be purged entirely. + /// + /// In case exception occurs, the original catalog (`catalogNameToBeReplaced`) is guaranteed to be untouched, the + /// state of `catalogNameToBeReplacedWith` is however unknown and should be treated as damaged. + /// + /// name of the catalog that will become the successor of the original catalog (old name) + /// name of the catalog that will be replaced and dropped (new name) public void ReplaceCatalog(string catalogNameToBeReplacedWith, string catalogNameToBeReplaced) { AssertActive(); @@ -187,6 +257,11 @@ public void ReplaceCatalog(string catalogNameToBeReplacedWith, string catalogNam } } + /// + /// Deletes catalog with name `catalogName` along with its contents on disk. + /// + /// name of the removed catalog + /// true if catalog was found in Evita and its contents were successfully removed public bool DeleteCatalogIfExists(string catalogName) { AssertActive(); @@ -206,6 +281,12 @@ public bool DeleteCatalogIfExists(string catalogName) return success; } + /// + /// Applies catalog mutation affecting entire catalog. + /// The reason why we use mutations for this is to be able to include those operations to the WAL that is + /// synchronized to replicas. + /// + /// an array of top level catalog schema mutations to be applied public void Update(params ITopLevelCatalogSchemaMutation[] catalogMutations) { AssertActive(); @@ -218,6 +299,16 @@ public void Update(params ITopLevelCatalogSchemaMutation[] catalogMutations) ExecuteWithBlockingEvitaService(evitaService => evitaService.Update(request)); } + /// + /// Executes querying logic in the newly created Evita session. Session is safely closed at the end of this method + /// and result is returned. + /// Query logic is intended to be read-only. For read-write logic use or + /// open a transaction manually in the logic itself. + /// + /// + /// name of catalog from which the data should be read + /// application logic that reads data + /// flags for ad-hoc created session public T QueryCatalog(string catalogName, Func queryLogic, params SessionFlags[] sessionFlags) { @@ -234,6 +325,16 @@ public T QueryCatalog(string catalogName, Func queryLo } } + /// + /// Executes querying logic in the newly created Evita session. Session is safely closed at the end of this method + /// and result is returned. + /// Query logic is intended to be read-only. For read-write logic use or + /// open a transaction manually in the logic itself. + /// + /// + /// name of catalog from which the data should be read + /// application logic that reads data + /// flags for ad-hoc created session public void QueryCatalog(string catalogName, Action queryLogic, params SessionFlags[] sessionFlags) { @@ -251,6 +352,18 @@ public void QueryCatalog(string catalogName, Action queryLog } + /// + /// Executes catalog read-write logic in the newly Evita session. When logic finishes without exception, changes are + /// committed to the index, otherwise changes are roll-backed and no data is affected. Changes made by the updating + /// logic are visible only within update function. Other threads outside the logic function work with non-changed + /// data until transaction is committed to the index. + /// Current version limitation: + /// Only single updater can execute in parallel (i.e. updates are expected to be invoked by single thread in serial way). + /// + /// + /// name of catalog upon which the changes should be executes + /// application logic that reads and writes data + /// flags for ad-hoc created session public T UpdateCatalog(string catalogName, Func updater, params SessionFlags[]? flags) { AssertActive(); @@ -274,6 +387,18 @@ public T UpdateCatalog(string catalogName, Func update } } + /// + /// Executes catalog read-write logic in the newly Evita session. When logic finishes without exception, changes are + /// committed to the index, otherwise changes are roll-backed and no data is affected. Changes made by the updating + /// logic are visible only within update function. Other threads outside the logic function work with non-changed + /// data until transaction is committed to the index. + /// Current version limitation: + /// Only single updater can execute in parallel (i.e. updates are expected to be invoked by single thread in serial way). + /// + /// + /// name of catalog upon which the changes should be executes + /// application logic that reads and writes data + /// flags for ad-hoc created session public void UpdateCatalog(string catalogName, Action updater, params SessionFlags[]? flags) { UpdateCatalog( @@ -288,6 +413,9 @@ public void UpdateCatalog(string catalogName, Action updater ); } + /// + /// Closes currently opened sessions and shuts down the channel pool. + /// public void Close() { if (Interlocked.CompareExchange(ref _active, 1, 0) == 1) @@ -299,6 +427,9 @@ public void Close() } } + /// + /// Called automatically when instance is disposed. + /// public void Dispose() { Close(); @@ -322,6 +453,18 @@ private T ExecuteWithStreamingEvitaService(Func + /// Method that is called within the to apply the wanted logic on a channel retrieved + /// from a channel pool. + /// + /// interface for retrieving a channel + /// function that contains channel building logic + /// logic to be executed on the created channel + /// channel type + /// response type + /// + /// thrown when error occurs by clients bad database manipulation + /// error cause by bad or unexpected behaviour on the database side private T ExecuteWithEvitaService(IChannelSupplier channelSupplier, Func stubBuilder, Func logic) { @@ -388,6 +531,15 @@ private T ExecuteWithEvitaService(IChannelSupplier channelSupplier, Func< ); } + /// + /// Creates for querying the database. This is the most versatile method for initializing a new + /// session allowing to pass all configurable options in `traits` argument. + /// + /// Don't forget to or when your work with Evita is finished. + /// EvitaSession is not thread safe! + /// + /// traits to customize the created session + /// new instance of EvitaSession public EvitaClientSession CreateSession(SessionTraits traits) { AssertActive(); @@ -419,6 +571,10 @@ public EvitaClientSession CreateSession(SessionTraits traits) return session; } + /// + /// Verifies this instance is still active. + /// + /// thrown when client instance has already been terminated private void AssertActive() { if (_active == 0) @@ -429,4 +585,4 @@ private void AssertActive() [GeneratedRegex("(\\w+:\\w+:\\w+): (.*)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] private static partial Regex MyRegex(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/EvitaClientSession.cs b/EvitaDB.Client/EvitaClientSession.cs index 979b079..46028df 100644 --- a/EvitaDB.Client/EvitaClientSession.cs +++ b/EvitaDB.Client/EvitaClientSession.cs @@ -29,7 +29,20 @@ namespace EvitaDB.Client; -public class EvitaClientSession : IClientContext, IDisposable +/// +/// Session are created by the clients to envelope a "piece of work" with evitaDB. In web environment it's a good idea +/// to have session per request, in batch processing it's recommended to keep session per "record page" or "transaction". +/// There may be multiple during single session instance life but there is no support +/// for transactional overlap - there may be at most single transaction open in single session. +/// +/// EvitaSession transactions behave like Snapshot +/// transactions. When no transaction is explicitly opened - each query to Evita behaves as one small transaction. Data +/// updates are not allowed without explicitly opened transaction. +/// +/// Don't forget to when your work with Evita is finished. +/// EvitaSession contract is NOT thread safe. +/// +public partial class EvitaClientSession : IClientContext, IDisposable { private static readonly ISchemaMutationConverter CatalogSchemaMutationConverter = new DelegatingLocalCatalogSchemaMutationConverter(); @@ -49,10 +62,7 @@ private static readonly ISchemaMutationConverter _onTerminationCallback; private readonly AtomicReference _transactionAccessor = new(); - private static readonly Regex ErrorMessagePattern = new( - "(\\w+:\\w+:\\w+): (.*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled - ); + private static readonly Regex ErrorMessagePattern = MyRegex(); public bool Active { get; private set; } = true; private long _lastCall; @@ -74,27 +84,46 @@ public EvitaClientSession(EvitaClient evitaClient, EvitaEntitySchemaCache schema _clientId = evitaClient.Configuration.ClientId; } + /// + /// Method creates new a new entity schema and collection for it in the catalog this session is tied to. It returns + /// an that could be used for extending the initial "empty" + /// . + /// + /// If the collection already exists the method returns a builder for entity schema of the already existing + /// entity collection - i.e. this method behaves the same as calling: + /// + /// GetEntitySchema("name")?.SealedEntitySchema.OpenForWrite() + /// + /// type of the collection to define + /// builder for applying more mutations on newly created entity schema public IEntitySchemaBuilder DefineEntitySchema(string entityType) { AssertActive(); ISealedEntitySchema newEntitySchema = ExecuteInTransactionIfPossible(_ => { - GrpcDefineEntitySchemaRequest request = new GrpcDefineEntitySchemaRequest - { - EntityType = entityType - }; + var request = new GrpcDefineEntitySchemaRequest { EntityType = entityType }; - GrpcDefineEntitySchemaResponse response = ExecuteWithEvitaSessionService(evitaSessionService => + var response = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DefineEntitySchema(request) ); - EntitySchema theSchema = EntitySchemaConverter.Convert(response.EntitySchema); + var theSchema = EntitySchemaConverter.Convert(response.EntitySchema); _schemaCache.SetLatestEntitySchema(theSchema); return new EntitySchemaDecorator(GetCatalogSchema, theSchema); }); return newEntitySchema.OpenForWrite(); } + /// + /// Method that is called within the to apply the wanted logic on a channel retrieved + /// from a channel pool. + /// + /// function that holds a logic passed by the caller + /// return type of the function + /// result of the applied function + /// thrown when no session has been passed to the server when one is required + /// error caused by invalid operations executed by the programmer + /// error caused by internal error in the database private T ExecuteWithEvitaSessionService( Func evitaSessionServiceClient) { @@ -103,7 +132,7 @@ private T ExecuteWithEvitaSessionService( _clientId, () => { - ChannelInvoker channel = _channelPool.GetChannel(); + var channel = _channelPool.GetChannel(); try { SessionIdHolder.SetSessionId(CatalogName, SessionId.ToString()); @@ -112,8 +141,8 @@ private T ExecuteWithEvitaSessionService( } catch (RpcException rpcException) { - StatusCode statusCode = rpcException.StatusCode; - string description = rpcException.Status.Detail; + var statusCode = rpcException.StatusCode; + var description = rpcException.Status.Detail; if (statusCode == StatusCode.Unauthenticated) { // close session and rethrow @@ -122,7 +151,7 @@ private T ExecuteWithEvitaSessionService( } else if (statusCode == StatusCode.InvalidArgument) { - Match expectedFormat = ErrorMessagePattern.Match(description); + var expectedFormat = ErrorMessagePattern.Match(description); if (expectedFormat.Success) { throw EvitaInvalidUsageException.CreateExceptionWithErrorCode( @@ -136,7 +165,7 @@ private T ExecuteWithEvitaSessionService( } else { - Match expectedFormat = ErrorMessagePattern.Match(description); + var expectedFormat = ErrorMessagePattern.Match(description); if (expectedFormat.Success) { throw EvitaInternalError.CreateExceptionWithErrorCode( @@ -173,12 +202,39 @@ private T ExecuteWithEvitaSessionService( }); } + /// + /// If supports transactions method + /// executes application `logic` in current session and commits the transaction at the end. Transaction is + /// automatically roll-backed when exception is thrown from the `logic` scope. Changes made by the updating logic are + /// visible only within update function. Other threads outside the logic function work with non-changed data until + /// transaction is committed to the index. + /// + /// When catalog doesn't support transactions application `logic` is immediately applied to the index data and logic + /// operates in a read + /// uncommitted mode. Application `logic` can only append new entities in non-transactional mode. + /// + /// logic to execute + /// return type + /// result of logic that possibly has been executed in transaction public T Execute(Func logic) { AssertActive(); return ExecuteInTransactionIfPossible(logic); } + /// + /// If supports transactions method + /// executes application `logic` in current session and commits the transaction at the end. Transaction is + /// automatically roll-backed when exception is thrown from the `logic` scope. Changes made by the updating logic are + /// visible only within update function. Other threads outside the logic function work with non-changed data until + /// transaction is committed to the index. + /// + /// When catalog doesn't support transactions application `logic` is immediately applied to the index data and logic + /// operates in a read + /// uncommitted mode. Application `logic` can only append new entities in non-transactional mode. + /// + /// logic to execute + /// result of logic that possibly has been executed in transaction public void Execute(Action logic) { AssertActive(); @@ -191,27 +247,44 @@ public void Execute(Action logic) ); } + /// + /// Returns list of all entity types available in this catalog. + /// public ISet GetAllEntityTypes() { AssertActive(); - GrpcEntityTypesResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.GetAllEntityTypes(new Empty()) ); return new HashSet(grpcResponse.EntityTypes); } + /// + /// Method executes query on and returns zero or exactly one entity result. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// type of classifier that should be returned from the method call + /// a computed response + /// thrown when invalid query was passed public TS? QueryOne(Query query) where TS : class, IEntityClassifier { AssertActive(); AssertRequestMakesSense(query); - StringWithParameters stringWithParameters = query.ToStringWithParametersExtraction(); + var stringWithParameters = query.ToStringWithParametersExtraction(); var request = new GrpcQueryRequest { Query = stringWithParameters.Query, - PositionalQueryParams = {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + PositionalQueryParams = { stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) } }; - GrpcQueryOneResponse grpcResponse = ExecuteWithEvitaSessionService(session => session.QueryOne(request)); + var grpcResponse = ExecuteWithEvitaSessionService(session => session.QueryOne(request)); if (typeof(IEntityReference).IsAssignableFrom(typeof(TS))) { @@ -239,22 +312,38 @@ public ISet GetAllEntityTypes() throw new EvitaInvalidUsageException("Unsupported return type `" + typeof(TS) + "`!"); } + /// + /// Method executes query on and returns simplified list of results. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. This method will throw out all possible extra results from, because there is + /// no way how to propagate them in return value. If you require extra results or paginated list use + /// the method. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// type of classifier that should be returned from the method call + /// a computed response + /// thrown when invalid query was passed public IList QueryList(Query query) where TS : IEntityClassifier { AssertActive(); AssertRequestMakesSense(query); - StringWithParameters stringWithParameters = query.ToStringWithParametersExtraction(); + var stringWithParameters = query.ToStringWithParametersExtraction(); var request = new GrpcQueryRequest { Query = stringWithParameters.Query, - PositionalQueryParams = {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + PositionalQueryParams = { stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) } }; - GrpcQueryListResponse grpcResponse = ExecuteWithEvitaSessionService(session => session.QueryList(request)); + var grpcResponse = ExecuteWithEvitaSessionService(session => session.QueryList(request)); if (typeof(IEntityReference).IsAssignableFrom(typeof(TS))) { - return (IList) EntityConverter.ToEntityReferences(grpcResponse.EntityReferences); + return (IList)EntityConverter.ToEntityReferences(grpcResponse.EntityReferences); } if (typeof(ISealedEntity).IsAssignableFrom(typeof(TS))) @@ -274,26 +363,38 @@ public IList QueryList(Query query) where TS : IEntityClassifier throw new EvitaInvalidUsageException("Unsupported return type `" + typeof(TS) + "`!"); } + /// + /// Method executes query on data and returns result. Because result is generic and may contain + /// different data as its contents (based on input query), additional parameter `expectedType` is passed. This parameter + /// allows to check whether passed response contains the expected type of data before returning it back to the client. + /// This should prevent late ClassCastExceptions on the client side. + /// + /// query to process + /// requested response type + /// expected type of returned entities + /// a requested result type + /// thrown when invalid query was passed + /// public T Query(Query query) where TS : IEntityClassifier where T : EvitaResponse { AssertActive(); AssertRequestMakesSense(query); - StringWithParameters stringWithParameters = query.ToStringWithParametersExtraction(); + var stringWithParameters = query.ToStringWithParametersExtraction(); var request = new GrpcQueryRequest { Query = stringWithParameters.Query, - PositionalQueryParams = {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + PositionalQueryParams = { stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) } }; - GrpcQueryResponse grpcResponse = ExecuteWithEvitaSessionService(session => session.Query(request)); - IEvitaResponseExtraResult[] extraResults = GetEvitaResponseExtraResults( + var grpcResponse = ExecuteWithEvitaSessionService(session => session.Query(request)); + var extraResults = GetEvitaResponseExtraResults( grpcResponse, new EvitaRequest(query, DateTimeOffset.Now) ); if (typeof(IEntityReference).IsAssignableFrom(typeof(TS))) { - IDataChunk recordPage = ResponseConverter.ConvertToDataChunk( + var recordPage = ResponseConverter.ConvertToDataChunk( grpcResponse, grpcRecordPage => EntityConverter.ToEntityReferences(grpcRecordPage.EntityReferences) ); @@ -302,7 +403,7 @@ public T Query(Query query) where TS : IEntityClassifier where T : EvitaR if (typeof(ISealedEntity).IsAssignableFrom(typeof(TS))) { - IDataChunk recordPage = ResponseConverter.ConvertToDataChunk( + var recordPage = ResponseConverter.ConvertToDataChunk( grpcResponse, grpcRecordPage => EntityConverter.ToEntities( grpcRecordPage.SealedEntities.ToList(), @@ -321,13 +422,21 @@ public T Query(Query query) where TS : IEntityClassifier where T : EvitaR throw new EvitaInvalidUsageException("Unsupported return type `" + typeof(TS) + "`!"); } + /// + /// Method executes query on data and returns result. + /// + /// input query, + /// for creation use or similar methods + /// for defining constraint use {@link QueryConstraints} static methods + /// full response data transfer object with all available data + /// public EvitaResponse QuerySealedEntity(Query query) { if (query.Require == null) { return Query( IQueryConstraints.Query( - query.Entities, + query.Collection, query.FilterBy, query.OrderBy, Require(EntityFetch()) @@ -340,11 +449,11 @@ public EvitaResponse QuerySealedEntity(Query query) { return Query( IQueryConstraints.Query( - query.Entities, + query.Collection, query.FilterBy, query.OrderBy, - (Require) query.Require.GetCopyWithNewChildren( - new IRequireConstraint?[] {Require(EntityFetch())} + (Require)query.Require.GetCopyWithNewChildren( + new IRequireConstraint?[] { Require(EntityFetch()) } .Concat(query.Require.Children).ToArray(), query.Require.AdditionalChildren ) @@ -355,14 +464,29 @@ public EvitaResponse QuerySealedEntity(Query query) return Query(query); } + /// + /// Method executes query on data and returns result. + /// + /// input query, + /// for creation use or similar methods + /// for defining constraint use {@link QueryConstraints} static methods + /// response data transfer object only primary keys and and entity types included + /// public EvitaResponse QueryEntityReference(Query query) { return Query(query); } + /// + /// Method alters one of the of the catalog this session is tied to. All + /// mutations will be applied or none of them (method call is atomic). The method call is idempotent - it means that + /// when the method is called multiple times with same mutations the changes occur only once. + /// + /// the builder that contains the mutations in the entity schema + /// possibly updated body of the or the original schema if no change occurred public ISealedEntitySchema UpdateAndFetchEntitySchema(IEntitySchemaBuilder entitySchemaBuilder) { - ModifyEntitySchemaMutation? schemaMutation = entitySchemaBuilder.ToMutation(); + var schemaMutation = entitySchemaBuilder.ToMutation(); if (schemaMutation is not null) { return UpdateAndFetchEntitySchema(schemaMutation); @@ -371,10 +495,13 @@ public ISealedEntitySchema UpdateAndFetchEntitySchema(IEntitySchemaBuilder entit return GetEntitySchemaOrThrow(entitySchemaBuilder.Name); } + /// + /// This internal method will physically call over the network and fetch actual {@link EntitySchema}. + /// private EntitySchema? FetchEntitySchema(string entityType) { - GrpcEntitySchemaResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => - evitaSessionService.GetEntitySchema(new GrpcEntitySchemaRequest {EntityType = entityType}) + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + evitaSessionService.GetEntitySchema(new GrpcEntitySchemaRequest { EntityType = entityType }) ); if (grpcResponse.EntitySchema is null) { @@ -384,20 +511,27 @@ public ISealedEntitySchema UpdateAndFetchEntitySchema(IEntitySchemaBuilder entit return EntitySchemaConverter.Convert(grpcResponse.EntitySchema); } + /// + /// Method returns entity by its type and primary key in requested form of completeness. This method allows quick + /// access to the entity contents when primary key is known. + /// public ISealedEntity? GetEntity(string entityType, int primaryKey, params IEntityContentRequire[] require) { AssertActive(); - StringWithParameters stringWithParameters = ToStringWithParameterExtraction(require); - GrpcEntityResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var stringWithParameters = ToStringWithParameterExtraction(require); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.GetEntity( new GrpcEntityRequest { EntityType = entityType, PrimaryKey = primaryKey, Require = stringWithParameters.Query, - PositionalQueryParams = {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + PositionalQueryParams = + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -421,36 +555,40 @@ public ISealedEntitySchema UpdateAndFetchEntitySchema(IEntitySchemaBuilder entit : null; } + /// + /// Method returns count of all entities stored in the collection of passed entity type. + /// public int GetEntityCollectionSize(string entityType) { AssertActive(); - GrpcEntityCollectionSizeResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.GetEntityCollectionSize( - new GrpcEntityCollectionSizeRequest - { - EntityType = entityType - } + new GrpcEntityCollectionSizeRequest { EntityType = entityType } ) ); return grpcResponse.Size; } + /// + /// Method alters the of the catalog this session is tied to. The method is equivalent + /// to but accepts the original builder. This method variant + /// is present as a shortcut option for the developers. + /// + /// the builder that contains the mutations in the catalog schema + /// version of the altered schema or current version if no modification occurred. public int UpdateCatalogSchema(params ILocalCatalogSchemaMutation[] schemaMutation) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - List + var grpcSchemaMutations = schemaMutation .Select(CatalogSchemaMutationConverter.Convert) .ToList(); - GrpcUpdateCatalogSchemaRequest request = new GrpcUpdateCatalogSchemaRequest - { - SchemaMutations = {grpcSchemaMutations} - }; + var request = new GrpcUpdateCatalogSchemaRequest { SchemaMutations = { grpcSchemaMutations } }; - GrpcUpdateCatalogSchemaResponse response = ExecuteWithEvitaSessionService(evitaSessionService => + var response = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpdateCatalogSchema(request) ); @@ -459,17 +597,20 @@ public int UpdateCatalogSchema(params ILocalCatalogSchemaMutation[] schemaMutati }); } + /// + /// Deletes entire collection of entities along with its schema. After this operation there will be nothing left + /// of the data that belong to the specified entity type. + /// + /// type of the entity which collection should be deleted + /// TRUE if collection was successfully deleted public bool DeleteCollection(string entityType) { AssertActive(); return ExecuteInTransactionIfPossible( _ => { - GrpcDeleteCollectionResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => - evitaSessionService.DeleteCollection(new GrpcDeleteCollectionRequest - { - EntityType = entityType - } + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + evitaSessionService.DeleteCollection(new GrpcDeleteCollectionRequest { EntityType = entityType } ) ); _schemaCache.RemoveLatestEntitySchema(entityType); @@ -478,66 +619,96 @@ public bool DeleteCollection(string entityType) ); } + /// + /// Method removes existing hierarchical entity in collection by its primary key. Method also removes all entities + /// of the same type that are transitively referencing the removed entity as its parent. All entities of other entity + /// types that reference removed entities in their still keep + /// the data untouched. + /// + /// type of entity to delete + /// primary key of entity to delete + /// number of removed entities public int DeleteEntityAndItsHierarchy(string entityType, int primaryKey) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcDeleteEntityAndItsHierarchyResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntityAndItsHierarchy( - new GrpcDeleteEntityRequest - { - EntityType = entityType, - PrimaryKey = primaryKey - } + new GrpcDeleteEntityRequest { EntityType = entityType, PrimaryKey = primaryKey } ) ); return grpcResponse.DeletedEntities; }); } + /// + /// Method removes existing hierarchical entity in collection by its primary key. Method also removes all entities + /// of the same type that are transitively referencing the removed entity as its parent. All entities of other entity + /// types that reference removed entities in their still keep + /// the data untouched. + /// + /// type of entity to delete + /// primary key of entity to delete + /// additional requirements on the entity to delete + /// number of removed entities public DeletedHierarchy DeleteEntityAndItsHierarchy(string entityType, int primaryKey, params IEntityContentRequire[] require) { return DeleteEntityHierarchyInternal(entityType, primaryKey, require); } + /// + /// Method removes existing entity in collection by its primary key. All entities of other entity types that reference + /// removed entity in their still keep the data untouched. + /// + /// type of the entity to be removed + /// primary key of the entity to be removed + /// true if entity existed and was removed public bool DeleteEntity(string entityType, int primaryKey) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcDeleteEntityResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntity( - new GrpcDeleteEntityRequest - { - EntityType = entityType, - PrimaryKey = primaryKey - } + new GrpcDeleteEntityRequest { EntityType = entityType, PrimaryKey = primaryKey } ) ); return grpcResponse.Entity is not null || grpcResponse.EntityReference is not null; }); } + /// + /// Method removes all entities that match passed query. All entities of other entity types that reference removed + /// entities in their {@link SealedEntity#getReference(String, int)} still keep the data untouched. This variant of + /// the delete by query method allows returning partial of full bodies of the removed entities. + /// + /// Beware: you need to provide or in the query to control the maximum number of removed + /// entities. Otherwise, the default value of maximum of `20` entities to remove will be used. + /// + /// query to specify which entities should be deleted + /// bodies of deleted entities according to public ISealedEntity[] DeleteSealedEntitiesAndReturnBodies(Query query) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - EvitaRequest evitaRequest = new EvitaRequest( + var evitaRequest = new EvitaRequest( query, DateTimeOffset.Now, typeof(ISealedEntity) ); - StringWithParameters stringWithParameters = query.ToStringWithParametersExtraction(); - GrpcDeleteEntitiesResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var stringWithParameters = query.ToStringWithParametersExtraction(); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntities( new GrpcDeleteEntitiesRequest { Query = stringWithParameters.Query, PositionalQueryParams = - {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -555,25 +726,43 @@ public ISealedEntity[] DeleteSealedEntitiesAndReturnBodies(Query query) }); } + /// + /// Method removes existing entity in collection by its primary key. All entities of other entity types that reference + /// removed entity in their still keep the data untouched. + /// + /// type of the entity that should be deleted + /// primary key of the entity that should be deleted + /// specifications to fetch entity to be deleted and returned from the method + /// removed entity fetched according to `require` definition public ISealedEntity? DeleteEntity(string entityType, int primaryKey, params IEntityContentRequire[] require) { return DeleteEntityInternal(entityType, primaryKey, require); } - + /// + /// Method removes all entities that match passed query. All entities of other entity types that reference removed + /// entities in their still keep the data untouched. + /// + /// Beware: you need to provide or in the query to control the maximum number of removed + /// entities. Otherwise, the default value of maximum of `20` entities to remove will be used. + /// + /// query to specify which entities should be deleted + /// number of deleted entities public int DeleteEntities(Query query) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - StringWithParameters stringWithParameters = ToStringWithParameterExtraction(query); - GrpcDeleteEntitiesResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var stringWithParameters = ToStringWithParameterExtraction(query); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntities( new GrpcDeleteEntitiesRequest { Query = stringWithParameters.Query, PositionalQueryParams = - {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -581,19 +770,27 @@ public int DeleteEntities(Query query) }); } + /// + /// Renames entire collection of entities along with its schema. After this operation there will be nothing left + /// of the data that belong to the specified entity type, and entity collection under the new name becomes available. + /// If you need to rename entity collection to a name of existing collection use + /// the method instead. + /// + /// In case exception occurs the original collection (`entityType`) is guaranteed to be untouched, + /// and the `newName` will not be present. + /// + /// current name of the entity collection + /// new name of the entity collection + /// TRUE if collection was successfully renamed public bool RenameCollection(string entityType, string newName) { AssertActive(); return ExecuteInTransactionIfPossible( _ => { - GrpcRenameCollectionResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.RenameCollection( - new GrpcRenameCollectionRequest - { - EntityType = entityType, - NewName = newName - } + new GrpcRenameCollectionRequest { EntityType = entityType, NewName = newName } ) ); _schemaCache.RemoveLatestEntitySchema(entityType); @@ -602,13 +799,24 @@ public bool RenameCollection(string entityType, string newName) ); } + /// + /// Replaces existing entity collection of particular with the contents of the another collection. When this method + /// is successfully finished, the entity collection `entityTypeToBeReplaced` will be known under the name of the + /// `entityTypeToBeReplacedWith` and the original contents of the `entityTypeToBeReplaced` will be purged entirely. + /// + /// In case exception occurs, both the original collection (`entityTypeToBeReplaced`) and replaced collection + /// (`entityTypeToBeReplacedWith`) are guaranteed to be untouched. + /// + /// name of the collection that will be replaced and dropped + /// name of the collection that will become the successor of the original catalog + /// TRUE if collection was successfully replaced public bool ReplaceCollection(string entityTypeToBeReplaced, string entityTypeToBeReplacedWith) { AssertActive(); return ExecuteInTransactionIfPossible( _ => { - GrpcReplaceCollectionResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.ReplaceCollection( new GrpcReplaceCollectionRequest { @@ -624,18 +832,22 @@ public bool ReplaceCollection(string entityTypeToBeReplaced, string entityTypeTo ); } + /// + /// Method alters one of the of the catalog this session is tied to. + /// All mutations will be applied or none of them (method call is atomic). It's also idempotent - it means that + /// when the method is called multiple times with same mutations the changes occur only once. + /// + /// the builder that contains the mutations in the entity schema + /// version of the altered schema or current version if no modification occurred. public int UpdateEntitySchema(ModifyEntitySchemaMutation schemaMutation) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcModifyEntitySchemaMutation grpcSchemaMutation = + var grpcSchemaMutation = ModifyEntitySchemaMutationConverter.Convert(schemaMutation); - GrpcUpdateEntitySchemaRequest request = new GrpcUpdateEntitySchemaRequest - { - SchemaMutation = grpcSchemaMutation - }; - GrpcUpdateEntitySchemaResponse response = ExecuteWithEvitaSessionService(evitaSessionService => + var request = new GrpcUpdateEntitySchemaRequest { SchemaMutation = grpcSchemaMutation }; + var response = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpdateEntitySchema(request) ); _schemaCache.AnalyzeMutations(schemaMutation); @@ -643,47 +855,65 @@ public int UpdateEntitySchema(ModifyEntitySchemaMutation schemaMutation) }); } + /// + /// Method alters one of the of the catalog this session is tied to. + /// The method is equivalent to but accepts the original builder. + /// This method variant is present as a shortcut option for the developers. + /// + /// the builder that contains the mutations in the entity schema + /// version of the altered schema or current version if no modification occurred. public int UpdateEntitySchema(IEntitySchemaBuilder entitySchemaBuilder) { - ModifyEntitySchemaMutation? mutation = entitySchemaBuilder.ToMutation(); + var mutation = entitySchemaBuilder.ToMutation(); return mutation is not null ? UpdateEntitySchema(mutation) : GetEntitySchemaOrThrow(entitySchemaBuilder.Name).Version; } + /// + /// Method alters one of the of the catalog this session is tied to. All + /// mutations will be applied or none of them (method call is atomic). The method call is idempotent - it means that + /// when the method is called multiple times with same mutations the changes occur only once. + /// + /// the builder that contains the mutations in the entity schema + /// possibly updated body of the or the original schema if no change occurred public ISealedEntitySchema UpdateAndFetchEntitySchema(ModifyEntitySchemaMutation schemaMutation) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcModifyEntitySchemaMutation grpcSchemaMutation = + var grpcSchemaMutation = ModifyEntitySchemaMutationConverter.Convert(schemaMutation); - GrpcUpdateEntitySchemaRequest request = new GrpcUpdateEntitySchemaRequest - { - SchemaMutation = grpcSchemaMutation - }; + var request = new GrpcUpdateEntitySchemaRequest { SchemaMutation = grpcSchemaMutation }; - GrpcUpdateAndFetchEntitySchemaResponse response = ExecuteWithEvitaSessionService(evitaSessionService => + var response = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpdateAndFetchEntitySchema(request) ); - EntitySchema updatedSchema = EntitySchemaConverter.Convert(response.EntitySchema); + var updatedSchema = EntitySchemaConverter.Convert(response.EntitySchema); _schemaCache.AnalyzeMutations(schemaMutation); _schemaCache.SetLatestEntitySchema(updatedSchema); return new EntitySchemaDecorator(GetCatalogSchema, updatedSchema); }); } + /// + /// Terminates opened transaction - either by rollback or commit depending on . + /// This method throws exception only when transaction hasn't been opened. + /// public void CloseTransaction() { AssertActive(); - EvitaClientTransaction? transaction = _transactionAccessor.Value; + var transaction = _transactionAccessor.Value; if (transaction is null) throw new UnexpectedTransactionStateException("No transaction has been opened!"); DestroyTransaction(); transaction.Close(); } + /// + /// Destroys transaction reference. + /// private void DestroyTransaction() { _transactionAccessor.GetAndSet(transaction => @@ -691,20 +921,28 @@ private void DestroyTransaction() Assert.IsTrue(transaction is not null, "Transaction unexpectedly not present!"); ExecuteWithEvitaSessionService(session => { - session.CloseTransaction(new GrpcCloseTransactionRequest {Rollback = transaction!.RollbackOnly}); + session.CloseTransaction(new GrpcCloseTransactionRequest { Rollback = transaction!.RollbackOnly }); return true; }); return null; }); } + /// + /// Switches catalog to the state and terminates the Evita session so that next session is + /// operating in the new catalog state. + /// + /// Session is only when the state transition successfully occurs and this is signalized + /// by return value. + /// + /// public bool GoLiveAndClose() { AssertActive(); - GrpcGoLiveAndCloseResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.GoLiveAndClose(new Empty()) ); - bool success = grpcResponse.Success; + var success = grpcResponse.Success; if (success) { CloseInternally(); @@ -713,6 +951,13 @@ public bool GoLiveAndClose() return success; } + /// + /// Terminates Evita session and releases all used resources. This method renders the session unusable and any further + /// calls to this session should end up with + /// + /// This method is idempotent and may be called multiple times. Only first call is really processed and others are + /// ignored. + /// public void Close() { if (Active) @@ -726,6 +971,9 @@ public void Close() } } + /// + /// Method internally closes the session + /// private void CloseInternally() { if (!Active) return; @@ -733,25 +981,29 @@ private void CloseInternally() _onTerminationCallback.Invoke(this); } + /// + /// Method alters the of the catalog this session is tied to. All mutations will be + /// applied or none of them (method call is atomic). The method call is idempotent - it means that when the method + /// is called multiple times with same mutations the changes occur only once. + /// + /// array of mutations that needs to be applied on current version of + /// possibly updated body of the or the original schema if no change occurred public ISealedCatalogSchema UpdateAndFetchCatalogSchema(params ILocalCatalogSchemaMutation[] schemaMutation) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - List grpcSchemaMutations = schemaMutation + var grpcSchemaMutations = schemaMutation .Select(CatalogSchemaMutationConverter.Convert) .ToList(); - GrpcUpdateCatalogSchemaRequest request = new GrpcUpdateCatalogSchemaRequest - { - SchemaMutations = {grpcSchemaMutations} - }; + var request = new GrpcUpdateCatalogSchemaRequest { SchemaMutations = { grpcSchemaMutations } }; - GrpcUpdateAndFetchCatalogSchemaResponse response = ExecuteWithEvitaSessionService(evitaSessionService => + var response = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpdateAndFetchCatalogSchema(request) ); - CatalogSchema updatedCatalogSchema = + var updatedCatalogSchema = CatalogSchemaConverter.Convert(GetEntitySchemaOrThrow, response.CatalogSchema); ISealedCatalogSchema updatedSchema = new CatalogSchemaDecorator(updatedCatalogSchema, GetEntitySchemaOrThrow); @@ -761,6 +1013,13 @@ public ISealedCatalogSchema UpdateAndFetchCatalogSchema(params ILocalCatalogSche }); } + /// + /// Method alters the of the catalog this session is tied to. The method is equivalent + /// to but accepts the original builder. This method + /// variant is present as a shortcut option for the developers. + /// + /// the builder that contains the mutations in the catalog schema + /// possibly updated body of the or the original schema if no change occurred public ISealedCatalogSchema UpdateAndFetchCatalogSchema(ICatalogSchemaBuilder catalogSchemaBuilder) { Assert.IsTrue( @@ -768,12 +1027,19 @@ public ISealedCatalogSchema UpdateAndFetchCatalogSchema(ICatalogSchemaBuilder ca "Schema builder targets `" + catalogSchemaBuilder.Name + "` catalog, but the session targets `" + CatalogName + "` catalog!" ); - ModifyCatalogSchemaMutation? modifyCatalogSchemaMutation = catalogSchemaBuilder.ToMutation(); + var modifyCatalogSchemaMutation = catalogSchemaBuilder.ToMutation(); return modifyCatalogSchemaMutation is not null ? UpdateAndFetchCatalogSchema(modifyCatalogSchemaMutation.SchemaMutations) : GetCatalogSchema(); } + /// + /// Method alters the {@link CatalogSchemaContract} of the catalog this session is tied to. The method is equivalent + /// to but accepts the original builder. This method variant + /// is present as a shortcut option for the developers. + /// + /// the builder that contains the mutations in the catalog schema + /// version of the altered schema or current version if no modification occurred. public int UpdateCatalogSchema(ICatalogSchemaBuilder catalogSchemaBuilder) { Assert.IsTrue( @@ -781,12 +1047,18 @@ public int UpdateCatalogSchema(ICatalogSchemaBuilder catalogSchemaBuilder) "Schema builder targets `" + catalogSchemaBuilder.Name + "` catalog, but the session targets `" + CatalogName + "` catalog!" ); - ModifyCatalogSchemaMutation? modifyCatalogSchemaMutation = catalogSchemaBuilder.ToMutation(); + var modifyCatalogSchemaMutation = catalogSchemaBuilder.ToMutation(); return modifyCatalogSchemaMutation is not null ? UpdateCatalogSchema(modifyCatalogSchemaMutation.SchemaMutations) : GetCatalogSchema().Version; } + /// + /// Extracts extra results from gRPC response. + /// + /// grpc response received from the server + /// instance of EvitaRequest required for correct deserialization + /// private IEvitaResponseExtraResult[] GetEvitaResponseExtraResults(GrpcQueryResponse grpcResponse, EvitaRequest evitaRequest) { @@ -802,12 +1074,18 @@ private IEvitaResponseExtraResult[] GetEvitaResponseExtraResults(GrpcQueryRespon : Array.Empty(); } + /// + /// Returns catalog schema of the catalog this session is connected to. + /// public ISealedCatalogSchema GetCatalogSchema() { AssertActive(); return _schemaCache.GetLatestCatalogSchema(FetchCatalogSchema, GetEntitySchema); } + /// + /// Returns catalog schema of the catalog this session is connected to. + /// public ISealedCatalogSchema GetCatalogSchema(EvitaClient evita) { AssertActive(); @@ -825,9 +1103,12 @@ public ISealedCatalogSchema GetCatalogSchema(EvitaClient evita) ); } + /// + /// This internal method will physically call over the network and fetch actual . + /// private CatalogSchema FetchCatalogSchema() { - GrpcCatalogSchemaResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.GetCatalogSchema(new Empty()) ); return CatalogSchemaConverter.Convert( @@ -835,70 +1116,96 @@ private CatalogSchema FetchCatalogSchema() ); } + /// + /// Returns schema definition for entity of specified type or throws a standardized exception. + /// public ISealedEntitySchema GetEntitySchemaOrThrow(string entityType) { AssertActive(); return GetEntitySchema(entityType) ?? throw new CollectionNotFoundException(entityType); } + /// + /// Returns schema definition for entity of specified type. + /// public ISealedEntitySchema? GetEntitySchema(string entityType) { AssertActive(); return _schemaCache.GetLatestEntitySchema(entityType, FetchEntitySchema, GetCatalogSchema); } + /// + /// Method inserts to or updates entity according to passed entity builder. Direct link to + /// + /// builder for applying more mutations to the entity public EntityReference UpsertEntity(IEntityBuilder entityBuilder) { - IEntityMutation? mutation = entityBuilder.ToMutation(); + var mutation = entityBuilder.ToMutation(); return mutation is not null ? UpsertEntity(mutation) : new EntityReference(entityBuilder.Type, entityBuilder.PrimaryKey); } + /// + /// Method inserts to or updates entity in collection according to passed set of mutations. + /// + /// list of mutation snippets that alter or form the entity public EntityReference UpsertEntity(IEntityMutation entityMutation) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcEntityMutation grpcEntityMutation = EntityMutationConverter.Convert(entityMutation); - GrpcUpsertEntityResponse grpcResult = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcEntityMutation = EntityMutationConverter.Convert(entityMutation); + var grpcResult = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpsertEntity( - new GrpcUpsertEntityRequest - { - EntityMutation = grpcEntityMutation - } + new GrpcUpsertEntityRequest { EntityMutation = grpcEntityMutation } ) ); - GrpcEntityReference grpcReference = grpcResult.EntityReference; + var grpcReference = grpcResult.EntityReference; return new EntityReference( grpcReference.EntityType, grpcReference.PrimaryKey ); }); } + /// + /// Shorthand method for that accepts that can produce + /// mutation. + /// + /// that contains changed entity state + /// require constraints to specify richness of returned entity + /// modified entity fetched according to `require` definition public ISealedEntity UpsertAndFetchEntity(IEntityBuilder entityBuilder, params IEntityContentRequire[] require) { - IEntityMutation? mutation = entityBuilder.ToMutation(); + var mutation = entityBuilder.ToMutation(); return mutation is not null ? UpsertAndFetchEntity(mutation, require) : GetEntityOrThrow(entityBuilder.Type, entityBuilder.PrimaryKey!.Value, require); } + /// + /// Method inserts to or updates entity in collection according to passed set of mutations. + /// + /// list of mutation snippets that alter or form the entity + /// require constraints to specify richness of returned entity + /// modified entity fetched according to `require` definition public ISealedEntity UpsertAndFetchEntity(IEntityMutation entityMutation, params IEntityContentRequire[] require) { AssertActive(); return ExecuteInTransactionIfPossible(_ => { - GrpcEntityMutation grpcEntityMutation = EntityMutationConverter.Convert(entityMutation); - StringWithParameters stringWithParameters = ToStringWithParameterExtraction(require); - GrpcUpsertEntityResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcEntityMutation = EntityMutationConverter.Convert(entityMutation); + var stringWithParameters = ToStringWithParameterExtraction(require); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.UpsertEntity( new GrpcUpsertEntityRequest { EntityMutation = grpcEntityMutation, Require = stringWithParameters.Query, PositionalQueryParams = - {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -920,14 +1227,22 @@ public ISealedEntity UpsertAndFetchEntity(IEntityMutation entityMutation, params }); } + /// + /// Return entity specified by passed constraints or throws exception when no entity is found. + /// public ISealedEntity GetEntityOrThrow(string type, int primaryKey, params IEntityContentRequire[] require) { - ISealedEntity? entity = GetEntity(type, primaryKey, require); + var entity = GetEntity(type, primaryKey, require); return entity ?? throw new EvitaInvalidUsageException("Entity `" + type + "` with id `" + primaryKey + "` doesn't exist!"); } + /// + /// Creates entity builder for new entity without specified primary key needed to be inserted to the collection. + /// + /// type of the entity that should be created + /// builder instance to be filled up and stored via public IEntityBuilder CreateNewEntity(string entityType) { AssertActive(); @@ -937,7 +1252,7 @@ public IEntityBuilder CreateNewEntity(string entityType) IEntitySchema entitySchema; if (GetCatalogSchema().CatalogEvolutionModes.Contains(CatalogEvolutionMode.AddingEntityTypes)) { - ISealedEntitySchema? schema = GetEntitySchema(entityType); + var schema = GetEntitySchema(entityType); entitySchema = schema is not null ? schema : EntitySchema.InternalBuild(entityType); } else @@ -950,6 +1265,13 @@ public IEntityBuilder CreateNewEntity(string entityType) ); } + /// + /// Creates entity builder for new entity with externally defined primary key needed to be inserted to + /// the collection. + /// + /// type of the entity that should be created + /// externally assigned primary key for the entity + /// builder instance to be filled up and stored via public IEntityBuilder CreateNewEntity(string entityType, int primaryKey) { AssertActive(); @@ -959,7 +1281,7 @@ public IEntityBuilder CreateNewEntity(string entityType, int primaryKey) IEntitySchema entitySchema; if (GetCatalogSchema().CatalogEvolutionModes.Contains(CatalogEvolutionMode.AddingEntityTypes)) { - ISealedEntitySchema? schema = GetEntitySchema(entityType); + var schema = GetEntitySchema(entityType); entitySchema = schema is not null ? schema : EntitySchema.InternalBuild(entityType); } else @@ -972,6 +1294,9 @@ public IEntityBuilder CreateNewEntity(string entityType, int primaryKey) ); } + /// + /// Initializes transaction reference. + /// private EvitaClientTransaction CreateAndInitTransaction() { if (!_sessionTraits.IsReadWrite()) @@ -985,11 +1310,11 @@ private EvitaClientTransaction CreateAndInitTransaction() " doesn't support transactions yet. Call `goLiveAndClose()` method first!"); } - GrpcOpenTransactionResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.OpenTransaction(new Empty()) ); - EvitaClientTransaction tx = new EvitaClientTransaction(this, grpcResponse.TransactionId); + var tx = new EvitaClientTransaction(this, grpcResponse.TransactionId); _transactionAccessor.GetAndSet(transaction => { Assert.IsPremiseValid(transaction == null, "Transaction unexpectedly found!"); @@ -1003,11 +1328,18 @@ private EvitaClientTransaction CreateAndInitTransaction() return tx; } + /// + /// Executes passed lambda in existing transaction or throws exception. + /// + /// logic to apply + /// return type of the passed logic + /// if transaction is not open + /// result of passed logic private T ExecuteInTransactionIfPossible(Func logic) { if (_transactionAccessor.Value == null && CatalogState == CatalogState.Alive) { - using EvitaClientTransaction newTransaction = CreateAndInitTransaction(); + using var newTransaction = CreateAndInitTransaction(); try { return logic.Invoke(this); @@ -1033,28 +1365,84 @@ private T ExecuteInTransactionIfPossible(Func logic) } } + /// + /// Method executes query on and returns zero or exactly one entity result. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// a computed response + /// thrown when invalid query was passed public EntityReference? QueryOneEntityReference(Query query) { return QueryOne(query); } + /// + /// Method executes query on and returns zero or exactly one entity result. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// a computed response + /// thrown when invalid query was passed public ISealedEntity? QueryOneSealedEntity(Query query) { return QueryOne(query); } + /// + /// Method executes query on and returns simplified list of results. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. This method will throw out all possible extra results from, because there is + /// no way how to propagate them in return value. If you require extra results or paginated list use + /// the method. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// a computed response + /// thrown when invalid query was passed public IList QueryListOfEntityReferences(Query query) { return QueryList(query); } + /// + /// Method executes query on and returns simplified list of results. Method + /// behaves exactly the same as but verifies the count of returned results and + /// translates it to simplified return type. This method will throw out all possible extra results from, because there is + /// no way how to propagate them in return value. If you require extra results or paginated list use + /// the method. + /// + /// Because result is generic and may contain different data as its contents (based on input query), additional + /// parameter `expectedType` is passed. This parameter allows to check whether passed response contains the expected + /// type of data before returning it back to the client. This should prevent late ClassCastExceptions on the client + /// side. + /// + /// query to process + /// a computed response + /// thrown when invalid query was passed public IList QueryListOfSealedEntities(Query query) { if (query.Require == null) { return QueryList( IQueryConstraints.Query( - query.Entities, + query.Collection, query.FilterBy, query.OrderBy, Require(EntityFetch()) @@ -1067,11 +1455,11 @@ public IList QueryListOfSealedEntities(Query query) { return QueryList( IQueryConstraints.Query( - query.Entities, + query.Collection, query.FilterBy, query.OrderBy, - (Require) query.Require.GetCopyWithNewChildren( - new IRequireConstraint?[] {Require(EntityFetch())}.Concat(query.Require.Children).ToArray(), + (Require)query.Require.GetCopyWithNewChildren( + new IRequireConstraint?[] { Require(EntityFetch()) }.Concat(query.Require.Children).ToArray(), query.Require.AdditionalChildren ) ) @@ -1081,6 +1469,11 @@ public IList QueryListOfSealedEntities(Query query) return QueryList(query); } + /// + /// Asserts if the request makes sense. This method is used to prevent invalid usage of the API. + /// This is a basic check that is performed on the client side to unnecessary calls to the server. + /// It verified expected types and requested types and throws exception if they don't match. + /// private static void AssertRequestMakesSense(Query query) where T : IEntityClassifier { if (typeof(ISealedEntity).IsAssignableFrom(typeof(T)) && @@ -1099,8 +1492,8 @@ private static void AssertRequestMakesSense(Query query) where T : IEntityCla AssertActive(); return ExecuteInTransactionIfPossible(_ => { - StringWithParameters stringWithParameters = ToStringWithParameterExtraction(require); - GrpcDeleteEntityResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var stringWithParameters = ToStringWithParameterExtraction(require); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntity( new GrpcDeleteEntityRequest { @@ -1108,7 +1501,9 @@ private static void AssertRequestMakesSense(Query query) where T : IEntityCla PrimaryKey = primaryKey, Require = stringWithParameters.Query, PositionalQueryParams = - {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -1141,8 +1536,8 @@ params IEntityContentRequire[] require AssertActive(); return ExecuteInTransactionIfPossible(_ => { - StringWithParameters stringWithParameters = ToStringWithParameterExtraction(require); - GrpcDeleteEntityAndItsHierarchyResponse grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => + var stringWithParameters = ToStringWithParameterExtraction(require); + var grpcResponse = ExecuteWithEvitaSessionService(evitaSessionService => evitaSessionService.DeleteEntityAndItsHierarchy( new GrpcDeleteEntityRequest { @@ -1150,7 +1545,9 @@ params IEntityContentRequire[] require PrimaryKey = primaryKey, Require = stringWithParameters.Query, PositionalQueryParams = - {stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam)} + { + stringWithParameters.Parameters.Select(QueryConverter.ConvertQueryParam) + } } ) ); @@ -1177,6 +1574,10 @@ grpcResponse.DeletedRootEntity is not null }); } + /// + /// Assert that checks if the session is active. If not, it throws . + /// + /// thrown when this session is not active private void AssertActive() { if (Active) @@ -1189,6 +1590,13 @@ private void AssertActive() } } + /// + /// Terminates Evita session and releases all used resources. This method renders the session unusable and any further + /// calls to this session should end up with + /// + /// This method is idempotent and may be called multiple times. Only first call is really processed and others are + /// ignored. + /// public void Dispose() { if (Active) @@ -1198,4 +1606,7 @@ public void Dispose() CloseInternally(); } -} \ No newline at end of file + + [GeneratedRegex("(\\w+:\\w+:\\w+): (.*)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] + private static partial Regex MyRegex(); +} diff --git a/EvitaDB.Client/Models/Data/AssociatedDataKey.cs b/EvitaDB.Client/Models/Data/AssociatedDataKey.cs index 50cf4e7..1f86fd7 100644 --- a/EvitaDB.Client/Models/Data/AssociatedDataKey.cs +++ b/EvitaDB.Client/Models/Data/AssociatedDataKey.cs @@ -24,6 +24,6 @@ public int CompareTo(AssociatedDataKey? other) public override string ToString() { - return $"AssociatedDataKey[associatedDataName={AssociatedDataName}, locale={(Locale == null ? "null" : Locale.TwoLetterISOLanguageName)}]"; + return AssociatedDataName + (Locale == null ? "" : ":" + Locale.TwoLetterISOLanguageName); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IAssociatedData.cs b/EvitaDB.Client/Models/Data/IAssociatedData.cs index 924fec7..ca5eb84 100644 --- a/EvitaDB.Client/Models/Data/IAssociatedData.cs +++ b/EvitaDB.Client/Models/Data/IAssociatedData.cs @@ -1,31 +1,122 @@ using System.Globalization; +using EvitaDB.Client.Exceptions; using EvitaDB.Client.Models.Schemas; using EvitaDB.Client.Utils; namespace EvitaDB.Client.Models.Data; +/// +/// This interface prescribes a set of methods that must be implemented by the object, that maintains set of associated data. +/// public interface IAssociatedData { + /// + /// Returns true if entity associated data were fetched along with the entity. Calling this method before calling any + /// other method that requires associated data to be fetched will allow you to avoid . + /// bool AssociatedDataAvailable(); + /// + /// Returns true if entity associated data in specified locale were fetched along with the entity. Calling this + /// method before calling any other method that requires associated data to be fetched will allow you to avoid + /// . + /// bool AssociatedDataAvailable(CultureInfo locale); + /// + /// Returns true if entity associated data of particular name was fetched along with the entity. Calling this method + /// before calling any other method that requires associated data to be fetched will allow you to avoid + /// . + /// bool AssociatedDataAvailable(string associatedDataName); + /// + /// Returns true if entity associated data of particular name in particular locale was fetched along with the entity. + /// Calling this method before calling any other method that requires associated data to be fetched will allow you to + /// avoid . + /// bool AssociatedDataAvailable(string associatedDataName, CultureInfo locale); + /// + /// Returns value associated with the key or null when the associatedData is missing. + /// object? GetAssociatedData(string associatedDataName); + /// + /// Returns value associated with the key or null when the associatedData is missing. + /// T? GetAssociatedData(string associatedDataName) where T : class; + /// + /// Returns value associated with the key or null when the associatedData is missing. + /// object? GetAssociatedData(string associatedDataName, CultureInfo locale); + /// + /// Returns value associated with the key or null when the associatedData is missing. + /// T? GetAssociatedData(string associatedDataName, CultureInfo locale) where T : class; + /// + /// Returns array of values associated with the key or null when the associatedData is missing. + /// object[]? GetAssociatedDataArray(string associatedDataName); + /// + /// Returns array of values associated with the key or null when the associatedData is missing. + /// When localized associatedData is not found it is looked up in generic (non-localized) associatedDatas. This makes this + /// method safest way how to lookup for associatedData if caller doesn't know whether it is localized or not. + /// object[]? GetAssociatedDataArray(string associatedDataName, CultureInfo locale); + /// + /// Returns array of values associated with the key or null when the associated data is missing. + /// + /// Method returns wrapper dto for the associated data that contains information about the associated data version + /// and state. + /// AssociatedDataValue? GetAssociatedDataValue(string associatedDataName); + /// + /// Returns array of values associated with the key or null when the associated data is missing. + /// When localized associated data is not found it is looked up in generic (non-localized) associated data. This + /// makes this method safest way how to lookup for associated data if caller doesn't know whether it is localized + /// or not. + /// + /// Method returns wrapper dto for the associated data that contains information about the associated data version + /// and state. + /// AssociatedDataValue? GetAssociatedDataValue(string associatedDataName, CultureInfo locale); + /// + /// Returns array of values associated with the key or null when the associated data is missing. + /// When localized associated data is not found it is looked up in generic (non-localized) associated data. + /// This makes this method the safest way how to lookup for associated data if caller doesn't know whether it is + /// localized or not. + /// + /// Method returns wrapper dto for the associated data that contains information about the associated data version + /// and state. + /// AssociatedDataValue? GetAssociatedDataValue(AssociatedDataKey associatedDataKey); + /// + /// Returns definition for the associatedData of specified name. + /// IAssociatedDataSchema? GetAssociatedDataSchema(string associatedDataName); + /// + /// Returns set of all keys registered in this associatedData set. The result set is not limited to the set + /// of currently fetched associated data. + /// ISet GetAssociatedDataNames(); + /// + /// Returns set of all keys (combination of associated data name and locale) registered in this associated data. + /// ISet GetAssociatedDataKeys(); + /// + /// Returns collection of all values present in this object. + /// ICollection GetAssociatedDataValues(); + /// + /// Returns collection of all values of `associatedDataName` present in this object. This method has usually sense + /// only when the associated data is present in multiple localizations. + /// ICollection GetAssociatedDataValues(string associatedDataName); + /// + /// Method returns set of locales used in the localized associated data. The result set is not limited to the set + /// of currently fetched associated data. + /// ISet GetAssociatedDataLocales(); + /// + /// Returns true if single associated data differs between first and second instance. + /// static bool AnyAssociatedDataDifferBetween(IAssociatedData first, IAssociatedData second) { ICollection thisValues = first.GetAssociatedDataValues(); @@ -51,4 +142,4 @@ static bool AnyAssociatedDataDifferBetween(IAssociatedData first, IAssociatedDat return QueryUtils.ValueDiffers(thisValue, otherValue); }); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IAttributes.cs b/EvitaDB.Client/Models/Data/IAttributes.cs index b7958e3..20ec00c 100644 --- a/EvitaDB.Client/Models/Data/IAttributes.cs +++ b/EvitaDB.Client/Models/Data/IAttributes.cs @@ -1,29 +1,127 @@ using System.Globalization; +using EvitaDB.Client.Exceptions; using EvitaDB.Client.Models.Schemas; using EvitaDB.Client.Utils; namespace EvitaDB.Client.Models.Data; -public interface IAttributes where TS : IAttributeSchema +/// +/// This interface prescribes a set of methods that must be implemented by the object, that maintains set of attributes. +/// +public interface IAttributes where TS : IAttributeSchema { + /// + /// Returns true if entity attributes were fetched along with the entity. Calling this method before calling any + /// other method that requires attributes to be fetched will allow you to avoid . + /// bool AttributesAvailable(); + /// + /// Returns true if entity attributes were fetched in specified locale along with the entity. Calling this method + /// before calling any other method that requires attributes to be fetched will allow you to avoid + /// bool AttributesAvailable(CultureInfo locale); + /// + /// Returns true if entity attribute of particular name was fetched along with the entity. Calling this method + /// before calling any other method that requires attributes to be fetched will allow you to avoid + /// bool AttributeAvailable(string attributeName); + /// + /// Returns true if entity attribute of particular name in particular locale was fetched along with the entity. + /// Calling this method before calling any other method that requires attributes to be fetched will allow you to avoid + /// + /// + /// + /// + /// bool AttributeAvailable(string attributeName, CultureInfo locale); + /// + /// Returns value associated with the key or null when the attribute is missing. + /// + /// // when the name attribute is of type string + /// string name = entity.GetAttribute("name"); + /// + /// object? GetAttribute(string attributeName); + /// + /// Returns value associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// + /// // when the name attribute is of type string + /// string name = entity.GetAttribute("name", new CultureInfo("en-US")); + /// + /// object? GetAttribute(string attributeName, CultureInfo locale); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// object[]? GetAttributeArray(string attributeName); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// object[]? GetAttributeArray(string attributeName, CultureInfo locale); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// + /// Method returns wrapper dto for the attribute that contains information about the attribute version and state. + /// AttributeValue? GetAttributeValue(string attributeName); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// + /// Method returns wrapper dto for the attribute that contains information about the attribute version and state. + /// AttributeValue? GetAttributeValue(string attributeName, CultureInfo locale); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// + /// Method returns wrapper dto for the attribute that contains information about the attribute version and state. + /// AttributeValue? GetAttributeValue(AttributeKey attributeKey); + /// + /// Returns definition for the attribute of specified name. + /// TS? GetAttributeSchema(string attributeName); + /// + /// Returns set of all attribute names registered in this attribute set. The result set is not limited to the set + /// of currently fetched attributes. + /// ISet GetAttributeNames(); + /// + /// Returns set of all keys (combination of attribute name and locale) registered in this attribute set. + /// ISet GetAttributeKeys(); + /// + /// Returns collection of all values present in this object. + /// ICollection GetAttributeValues(); + /// + /// Returns array of values associated with the key or null when the attribute is missing. + /// When localized attribute is not found it is looked up in generic (non-localized) attributes. This makes this + /// method the safest way how to lookup for attribute if caller doesn't know whether it is localized or not. + /// + /// Method returns wrapper dto for the attribute that contains information about the attribute version and state. + /// ICollection GetAttributeValues(string attributeName); + /// + /// Method returns set of all locales used in the localized attributes. The result set is not limited to the set + /// of currently fetched attributes. + /// ISet GetAttributeLocales(); + /// + /// Returns true if single attribute differs between first and second instance. + /// static bool AnyAttributeDifferBetween(IAttributes first, IAttributes second) { IEnumerable thisValues = @@ -51,4 +149,4 @@ static bool AnyAttributeDifferBetween(IAttributes first, IAttributes sec return it.Dropped != other.Dropped || QueryUtils.ValueDiffers(thisValue, otherValue); }); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IDevelopmentConstants.cs b/EvitaDB.Client/Models/Data/IDevelopmentConstants.cs new file mode 100644 index 0000000..4bd5d1d --- /dev/null +++ b/EvitaDB.Client/Models/Data/IDevelopmentConstants.cs @@ -0,0 +1,7 @@ +namespace EvitaDB.Client.Models.Data; + +public interface IDevelopmentConstants +{ + const string TestRun = "TestRun"; + static bool IsTestRun => Environment.GetEnvironmentVariable(TestRun) is "true"; +} diff --git a/EvitaDB.Client/Models/Data/IDroppable.cs b/EvitaDB.Client/Models/Data/IDroppable.cs index 9cf8bec..74e3160 100644 --- a/EvitaDB.Client/Models/Data/IDroppable.cs +++ b/EvitaDB.Client/Models/Data/IDroppable.cs @@ -1,6 +1,14 @@ namespace EvitaDB.Client.Models.Data; +/// +/// This interface marks data objects that are turned into a tombstone once they are removed. Dropped data still occupy +/// the original place but may be cleaned by automatic tidy process. They can be also revived anytime by setting new +/// value. Dropped item must be handled by the system as non-existing data. +/// public interface IDroppable : IVersioned { + /// + /// Returns true if data object is removed (i.e. has tombstone flag present). + /// bool Dropped { get; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IEntity.cs b/EvitaDB.Client/Models/Data/IEntity.cs index 3f01423..033fa5f 100644 --- a/EvitaDB.Client/Models/Data/IEntity.cs +++ b/EvitaDB.Client/Models/Data/IEntity.cs @@ -1,24 +1,69 @@ using System.Globalization; +using EvitaDB.Client.Exceptions; +using EvitaDB.Client.Models.Data.Structure; using EvitaDB.Client.Models.Schemas; namespace EvitaDB.Client.Models.Data; +/// +/// Contract for classes that allow reading information about instance. +/// public interface IEntity : IEntityClassifierWithParent, IAttributes, IAssociatedData, IPrices, IDroppable, IContentComparator { + /// + /// Returns schema of the entity, that fully describes its structure and capabilities. Schema is up-to-date to the + /// moment entity was fetched from evitaDB. + /// IEntitySchema Schema { get; } + /// + /// Returns primary key of the entity that is UNIQUE among all other entities of the same type. + /// Primary key may be null only when entity is created in case evitaDB is responsible for automatically assigning + /// new primary key. Once entity is stored into evitaDB it MUST have non-null primary key. So the NULL can be + /// returned only in the rare case when new entity is created in the client code and hasn't yet been stored to + /// evitaDB. + /// int? Parent { get; } + /// + /// Returns collection of of this entity. The references represent relations to other evitaDB + /// entities or external entities in different systems. + /// IEnumerable GetReferences(); + /// + /// Returns collection of to certain type of other entities. References represent relations to + /// other evitaDB entities or external entities in different systems. + /// IEnumerable GetReferences(string referenceName); - new IPrice? PriceForSale { get; } + /// + /// Returns single instance that is referencing passed entity type with certain primary key. + /// The references represent relations to other evitaDB entities or external entities in different systems. + /// IReference? GetReference(string referenceName, int referencedEntityId); + /// + /// Returns set of locales this entity has any of localized data in. Although may + /// support wider range of the locales, this method returns only those that are used by data of this very entity + /// instance. + /// ISet GetAllLocales(); + /// + /// Returns true if entity hierarchy was fetched along with the entity. Calling this method before calling any + /// other method that requires prices to be fetched will allow you to avoid . + /// + /// Method also returns false if the entity is not allowed to be hierarchical by the schema. Checking this method + /// also allows you to avoid in such case. + /// bool ParentAvailable(); + /// + /// Returns true if entity references were fetched along with the entity. Calling this method before calling any + /// other method that requires references to be fetched will allow you to avoid . + /// bool ReferencesAvailable(); - + /// + /// Method returns true if any entity inner data differs from other entity. + /// new bool DiffersFrom(IEntity? otherEntity) { - if (this == otherEntity) return false; + if (Equals(this, otherEntity)) return false; if (otherEntity == null) return true; if (!Equals(PrimaryKey, otherEntity.PrimaryKey)) return true; @@ -53,4 +98,4 @@ public interface IEntity : IEntityClassifierWithParent, IAttributes +/// Common ancestor for contracts that either directly represent or reference to it +/// - i.e. . We don't use sealed interface here because there are multiple implementations +/// of those interfaces but only these two aforementioned extending interfaces could extend from this one. +/// public interface IEntityClassifier { + /// + /// Reference to of the entity. Might be also anything + /// that identifies type some external resource not maintained by Evita. + /// public string Type { get; } + /// + /// Unique Integer positive number (max. 263-1) representing the entity. Can be used for fast lookup for + /// entity (entities). Primary key must be unique within the same entity type. + /// May be left empty if it should be auto generated by the database. + /// Entities can be looked up by primary key by using query + /// public int? PrimaryKey { get; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IEntityClassifierWithParent.cs b/EvitaDB.Client/Models/Data/IEntityClassifierWithParent.cs index 5aaf1c5..6498882 100644 --- a/EvitaDB.Client/Models/Data/IEntityClassifierWithParent.cs +++ b/EvitaDB.Client/Models/Data/IEntityClassifierWithParent.cs @@ -1,6 +1,16 @@ -namespace EvitaDB.Client.Models.Data; +using EvitaDB.Client.Models.Data.Structure; +namespace EvitaDB.Client.Models.Data; + +/// +/// Common ancestor for contracts that either directly represent or reference to it and may +/// contain reference to parent entities. We don't use sealed interface here because there are multiple implementations +/// of those interfaces but only these two aforementioned extending interfaces could extend from this one. +/// public interface IEntityClassifierWithParent : IEntityClassifier { + /// + /// Optional reference to of the referenced entity. + /// IEntityClassifierWithParent? ParentEntity { get; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IPrices.cs b/EvitaDB.Client/Models/Data/IPrices.cs index 6326808..12056c4 100644 --- a/EvitaDB.Client/Models/Data/IPrices.cs +++ b/EvitaDB.Client/Models/Data/IPrices.cs @@ -1,22 +1,76 @@ using EvitaDB.Client.DataTypes; +using EvitaDB.Client.Exceptions; +using EvitaDB.Client.Queries; using EvitaDB.Client.Queries.Requires; namespace EvitaDB.Client.Models.Data; public interface IPrices : IVersioned { + /// + /// Returns price inner record handling that controls how prices that share same `inner entity id` will behave during + /// filtering and sorting. + /// PriceInnerRecordHandling? InnerRecordHandling { get; } + /// + /// Returns a price for which the entity should be sold. This method can be used only in context of a + /// with price related constraints so that `currency` and `priceList` priority can be extracted from the query. + /// The moment is either extracted from the query as well (if present) or current date and time is used. + /// public IPrice? PriceForSale { get; } + /// + /// Returns price by its business key identification. + /// IPrice? GetPrice(PriceKey priceKey); + /// + /// Returns price by its business key identification. + /// IPrice? GetPrice(int priceId, string priceList, Currency currency); + /// + /// Returns a price for which the entity should be sold. Only indexed prices in requested currency, valid + /// at the passed moment are taken into an account. Prices are also limited by the passed set of price lists and + /// the first price found in the order of the requested price list ids will be returned. + /// bool HasPriceInInterval(decimal from, decimal to, QueryPriceMode queryPriceMode); - IEnumerable GetPrices(); + /// + /// Returns all prices of the entity. + /// + IList GetPrices(); + /// + /// Returns true if entity prices were fetched along with the entity. Calling this method before calling any + /// other method that requires prices to be fetched will allow you to avoid {@link ContextMissingException}. + /// + /// Method also returns false if the prices are not enabled for the entity by the schema. Checking this method + /// also allows you to avoid getting an exception in such case. + /// + /// bool PricesAvailable(); + /// + /// Returns all prices for which the entity could be sold. This method can be used in context of a + /// with price related constraints so that `currency` and `priceList` priority can be extracted from the query. + /// The moment is either extracted from the query as well (if present) or current date and time is used. + /// + /// The method differs from in the sense of never returning {@link ContextMissingException} + /// and returning list of all possibly matching selling prices (not only single one). Returned list may be also + /// empty if there is no such price. + /// IList GetAllPricesForSale(Currency? currency, DateTimeOffset? atTheMoment, params string[] priceListPriority); + /// + /// Returns all prices for which the entity could be sold. This method can be used in context of a + /// with price related constraints so that `currency` and `priceList` priority can be extracted from the query. + /// The moment is either extracted from the query as well (if present) or current date and time is used. + /// + /// The method differs from in the sense of never returning + /// and returning list of all possibly matching selling prices (not only single one). Returned list may be also + /// empty if there is no such price. + /// IList GetAllPricesForSale(); + /// + /// Returns true if single price differs between first and second instance. + /// public static bool AnyPriceDifferBetween(IPrices first, IPrices second) { IEnumerable thisValues = first.PricesAvailable() ? first.GetPrices() : new List(); @@ -31,4 +85,4 @@ public static bool AnyPriceDifferBetween(IPrices first, IPrices second) return enumerable .Any(it => it.DiffersFrom(second.GetPrice(it.PriceId, it.PriceList, it.Currency))); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/IVersioned.cs b/EvitaDB.Client/Models/Data/IVersioned.cs index 73fc218..41d2bed 100644 --- a/EvitaDB.Client/Models/Data/IVersioned.cs +++ b/EvitaDB.Client/Models/Data/IVersioned.cs @@ -1,6 +1,15 @@ namespace EvitaDB.Client.Models.Data; +/// +/// This interface marks all objects that are immutable and versioned. Whenever new instance of the class instance is +/// created and takes place of another class instance (i.e. is successor of that data) its version must be increased +/// by one. +/// Versioned data are used for handling [optimistic locking](https://en.wikipedia.org/wiki/Optimistic_concurrency_control). +/// public interface IVersioned { + /// + /// Returns version of the object. + /// public int Version { get; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Mutations/Reference/InsertReferenceMutation.cs b/EvitaDB.Client/Models/Data/Mutations/Reference/InsertReferenceMutation.cs index aa9ec6c..3b5652f 100644 --- a/EvitaDB.Client/Models/Data/Mutations/Reference/InsertReferenceMutation.cs +++ b/EvitaDB.Client/Models/Data/Mutations/Reference/InsertReferenceMutation.cs @@ -58,4 +58,4 @@ existingValue.Group is not null && !existingValue.Group.Dropped "This mutation cannot be used for updating reference." ); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/AssociatedData.cs b/EvitaDB.Client/Models/Data/Structure/AssociatedData.cs index 3358534..f5bb6bc 100644 --- a/EvitaDB.Client/Models/Data/Structure/AssociatedData.cs +++ b/EvitaDB.Client/Models/Data/Structure/AssociatedData.cs @@ -19,6 +19,7 @@ public class AssociatedData : IAssociatedData public bool AssociatedDataAvailable(CultureInfo locale) => true; public bool AssociatedDataAvailable(string associatedDataName) => true; public bool AssociatedDataAvailable(string associatedDataName, CultureInfo locale) => true; + private IList OrderedAssociatedDataValues { get; } = new List(); public AssociatedData( IEntitySchema entitySchema, @@ -28,7 +29,8 @@ IDictionary associatedDataTypes { EntitySchema = entitySchema; AssociatedDataValues = new Dictionary(); - foreach (AssociatedDataValue associatedDataValue in associatedDataValues) + OrderedAssociatedDataValues = associatedDataValues.ToList(); + foreach (AssociatedDataValue associatedDataValue in OrderedAssociatedDataValues) { AssociatedDataValues.Add(associatedDataValue.Key, associatedDataValue); } @@ -42,14 +44,22 @@ IDictionary associatedDataTypes */ public AssociatedData( IEntitySchema entitySchema, - ICollection? associatedDataValues + IDictionary? associatedDataValues ) { EntitySchema = entitySchema; - AssociatedDataValues = associatedDataValues is null - ? new Dictionary() - : associatedDataValues - .ToDictionary(x => x.Key, x => x); + if (associatedDataValues is null) + { + AssociatedDataValues = new Dictionary(); + } + else + { + foreach (var aData in associatedDataValues) + { + OrderedAssociatedDataValues.Add(aData.Value); + } + AssociatedDataValues = associatedDataValues; + } AssociatedDataTypes = entitySchema.AssociatedData; } @@ -284,4 +294,4 @@ public override string ToString() { return string.Join("; ", GetAssociatedDataValues().Select(x => x.ToString())); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Attributes.cs b/EvitaDB.Client/Models/Data/Structure/Attributes.cs index 271e58e..b2a18e7 100644 --- a/EvitaDB.Client/Models/Data/Structure/Attributes.cs +++ b/EvitaDB.Client/Models/Data/Structure/Attributes.cs @@ -9,7 +9,7 @@ namespace EvitaDB.Client.Models.Data.Structure; public abstract class Attributes : IAttributes where TS : IAttributeSchema { [JsonIgnore] protected internal IEntitySchema EntitySchema { get; } - internal Dictionary AttributeValues { get; } + internal IDictionary AttributeValues { get; } [JsonIgnore] public IDictionary AttributeTypes { get; } private ISet? AttributeNames { get; set; } private ISet? AttributeLocales { get; set; } @@ -18,7 +18,28 @@ public abstract class Attributes : IAttributes where TS : IAttributeSche public bool AttributesAvailable(CultureInfo locale) => true; public bool AttributeAvailable(string attributeName) => true; public bool AttributeAvailable(string attributeName, CultureInfo locale) => true; + protected IList OrderedAttributeValues { get; } = new List(); + protected Attributes( + IEntitySchema entitySchema, + IDictionary attributeValues, + IDictionary attributeTypes + ) + { + EntitySchema = entitySchema; + foreach (var attribute in attributeValues) + { + OrderedAttributeValues.Add(attribute.Value); + } + AttributeValues = attributeValues; + AttributeTypes = attributeTypes; + AttributeLocales = OrderedAttributeValues + .Where(x=>!x.Dropped) + .Select(x=>x.Key.Locale) + .Where(x=>x is not null) + .ToHashSet()!; + } + protected Attributes( IEntitySchema entitySchema, ICollection attributeValues, @@ -26,9 +47,10 @@ IDictionary attributeTypes ) { EntitySchema = entitySchema; - AttributeValues = attributeValues.ToDictionary(x => x.Key, x => x); + OrderedAttributeValues = attributeValues.ToList(); + AttributeValues = attributeValues.ToDictionary(x=>x.Key, x=>x); AttributeTypes = attributeTypes; - AttributeLocales = attributeValues + AttributeLocales = OrderedAttributeValues .Where(x=>!x.Dropped) .Select(x=>x.Key.Locale) .Where(x=>x is not null) @@ -241,4 +263,4 @@ public override string ToString() } protected abstract AttributeNotFoundException CreateAttributeNotFoundException(string attributeName); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Entity.cs b/EvitaDB.Client/Models/Data/Structure/Entity.cs index c0bbfca..99d9873 100644 --- a/EvitaDB.Client/Models/Data/Structure/Entity.cs +++ b/EvitaDB.Client/Models/Data/Structure/Entity.cs @@ -30,40 +30,43 @@ public class Entity : ISealedEntity public ISet Locales { get; } public bool Dropped { get; } public PriceInnerRecordHandling? InnerRecordHandling => Prices.InnerRecordHandling; - + /// /// This predicate filters out non-fetched locales. /// public LocalePredicate LocalePredicate { get; } - + /// /// This predicate filters out access to the hierarchy parent that were not fetched in query. /// public HierarchyPredicate HierarchyPredicate { get; } - + /// /// This predicate filters out attributes that were not fetched in query. /// public AttributeValuePredicate AttributePredicate { get; } - + /// /// This predicate filters out associated data that were not fetched in query. /// public AssociatedDataValuePredicate AssociatedDataPredicate { get; } - + /// /// This predicate filters out references that were not fetched in query. /// public ReferencePredicate ReferencePredicate { get; } - + /// /// This predicate filters out prices that were not fetched in query. /// public PricePredicate PricePredicate { get; } + public bool ParentAvailable() => Schema.WithHierarchy; public bool PricesAvailable() => Prices.PricesAvailable(); - public IList GetAllPricesForSale(Currency? currency, DateTimeOffset? atTheMoment, params string[] priceListPriority) + + public IList GetAllPricesForSale(Currency? currency, DateTimeOffset? atTheMoment, + params string[] priceListPriority) { return Prices.GetAllPricesForSale(currency, atTheMoment, priceListPriority); } @@ -267,7 +270,7 @@ PricePredicate pricePredicate PrimaryKey = primaryKey; _parent = parent; _parentEntity = parentEntity; - References = references.ToImmutableSortedDictionary(x => x.ReferenceKey, x => x); + References = references.ToImmutableDictionary(x => x.ReferenceKey, x => x); Attributes = attributes; AssociatedData = associatedData; Prices = prices; @@ -308,7 +311,7 @@ private Entity( PrimaryKey = primaryKey; _parent = parent; _parentEntity = parentEntity; - References = references.ToImmutableSortedDictionary(x => x.ReferenceKey, x => x); + References = references.ToImmutableDictionary(x => x.ReferenceKey, x => x); Attributes = attributes; AssociatedData = associatedData; Prices = prices; @@ -766,10 +769,11 @@ Dictionary newAttributes } else { - List attributeValues = possibleEntity is null - ? new List() - : possibleEntity.GetAttributeValues().Where(x => !newAttributes.ContainsKey(x.Key)).ToList(); - attributeValues.AddRange(newAttributes.Values); + IDictionary attributeValues = possibleEntity is null + ? new Dictionary() + : possibleEntity.GetAttributeValues().Where(x => !newAttributes.ContainsKey(x.Key)) + .Concat(newAttributes.Values) + .ToDictionary(x => x.Key, x => x); List attributeSchemas = entitySchema.Attributes.Values.ToList(); attributeSchemas.AddRange(newAttributes.Values .Where(x => !entitySchema.Attributes.ContainsKey(x.Key.AttributeName)) @@ -807,7 +811,7 @@ public ISet GetAllLocales() public object? GetAttribute(string attributeName) { - return Attributes.GetAttribute(attributeName); + return GetAttributeValue(attributeName)?.Value; } public object[]? GetAttributeArray(string attributeName) @@ -817,7 +821,19 @@ public ISet GetAllLocales() public AttributeValue? GetAttributeValue(string attributeName) { - return Attributes.GetAttributeValue(attributeName); + AttributeKey attributeKey; + if (AttributePredicate.LocaleSet) + { + CultureInfo? locale = AttributePredicate.Locale; + attributeKey = locale == null ? new AttributeKey(attributeName) : new AttributeKey(attributeName, locale); + } + else + { + attributeKey = new AttributeKey(attributeName); + } + + AttributePredicate.CheckFetched(attributeKey); + return Attributes.GetAttributeValue(attributeKey); } public object? GetAttribute(string attributeName, CultureInfo locale) @@ -892,7 +908,21 @@ public ISet GetAttributeLocales() public AssociatedDataValue? GetAssociatedDataValue(string associatedDataName) { - return AssociatedData.GetAssociatedDataValue(associatedDataName); + AssociatedDataKey associatedDataKey; + if (AssociatedDataPredicate.LocaleSet) + { + CultureInfo? locale = AssociatedDataPredicate.Locale; + associatedDataKey = locale == null + ? new AssociatedDataKey(associatedDataName) + : new AssociatedDataKey(associatedDataName, locale); + } + else + { + associatedDataKey = new AssociatedDataKey(associatedDataName); + } + + AssociatedDataPredicate.CheckFetched(associatedDataKey); + return AssociatedData.GetAssociatedDataValue(associatedDataKey); } public object? GetAssociatedData(string associatedDataName, CultureInfo locale) @@ -966,7 +996,7 @@ public bool HasPriceInInterval(decimal from, decimal to, QueryPriceMode queryPri throw new ContextMissingException(); } - public IEnumerable GetPrices() + public IList GetPrices() { return Prices.GetPrices(); } @@ -997,7 +1027,7 @@ public override bool Equals(object? o) { if (this == o) return true; if (o == null || GetType() != o.GetType()) return false; - Entity entity = (Entity) o; + Entity entity = (Entity)o; return Version == entity.Version && Type.Equals(entity.Type) && PrimaryKey == entity.PrimaryKey; } @@ -1028,4 +1058,4 @@ public override string ToString() ? "" : ", localized to " + string.Join(", ", locales.Select(x => x.TwoLetterISOLanguageName)))); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/EntityAttributes.cs b/EvitaDB.Client/Models/Data/Structure/EntityAttributes.cs index 22beeac..25ba7fb 100644 --- a/EvitaDB.Client/Models/Data/Structure/EntityAttributes.cs +++ b/EvitaDB.Client/Models/Data/Structure/EntityAttributes.cs @@ -4,17 +4,23 @@ namespace EvitaDB.Client.Models.Data.Structure; /// -/// Extension of for entity attributes. +/// Extension of for entity attributes. /// public class EntityAttributes : Attributes { + public EntityAttributes(IEntitySchema entitySchema, IDictionary attributeValues, + IDictionary attributeTypes) + : base(entitySchema, attributeValues, attributeTypes) + { + } + public EntityAttributes(IEntitySchema entitySchema, ICollection attributeValues, IDictionary attributeTypes) : base(entitySchema, attributeValues, attributeTypes) { } - public EntityAttributes(IEntitySchema entitySchema) : base(entitySchema, new List(), + public EntityAttributes(IEntitySchema entitySchema) : base(entitySchema, new Dictionary(), entitySchema.GetAttributes()) { } @@ -23,4 +29,4 @@ protected override AttributeNotFoundException CreateAttributeNotFoundException(s { return new AttributeNotFoundException(attributeName, EntitySchema); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/EntityReference.cs b/EvitaDB.Client/Models/Data/Structure/EntityReference.cs index 5b5980b..315103b 100644 --- a/EvitaDB.Client/Models/Data/Structure/EntityReference.cs +++ b/EvitaDB.Client/Models/Data/Structure/EntityReference.cs @@ -2,4 +2,8 @@ public record EntityReference(string Type, int? PrimaryKey) : IEntityReference { -} \ No newline at end of file + public override string ToString() + { + return Type + ": " + PrimaryKey; + } +} diff --git a/EvitaDB.Client/Models/Data/Structure/ExistingEntityBuilder.cs b/EvitaDB.Client/Models/Data/Structure/ExistingEntityBuilder.cs index 50f7d83..498e24a 100644 --- a/EvitaDB.Client/Models/Data/Structure/ExistingEntityBuilder.cs +++ b/EvitaDB.Client/Models/Data/Structure/ExistingEntityBuilder.cs @@ -84,7 +84,7 @@ public bool HasPriceInInterval(decimal from, decimal to, QueryPriceMode queryPri return PricesBuilder.HasPriceInInterval(from, to, queryPriceMode); } - public IEnumerable GetPrices() + public IList GetPrices() { return PricesBuilder.GetPrices(); } @@ -847,4 +847,4 @@ public ISet GetAssociatedDataLocales() { return AssociatedDataBuilder.GetAssociatedDataLocales(); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/ExistingPricesBuilder.cs b/EvitaDB.Client/Models/Data/Structure/ExistingPricesBuilder.cs index 4e49abe..202aba5 100644 --- a/EvitaDB.Client/Models/Data/Structure/ExistingPricesBuilder.cs +++ b/EvitaDB.Client/Models/Data/Structure/ExistingPricesBuilder.cs @@ -204,7 +204,7 @@ public bool HasPriceInInterval(decimal from, decimal to, QueryPriceMode queryPri throw new ContextMissingException(); } - public IEnumerable GetPrices() + public IList GetPrices() { return GetPricesWithoutPredicate() .ToList(); @@ -362,4 +362,4 @@ public Prices Build() return BasePrices; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/InitialEntityBuilder.cs b/EvitaDB.Client/Models/Data/Structure/InitialEntityBuilder.cs index 0b8f760..717bbb7 100644 --- a/EvitaDB.Client/Models/Data/Structure/InitialEntityBuilder.cs +++ b/EvitaDB.Client/Models/Data/Structure/InitialEntityBuilder.cs @@ -31,7 +31,7 @@ public class InitialEntityBuilder : IEntityBuilder private IDictionary References { get; } public PriceInnerRecordHandling? InnerRecordHandling => PricesBuilder.InnerRecordHandling; - public IEnumerable GetPrices() + public IList GetPrices() { return PricesBuilder.GetPrices(); } @@ -648,4 +648,4 @@ private IReferenceSchema GetReferenceSchemaOrThrowException(string referenceName { return Schema.GetReference(referenceName) ?? throw new ReferenceNotKnownException(referenceName); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/InitialPricesBuilder.cs b/EvitaDB.Client/Models/Data/Structure/InitialPricesBuilder.cs index facde4f..9dec708 100644 --- a/EvitaDB.Client/Models/Data/Structure/InitialPricesBuilder.cs +++ b/EvitaDB.Client/Models/Data/Structure/InitialPricesBuilder.cs @@ -69,9 +69,9 @@ public bool HasPriceInInterval(decimal from, decimal to, QueryPriceMode queryPri throw new ContextMissingException(); } - public IEnumerable GetPrices() + public IList GetPrices() { - return Prices.Values; + return Prices.Values.ToList(); } public IPricesBuilder SetPrice(int priceId, string priceList, Currency currency, decimal priceWithoutTax, @@ -177,4 +177,4 @@ private void AssertPriceNotAmbiguousBeforeAdding(Price price) throw new AmbiguousPriceException(conflictingPrice, price); } } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Predicates/AssociatedDataValuePredicate.cs b/EvitaDB.Client/Models/Data/Structure/Predicates/AssociatedDataValuePredicate.cs index 6f60bff..10fce6f 100644 --- a/EvitaDB.Client/Models/Data/Structure/Predicates/AssociatedDataValuePredicate.cs +++ b/EvitaDB.Client/Models/Data/Structure/Predicates/AssociatedDataValuePredicate.cs @@ -35,6 +35,8 @@ public class AssociatedDataValuePredicate /// Contains true if any of the associated data of the entity has been fetched / requested. /// public bool RequiresEntityAssociatedData { get; } + + public bool LocaleSet => Locale != null || ImplicitLocale != null || Locales != null; public AssociatedDataValuePredicate() { @@ -135,4 +137,4 @@ Locales is not null && ); } } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Predicates/AttributeValuePredicate.cs b/EvitaDB.Client/Models/Data/Structure/Predicates/AttributeValuePredicate.cs index 73710d2..80e3c1a 100644 --- a/EvitaDB.Client/Models/Data/Structure/Predicates/AttributeValuePredicate.cs +++ b/EvitaDB.Client/Models/Data/Structure/Predicates/AttributeValuePredicate.cs @@ -34,6 +34,8 @@ public class AttributeValuePredicate /// Contains true if any of the attributes of the entity has been fetched / requested. /// public bool RequiresEntityAttributes { get; } + + public bool LocaleSet => Locale != null || ImplicitLocale != null || Locales != null; public AttributeValuePredicate() { @@ -130,4 +132,4 @@ Locales is not null && ); } } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Prices.cs b/EvitaDB.Client/Models/Data/Structure/Prices.cs index 6f4490e..54b9922 100644 --- a/EvitaDB.Client/Models/Data/Structure/Prices.cs +++ b/EvitaDB.Client/Models/Data/Structure/Prices.cs @@ -12,9 +12,11 @@ public class Prices : IPrices [JsonIgnore] internal IEntitySchema EntitySchema { get; } private bool WithPrice { get; } public int Version { get; } - private ImmutableDictionary PriceIndex { get; } + private IImmutableDictionary PriceIndex { get; } public PriceInnerRecordHandling? InnerRecordHandling { get; } - public IPrice? PriceForSale => throw new ContextMissingException(); + public IPrice PriceForSale => throw new ContextMissingException(); + public IList GetPrices() => OrderedPriceValues; + private IList OrderedPriceValues { get; } = new List(); public Prices(IEntitySchema entitySchema, PriceInnerRecordHandling priceInnerRecordHandling) { @@ -31,7 +33,8 @@ public Prices(IEntitySchema entitySchema, IEnumerable prices, EntitySchema = entitySchema; WithPrice = entitySchema.WithPrice; Version = 1; - PriceIndex = prices.ToDictionary(x => x.Key, x => x).ToImmutableDictionary(); + OrderedPriceValues = prices.ToList(); + PriceIndex = OrderedPriceValues.ToDictionary(x => x.Key, x => x).ToImmutableDictionary(); InnerRecordHandling = priceInnerRecordHandling; } @@ -47,7 +50,8 @@ public Prices(IEntitySchema entitySchema, int version, IEnumerable price EntitySchema = entitySchema; WithPrice = withPrice; Version = version; - PriceIndex = prices.ToDictionary(x => x.Key, x => x).ToImmutableDictionary(); + OrderedPriceValues = prices.ToList(); + PriceIndex = OrderedPriceValues.ToDictionary(x => x.Key, x => x).ToImmutableDictionary(); InnerRecordHandling = priceInnerRecordHandling; } @@ -88,9 +92,7 @@ public IList GetAllPricesForSale(Currency? currency, DateTimeOffset? atT .Where(it => !pLists.Any() || pLists.Contains(it.PriceList)) .ToList(); } - - public IEnumerable GetPrices() => PriceIndex.Values; - + public override string ToString() { if (PricesAvailable()) @@ -104,4 +106,4 @@ public override string ToString() return "entity has no prices"; } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/Reference.cs b/EvitaDB.Client/Models/Data/Structure/Reference.cs index 4f02e81..f5bdac3 100644 --- a/EvitaDB.Client/Models/Data/Structure/Reference.cs +++ b/EvitaDB.Client/Models/Data/Structure/Reference.cs @@ -47,7 +47,7 @@ public Reference( Attributes = new ReferenceAttributes( entitySchema, referenceSchema ?? CreateImplicitSchema(referenceName, referencedEntityType, cardinality, group), - new List(), + new Dictionary(), referenceSchema is not null ? referenceSchema.GetAttributes() : new Dictionary() ); ReferencedEntity = referencedEntity; @@ -76,7 +76,7 @@ public Reference( Attributes = new ReferenceAttributes( entitySchema, referenceSchema ?? CreateImplicitSchema(referenceName, referencedEntityType, cardinality, group), - new List(), + new Dictionary(), referenceSchema is not null ? referenceSchema.GetAttributes() : new Dictionary() ); ReferencedEntity = referencedEntity; @@ -117,7 +117,7 @@ public Reference( string? referencedEntityType, Cardinality? cardinality, GroupEntityReference? group, - ICollection attributes, + IDictionary attributes, ISealedEntity? referencedEntity = null, ISealedEntity? groupEntity = null, bool dropped = false) @@ -139,6 +139,37 @@ public Reference( GroupEntity = groupEntity; Dropped = dropped; } + + public Reference( + IEntitySchema entitySchema, + int version, + string referenceName, + int referencedEntityPrimaryKey, + string? referencedEntityType, + Cardinality? cardinality, + GroupEntityReference? group, + ICollection attributes, + ISealedEntity? referencedEntity = null, + ISealedEntity? groupEntity = null, + bool dropped = false) + { + EntitySchema = entitySchema; + Version = version; + ReferenceKey = new ReferenceKey(referenceName, referencedEntityPrimaryKey); + _referenceCardinality = cardinality; + _referencedEntityType = referencedEntityType; + Group = group; + IReferenceSchema? referenceSchema = entitySchema.GetReference(referenceName); + Attributes = new ReferenceAttributes( + entitySchema, + referenceSchema ?? CreateImplicitSchema(referenceName, referencedEntityType, cardinality, group), + attributes, + referenceSchema is not null ? referenceSchema.GetAttributes() : new Dictionary() + ); + ReferencedEntity = referencedEntity; + GroupEntity = groupEntity; + Dropped = dropped; + } public Reference( IEntitySchema entitySchema, @@ -273,4 +304,4 @@ public override string ToString() (Group == null ? "" : " in " + Group) + (Attributes.AttributesAvailable() ? ", attrs: " + Attributes : ""); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Data/Structure/ReferenceAttributes.cs b/EvitaDB.Client/Models/Data/Structure/ReferenceAttributes.cs index b826bdb..645736f 100644 --- a/EvitaDB.Client/Models/Data/Structure/ReferenceAttributes.cs +++ b/EvitaDB.Client/Models/Data/Structure/ReferenceAttributes.cs @@ -11,11 +11,21 @@ public class ReferenceAttributes : Attributes private IReferenceSchema ReferenceSchema { get; } public ReferenceAttributes(IEntitySchema entitySchema, IReferenceSchema referenceSchema) - : base(entitySchema, new List(), referenceSchema.GetAttributes()) + : base(entitySchema, new Dictionary(), referenceSchema.GetAttributes()) { ReferenceSchema = referenceSchema; } + public ReferenceAttributes( + IEntitySchema entitySchema, + IReferenceSchema referenceSchema, + IDictionary attributeValues, + IDictionary attributeTypes + ) : base(entitySchema, attributeValues, attributeTypes) + { + ReferenceSchema = referenceSchema; + } + public ReferenceAttributes( IEntitySchema entitySchema, IReferenceSchema referenceSchema, @@ -30,4 +40,4 @@ protected override AttributeNotFoundException CreateAttributeNotFoundException(s { return new AttributeNotFoundException(attributeName, ReferenceSchema, EntitySchema); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/EvitaRequest.cs b/EvitaDB.Client/Models/EvitaRequest.cs index dc4bfc5..da0edbe 100644 --- a/EvitaDB.Client/Models/EvitaRequest.cs +++ b/EvitaDB.Client/Models/EvitaRequest.cs @@ -4,6 +4,7 @@ using EvitaDB.Client.Queries; using EvitaDB.Client.Queries.Filter; using EvitaDB.Client.Queries.Head; +using EvitaDB.Client.Queries.Order; using EvitaDB.Client.Queries.Requires; using EvitaDB.Client.Utils; @@ -70,7 +71,7 @@ public EvitaRequest( DateTimeOffset alignedNow, Type? expectedType = null) { - Collection? header = query.Entities; + Collection? header = query.Collection; _entityType = header?.EntityType; Query = query; AlignedNow = alignedNow; @@ -78,6 +79,70 @@ public EvitaRequest( _expectedType = expectedType; } + public EvitaRequest( + EvitaRequest evitaRequest, + string entityType, + FilterBy filterBy, + OrderBy? orderBy, + CultureInfo? locale) + { + _requiresEntity = true; + _entityRequirement = evitaRequest._entityRequirement; + _entityType = entityType; + Query = IQueryConstraints.Query( + IQueryConstraints.Collection(entityType), + filterBy, + orderBy, + IQueryConstraints.Require(_entityRequirement) + ); + AlignedNow = evitaRequest.AlignedNow; + _implicitLocale = evitaRequest._implicitLocale; + _primaryKeys = null; + _queryPriceMode = evitaRequest._queryPriceMode; + _priceValidInTimeSet = true; + _priceValidInTime = evitaRequest.GetRequiresPriceValidIn(); + _currencySet = true; + _currency = evitaRequest.GetRequiresCurrency(); + _requiresPriceLists = evitaRequest.RequiresPriceLists(); + _priceLists = evitaRequest.GetRequiresPriceLists(); + _additionalPriceLists = evitaRequest.GetFetchesAdditionalPriceLists(); + _locale = locale ?? evitaRequest.GetLocale(); + _localeExamined = true; + _expectedType = evitaRequest._expectedType; + } + + public EvitaRequest( + EvitaRequest evitaRequest, + string entityType, + IEntityFetchRequire requirements) + { + _requiresEntity = true; + _entityRequirement = new EntityFetch(requirements.Requirements); + _entityType = entityType; + Query = IQueryConstraints.Query( + IQueryConstraints.Collection(entityType), + evitaRequest.Query.FilterBy, + evitaRequest.Query.OrderBy, + IQueryConstraints.Require(_entityRequirement) + ); + AlignedNow = evitaRequest.AlignedNow; + _implicitLocale = evitaRequest._implicitLocale; + _primaryKeys = null; + _queryPriceMode = evitaRequest._queryPriceMode; + _priceValidInTimeSet = true; + _priceValidInTime = evitaRequest.GetRequiresPriceValidIn(); + _currencySet = true; + _currency = evitaRequest.GetRequiresCurrency(); + _requiresPriceLists = evitaRequest.RequiresPriceLists(); + _priceLists = evitaRequest.GetRequiresPriceLists(); + _additionalPriceLists = evitaRequest.GetFetchesAdditionalPriceLists(); + _locale = evitaRequest.GetLocale(); + _localeExamined = true; + _expectedType = evitaRequest._expectedType; + _limit = evitaRequest._limit; + _resultForm = evitaRequest._resultForm; + } + /** * Returns true if query targets specific entity type. */ @@ -99,7 +164,7 @@ public bool EntityTypeRequested() */ public string GetEntityTypeOrThrowException(string purpose) { - Collection? header = Query.Entities; + Collection? header = Query.Collection; return header is not null ? header.EntityType : throw new EntityCollectionRequiredException(purpose); } @@ -152,7 +217,7 @@ public ISet GetRequiredLocales() CultureInfo? theLocale = GetLocale(); if (theLocale != null) { - _requiredLocaleSet = new HashSet(new[] {theLocale}); + _requiredLocaleSet = new HashSet(new[] { theLocale }); } } else @@ -168,7 +233,7 @@ public ISet GetRequiredLocales() CultureInfo? theLocale = GetLocale(); if (theLocale != null) { - _requiredLocaleSet = new HashSet(new[] {theLocale}); + _requiredLocaleSet = new HashSet(new[] { theLocale }); } } @@ -622,6 +687,27 @@ public IDataChunk CreateDataChunk(int totalRecordCount, IList data) }; } + public EvitaRequest DeriveCopyWith(string entityType, IEntityFetchRequire requirements) + { + return new EvitaRequest( + this, + entityType, requirements + ); + } + + public EvitaRequest DeriveCopyWith( + string entityType, + FilterBy filterConstraint, + OrderBy orderConstraint, + CultureInfo locale + ) + { + return new EvitaRequest( + this, + entityType, filterConstraint, orderConstraint, locale + ); + } + private void InitPagination() { Page? page = QueryUtils.FindRequire(Query); @@ -651,4 +737,4 @@ private enum ResultForm PaginatedList, StripList } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/ExtraResults/FacetSummary.cs b/EvitaDB.Client/Models/ExtraResults/FacetSummary.cs index 501e506..98640b8 100644 --- a/EvitaDB.Client/Models/ExtraResults/FacetSummary.cs +++ b/EvitaDB.Client/Models/ExtraResults/FacetSummary.cs @@ -1,11 +1,15 @@ using System.Collections.Immutable; +using EvitaDB.Client.DataTypes; using EvitaDB.Client.Models.Data; +using EvitaDB.Client.Models.Data.Structure; +using EvitaDB.Client.Models.Data.Structure.Predicates; +using EvitaDB.Client.Models.Schemas; using EvitaDB.Client.Models.Schemas.Dtos; using EvitaDB.Client.Utils; namespace EvitaDB.Client.Models.ExtraResults; -public class FacetSummary : IEvitaResponseExtraResult +public class FacetSummary : IEvitaResponseExtraResult, IPrettyPrintable { private readonly IDictionary _referenceStatistics; @@ -17,9 +21,12 @@ public FacetSummary(IDictionary> refer FacetGroupStatistics? nonGroupedStatistics = stats.Value .FirstOrDefault(x => x.GroupEntity is null); result.Add(stats.Key, - new ReferenceStatistics(nonGroupedStatistics, + new ReferenceStatistics( + nonGroupedStatistics, stats.Value.Where(x => x.GroupEntity is not null) - .ToDictionary(key => key.GroupEntity!.PrimaryKey!.Value, value => value))); + .ToDictionary(key => key.GroupEntity!.PrimaryKey!.Value, value => value) + ) + ); } _referenceStatistics = result.ToImmutableDictionary(); @@ -54,6 +61,7 @@ public ICollection GetFacetGroupStatistics() => { rs.Add(x.NonGroupedStatistics); } + return rs; }) .ToList(); @@ -67,7 +75,7 @@ public override bool Equals(object? o) { if (this == o) return true; if (o == null || GetType() != o.GetType()) return false; - FacetSummary that = (FacetSummary) o; + FacetSummary that = (FacetSummary)o; foreach (KeyValuePair referenceEntry in _referenceStatistics) { @@ -85,10 +93,50 @@ public override bool Equals(object? o) public override string ToString() { - return ToString(statistics => "", facetStatistics => ""); + return "Facet summary with: " + _referenceStatistics.Count + " references"; } - public string ToString(Func groupRenderer, + public string PrettyPrint() + { + PrettyPrintingContext context = new PrettyPrintingContext(); + return PrettyPrint( + statistics => + statistics.GroupEntity is ISealedEntity entity + ? PrintRepresentative(entity, context) + : "", + facetStatistics => facetStatistics.FacetEntity is ISealedEntity entity + ? PrintRepresentative(entity, context) + : "" + ); + } + + private static string PrintRepresentative(ISealedEntity entity, PrettyPrintingContext context) + { + AttributeValuePredicate attributePredicate = ((Entity)entity).AttributePredicate; + if (!attributePredicate.WasFetched()) + { + return ""; + } + + ISet set = attributePredicate.AttributeSet; + if (!set.Any()) + { + return string.Join(", ", context.GetRepresentativeAttribute(entity.Schema) + .Select(attribute => EvitaDataTypes.FormatValue(entity.GetAttribute(attribute)?.ToString()))); + } + + if (set.Count == 1) + { + return EvitaDataTypes.FormatValue(entity.GetAttribute(set.First())); + } + + ISet representativeAttributes = context.GetRepresentativeAttribute(entity.Schema); + return string.Join(", ", set + .Where(x => representativeAttributes.Contains(x)) + .Select(attribute => EvitaDataTypes.FormatValue(entity.GetAttribute(attribute)?.ToString()))); + } + + public string PrettyPrint(Func groupRenderer, Func facetRenderer) { return "Facet summary:\n" + string.Join("\n", _referenceStatistics @@ -96,7 +144,7 @@ public string ToString(Func groupRenderer, .SelectMany(groupsByReferenceName => { ReferenceStatistics stats = groupsByReferenceName.Value; - ICollection groupStatistics = stats.GroupedStatistics.Values; + IList groupStatistics = stats.GroupedStatistics.Values.ToList(); if (stats.NonGroupedStatistics is not null) { groupStatistics.Add(stats.NonGroupedStatistics); @@ -106,7 +154,7 @@ public string ToString(Func groupRenderer, (groupRenderer(statistics).Trim() != "" ? groupRenderer(statistics) : statistics.GroupEntity?.PrimaryKey.ToString() ?? - "") + + "non-grouped") + " [" + statistics.Count + "]:\n" + string.Join("\n", statistics .GetFacetStatistics() @@ -121,7 +169,41 @@ public string ToString(Func groupRenderer, ) ); } - )); + ) + ); + } + + private class PrettyPrintingContext + { + /// + /// Contains set of representative attribute names for each entity type. + /// + private Dictionary> RepresentativeAttributes { get; } = new(); + + /// + /// Returns set of names for passed entity schema. + /// + /// Entity schema to get representative attributes for. + /// Set of representative attribute names. + public ISet GetRepresentativeAttribute(IEntitySchema entitySchema) + { + if (RepresentativeAttributes.TryGetValue(entitySchema.Name, out ISet? attributes)) + { + return attributes; + } + + ISet attributeNames = entitySchema + .Attributes + .Values + .Where(x => x.Representative) + .Select(a => a.Name) + .ToHashSet(); + RepresentativeAttributes.Add( + entitySchema.Name, + attributeNames + ); + return attributeNames; + } } } @@ -136,7 +218,6 @@ public override int GetHashCode() public virtual bool Equals(RequestImpact? other) { - if (this == other) return true; return Difference == other?.Difference && MatchCount == other.MatchCount; } @@ -183,7 +264,7 @@ public int CompareTo(FacetStatistics? other) return 0; } - return (int) FacetEntity.PrimaryKey?.CompareTo(other?.FacetEntity.PrimaryKey)!; + return (int)FacetEntity.PrimaryKey?.CompareTo(other?.FacetEntity.PrimaryKey)!; } public override int GetHashCode() @@ -195,7 +276,7 @@ public override bool Equals(object? o) { if (this == o) return true; if (o == null || GetType() != o.GetType()) return false; - FacetStatistics that = (FacetStatistics) o; + FacetStatistics that = (FacetStatistics)o; return Requested == that.Requested && Count == that.Count && Equals(FacetEntity, that.FacetEntity) && Equals(Impact, that.Impact); } @@ -319,7 +400,7 @@ public override bool Equals(object? o) { if (this == o) return true; if (o == null || GetType() != o.GetType()) return false; - FacetGroupStatistics that = (FacetGroupStatistics) o; + FacetGroupStatistics that = (FacetGroupStatistics)o; if (!ReferenceName.Equals(that.ReferenceName) || Count != that.Count || Equals(GroupEntity, that.GroupEntity) || @@ -347,4 +428,4 @@ public override int GetHashCode() { return HashCode.Combine(ReferenceName, GroupEntity?.Type, Count, _facetStatistics); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/ExtraResults/Histogram.cs b/EvitaDB.Client/Models/ExtraResults/Histogram.cs index 66d9de2..5e1c01b 100644 --- a/EvitaDB.Client/Models/ExtraResults/Histogram.cs +++ b/EvitaDB.Client/Models/ExtraResults/Histogram.cs @@ -1,5 +1,6 @@ using System.Text; using EvitaDB.Client.Utils; +using Newtonsoft.Json; namespace EvitaDB.Client.Models.ExtraResults; @@ -42,4 +43,4 @@ public override string ToString() } return sb.ToString(); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/ExtraResults/IHistogram.cs b/EvitaDB.Client/Models/ExtraResults/IHistogram.cs index ef88b60..04f0e4c 100644 --- a/EvitaDB.Client/Models/ExtraResults/IHistogram.cs +++ b/EvitaDB.Client/Models/ExtraResults/IHistogram.cs @@ -1,11 +1,29 @@ -namespace EvitaDB.Client.Models.ExtraResults; +using Newtonsoft.Json; + +namespace EvitaDB.Client.Models.ExtraResults; public interface IHistogram { + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] public decimal Min { get; } public decimal Max { get; } public int OverallCount { get; } public Bucket[] Buckets { get; } } -public record Bucket(int Index, decimal Threshold, int Occurrences, bool Requested); \ No newline at end of file +public record Bucket +{ + public int Index { get; init; } + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)] + public decimal Threshold { get; init; } + public int Occurrences { get; init; } + public bool Requested { get; init; } + + public Bucket(int index, decimal threshold, int occurrences, bool requested) + { + Index = index; + Threshold = threshold; + Occurrences = occurrences; + Requested = requested; + } +} diff --git a/EvitaDB.Client/Models/ExtraResults/IPrettyPrintable.cs b/EvitaDB.Client/Models/ExtraResults/IPrettyPrintable.cs new file mode 100644 index 0000000..f1141a3 --- /dev/null +++ b/EvitaDB.Client/Models/ExtraResults/IPrettyPrintable.cs @@ -0,0 +1,10 @@ +namespace EvitaDB.Client.Models.ExtraResults; + +public interface IPrettyPrintable +{ + /// + /// Returns pretty-printed string representation of the object in a text (MarkDown format). + /// + /// pretty-printed string representation of the object + string PrettyPrint(); +} diff --git a/EvitaDB.Client/Models/Schemas/Builders/InternalEntitySchemaBuilder.cs b/EvitaDB.Client/Models/Schemas/Builders/InternalEntitySchemaBuilder.cs index b19644b..5a03fd7 100644 --- a/EvitaDB.Client/Models/Schemas/Builders/InternalEntitySchemaBuilder.cs +++ b/EvitaDB.Client/Models/Schemas/Builders/InternalEntitySchemaBuilder.cs @@ -692,6 +692,10 @@ public bool SupportsLocale(CultureInfo locale) return _instance.SupportsLocale(locale); } + public IList OrderedAttributes => BaseSchema.OrderedAttributes; + + public IList OrderedAssociatedData => BaseSchema.OrderedAssociatedData; + public IAttributeSchema GetAttributeOrThrow(string name) { return _instance.GetAttributeOrThrow(name); @@ -767,4 +771,4 @@ IEntitySchemaBuilder IEntitySchemaEditor.CooperatingWith(F { return CooperatingWith(catalogSupplier); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Schemas/Dtos/EntitySchema.cs b/EvitaDB.Client/Models/Schemas/Dtos/EntitySchema.cs index 8d21754..16fa1c0 100644 --- a/EvitaDB.Client/Models/Schemas/Dtos/EntitySchema.cs +++ b/EvitaDB.Client/Models/Schemas/Dtos/EntitySchema.cs @@ -22,15 +22,21 @@ public class EntitySchema : IEntitySchema public ISet EvolutionModes { get; } public IEnumerable NonNullableAttributes { get; } public IEnumerable NonNullableAssociatedData { get; } - public IDictionary Attributes { get; } + + public IDictionary Attributes { get; } = + new Dictionary(); private IDictionary AttributeNameIndex { get; } private IDictionary SortableAttributeCompounds { get; } private IDictionary SortableAttributeCompoundNameIndex { get; } private IDictionary> AttributeToSortableAttributeCompoundIndex { get; } - public IDictionary AssociatedData { get; } + + public IDictionary AssociatedData { get; } = + new Dictionary(); private IDictionary AssociatedDataNameIndex { get; } public IDictionary References { get; } private IDictionary ReferenceNameIndex { get; } + public IList OrderedAttributes { get; } = new List(); + public IList OrderedAssociatedData { get; } = new List(); private EntitySchema( int version, @@ -61,13 +67,26 @@ private EntitySchema( IndexedPricePlaces = indexedPricePlaces; Locales = locales.ToImmutableSortedSet(Comparer.Create((x, y) => string.Compare(x.TwoLetterISOLanguageName, y.TwoLetterISOLanguageName, StringComparison.Ordinal))); Currencies = currencies.ToImmutableSortedSet(Comparer.Create((x, y) => string.Compare(x.CurrencyCode, y.CurrencyCode, StringComparison.Ordinal))); - Attributes = attributes.ToImmutableSortedDictionary(x => x.Key, x => x.Value); + + foreach (var (key, value) in attributes) + { + Attributes.Add(key, value); + OrderedAttributes.Add(value); + } AttributeNameIndex = InternalGenerateNameVariantIndex(Attributes.Values, x => x.NameVariants); - AssociatedData = associatedData.ToImmutableSortedDictionary(x => x.Key, x => x.Value); + Attributes = Attributes.ToImmutableDictionary(); + + foreach (var (key, value) in associatedData) + { + AssociatedData.Add(key, value); + OrderedAssociatedData.Add(value); + } AssociatedDataNameIndex = InternalGenerateNameVariantIndex(AssociatedData.Values, x => x.NameVariants); - References = references.ToImmutableSortedDictionary(x => x.Key, x => x.Value); + AssociatedData = AssociatedData.ToImmutableDictionary(); + + References = references.ToImmutableDictionary(x => x.Key, x => x.Value); ReferenceNameIndex = InternalGenerateNameVariantIndex(References.Values, x => x.NameVariants); EvolutionModes = evolutionMode; @@ -238,8 +257,8 @@ IDictionary sortableAttributeCompounds indexedPricePlaces, locales.ToImmutableHashSet(), currencies.ToImmutableHashSet(), - attributes.ToImmutableDictionary(), - associatedData.ToImmutableDictionary(), + attributes, + associatedData, references.ToImmutableDictionary(), evolutionMode.ToImmutableHashSet(), sortableAttributeCompounds.ToImmutableDictionary() @@ -441,4 +460,4 @@ public IList GetSortableAttributeCompoundsForAt private record AttributeToCompound(AttributeElement Attribute, SortableAttributeCompoundSchema CompoundSchema); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Schemas/EntitySchemaDecorator.cs b/EvitaDB.Client/Models/Schemas/EntitySchemaDecorator.cs index cac96e9..62d81cd 100644 --- a/EvitaDB.Client/Models/Schemas/EntitySchemaDecorator.cs +++ b/EvitaDB.Client/Models/Schemas/EntitySchemaDecorator.cs @@ -88,6 +88,10 @@ public bool SupportsLocale(CultureInfo locale) return Delegate.SupportsLocale(locale); } + public IList OrderedAttributes => Delegate.OrderedAttributes; + + public IList OrderedAssociatedData => Delegate.OrderedAssociatedData; + public IAttributeSchema GetAttributeOrThrow(string name) { return Delegate.GetAttributeOrThrow(name); @@ -179,4 +183,4 @@ public IList GetSortableAttributeCompoundsForAt { return Delegate.GetSortableAttributeCompoundsForAttribute(attributeName); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Models/Schemas/IEntitySchema.cs b/EvitaDB.Client/Models/Schemas/IEntitySchema.cs index 5a75744..045f921 100644 --- a/EvitaDB.Client/Models/Schemas/IEntitySchema.cs +++ b/EvitaDB.Client/Models/Schemas/IEntitySchema.cs @@ -35,4 +35,6 @@ bool SupportsCurrency(Currency currency) { return Currencies.Contains(currency); } -} \ No newline at end of file + IList OrderedAttributes { get; } + IList OrderedAssociatedData { get; } +} diff --git a/EvitaDB.Client/Queries/Filter/And.cs b/EvitaDB.Client/Queries/Filter/And.cs index e341f75..1503316 100644 --- a/EvitaDB.Client/Queries/Filter/And.cs +++ b/EvitaDB.Client/Queries/Filter/And.cs @@ -1,5 +1,24 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `and` container represents a logical conjunction. + +/// The following query: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// and( +/// entityPrimaryKeyInSet(110066, 106742, 110513), +/// entityPrimaryKeyInSet(110066, 106742), +/// entityPrimaryKeyInSet(107546, 106742, 107546) +/// ) +/// ) +/// ) +/// +/// ... returns a single result - product with entity primary key 106742, which is the only one that all three +/// `entityPrimaryKeyInSet` constraints have in common. +/// public class And : AbstractFilterConstraintContainer { public And(params IFilterConstraint?[] children) : base(children) @@ -10,4 +29,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return new And(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeBetween.cs b/EvitaDB.Client/Queries/Filter/AttributeBetween.cs index 16b7503..83246f9 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeBetween.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeBetween.cs @@ -1,5 +1,43 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `between` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument and value passed in third argument. First argument must be , second and third +/// argument may be any of supported comparable types. +/// +/// Type of the attribute value and second argument must be convertible one to another otherwise `between` function +/// returns false. +/// +/// Function returns true if value in a filterable attribute of such a name is greater than or equal to value in second argument +/// and lesser than or equal to value in third argument. +/// +/// Example: +/// +/// between("age", 20, 25) +/// +/// Function supports attribute arrays and when attribute is of array type `between` returns true if *any of attribute* values +/// is between the passed interval the value in the query. If we have the attribute `amount` with value `[1, 9]` all +/// these constraints will match: +/// +/// between("amount", 0, 50) +/// between("amount", 0, 5) +/// between("amount", 8, 10) +/// +/// If attribute is of `Range` type `between` query behaves like overlap - it returns true if examined range and +/// any of the attribute ranges (see previous paragraph about array types) share anything in common. All the following +/// constraints return true when we have the attribute `validity` with following `NumberRange` values: `[[2,5],[8,10]]`: +/// +/// between("validity", 0, 3) +/// between("validity", 0, 100) +/// between("validity", 9, 10) +/// +/// ... but these constraints will return false: +/// +/// between("validity", 11, 15) +/// between("validity", 0, 1) +/// between("validity", 6, 7) +/// +/// public class AttributeBetween : AbstractAttributeFilterConstraintLeaf where T : IComparable { private AttributeBetween(params object?[] arguments) : base(arguments) @@ -16,4 +54,4 @@ public AttributeBetween(string attributeName, T? from, T? to) : base(attributeNa public override bool Applicable => Arguments.Length == 3 && (From is not null || To is not null); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeContains.cs b/EvitaDB.Client/Queries/Filter/AttributeContains.cs index d52cad5..efc1ba2 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeContains.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeContains.cs @@ -1,5 +1,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `contains` is query that searches value of the attribute with name passed in first argument for presence of the +/// value passed in the second argument. +/// +/// Function returns true if attribute value contains secondary argument (starting with any position). Function is case +/// sensitive and comparison is executed using `UTF-8` encoding (C# native). +/// Example: +/// +/// contains("code", "evitaDB") +/// +/// Function supports attribute arrays and when attribute is of array type `contains` returns true if any of attribute +/// values contains the value in the query. If we have the attribute `code` with value `["cat","mouse","dog"]` all these +/// constraints will match: +/// +/// contains("code","mou") +/// contains("code","o") +/// +/// public class AttributeContains : AbstractAttributeFilterConstraintLeaf { public string TextToSearch => (string) Arguments[1]!; @@ -12,4 +30,4 @@ private AttributeContains(params object?[] arguments) : base(arguments) public AttributeContains(string attributeName, string textToSearch) : base(attributeName, textToSearch) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeEndsWith.cs b/EvitaDB.Client/Queries/Filter/AttributeEndsWith.cs index 7bdc7e9..a4cfbe9 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeEndsWith.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeEndsWith.cs @@ -1,5 +1,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `endsWith` is query that searches value of the attribute with name passed in first argument for presence of the +/// value passed in the second argument. +/// Function returns true if attribute value contains secondary argument (using reverse lookup from last position). +/// InSet other words attribute value ends with string passed in second argument. Function is case sensitive and comparison +/// is executed using `UTF-8` encoding (C# native). +/// Example: +/// +/// endsWith("code", "ida") +/// +/// Function supports attribute arrays and when attribute is of array type `endsWith` returns true if any of attribute +/// values ends with the value in the query. If we have the attribute `code` with value `["cat","mouse","dog"]` all these +/// constraints will match: +/// +/// contains("code","at") +/// contains("code","og") +/// +/// public class AttributeEndsWith : AbstractAttributeFilterConstraintLeaf { private AttributeEndsWith(params object?[] arguments) : base(arguments) @@ -13,4 +31,4 @@ public AttributeEndsWith(string attributeName, string textToSearch) : base(attri public string TextToSearch => (string) Arguments[1]!; public new bool Applicable => IsArgumentsNonNull() && Arguments.Length == 2; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeEquals.cs b/EvitaDB.Client/Queries/Filter/AttributeEquals.cs index bd44ac2..1a73e89 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeEquals.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeEquals.cs @@ -1,5 +1,24 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `equals` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument. First argument must be , second argument may be any of comparable types. +/// Type of the attribute value and second argument must be convertible one to another otherwise `equals` function +/// returns false. +/// Function returns true if both values are equal. +/// Example: +/// +/// equals("code", "abc") +/// +/// Function supports attribute arrays and when attribute is of array type `equals` returns true if any of attribute values +/// equals the value in the query. If we have the attribute `code` with value `["A","B","C"]` all these constraints will +/// match: +/// +/// equals("code","A") +/// equals("code","B") +/// equals("code","C") +/// +/// public class AttributeEquals : AbstractAttributeFilterConstraintLeaf { public T AttributeValue => (T?) Arguments[1]!; @@ -13,4 +32,4 @@ private AttributeEquals(params object?[] arguments) : base(arguments) public AttributeEquals(string attributeName, T attributeValue) : base(attributeName, attributeValue) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeGreaterThan.cs b/EvitaDB.Client/Queries/Filter/AttributeGreaterThan.cs index 6f48b32..c27ad46 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeGreaterThan.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeGreaterThan.cs @@ -1,5 +1,18 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `greaterThan` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument. First argument must be , second argument may be any of comparable types. +/// Type of the attribute value and second argument must be convertible one to another otherwise `greaterThan` function +/// returns false. +/// Function returns true if value in a filterable attribute of such a name is greater than value in second argument. +/// Function currently doesn't support attribute arrays and when attribute is of array type. Query returns error when this +/// query is used in combination with array type attribute. This may however change in the future. +/// Example: +/// +/// greaterThan("age", 20) +/// +/// public class AttributeGreaterThan : AbstractAttributeFilterConstraintLeaf where T : IComparable { private AttributeGreaterThan(params object?[] arguments) : base(arguments) @@ -13,4 +26,4 @@ public AttributeGreaterThan(string attributeName, T value) : base(attributeName, public T AttributeValue => (T) Arguments[1]!; public override bool Applicable => Arguments.Length == 2 && IsArgumentsNonNull(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeGreaterThanEquals.cs b/EvitaDB.Client/Queries/Filter/AttributeGreaterThanEquals.cs index 866e9a6..9465432 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeGreaterThanEquals.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeGreaterThanEquals.cs @@ -1,5 +1,19 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `greaterThanEquals` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument. First argument must be , second argument may be any of comparable types. +/// Type of the attribute value and second argument must be convertible one to another otherwise `greaterThanEquals` function +/// returns false. +/// Function returns true if value in a filterable attribute of such a name is greater than value in second argument or +/// equal. +/// Function currently doesn't support attribute arrays and when attribute is of array type. Query returns error when this +/// query is used in combination with array type attribute. This may however change in the future. +/// Example: +/// +/// greaterThanEquals("age", 20) +/// +/// public class AttributeGreaterThanEquals : AbstractAttributeFilterConstraintLeaf where T : IComparable { private AttributeGreaterThanEquals(params object?[] arguments) : base(arguments) @@ -13,4 +27,4 @@ public AttributeGreaterThanEquals(string attributeName, T value) : base(attribut public T AttributeValue => (T) Arguments[1]!; public override bool Applicable => Arguments.Length == 2 && IsArgumentsNonNull(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeInRange.cs b/EvitaDB.Client/Queries/Filter/AttributeInRange.cs index c8a8168..7bb6e0d 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeInRange.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeInRange.cs @@ -1,7 +1,29 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.DataTypes; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Filter; +/// +/// This `inRange` is query that compares value of the attribute with name passed in first argument with the date +/// and time passed in the second argument. First argument must be , second argument must be +/// type. If second argument is not passed - current date and time (now) is used. +/// Type of the attribute value must implement class. +/// Function returns true if second argument is greater than or equal to range start (from), and is lesser than +/// or equal to range end (to). +/// Example: +/// +/// inRange("valid", 2020-07-30T20:37:50+00:00) +/// inRange("age", 18) +/// +/// Function supports attribute arrays and when attribute is of array type `inRange` returns true if any of attribute +/// values has range, that envelopes the passed value the value in the query. If we have the attribute `age` with value +/// `[[18, 25],[60,65]]` all these constraints will match: +/// +/// inRange("age", 18) +/// inRange("age", 24) +/// inRange("age", 63) +/// +/// public class AttributeInRange : AbstractAttributeFilterConstraintLeaf where T : IComparable { @@ -51,4 +73,4 @@ public AttributeInRange(string attributeName, byte value) : base(attributeName, public TNumber? TheValue() where TNumber : struct, IComparable => Arguments is [_, TNumber theValue and (byte or short or int or long or decimal)] ? theValue : null; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeInSet.cs b/EvitaDB.Client/Queries/Filter/AttributeInSet.cs index 7979f0b..e8a60b2 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeInSet.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeInSet.cs @@ -1,5 +1,24 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `inSet` is query that compares value of the attribute with name passed in first argument with all the values passed +/// in the second, third and additional arguments. First argument must be , additional arguments may be any +/// of comparable types. +/// Type of the attribute value and additional arguments must be convertible one to another otherwise `in` function +/// skips value comparison and ultimately returns false. +/// Function returns true if attribute value is equal to at least one of additional values. +/// Example: +/// +/// inSet("level", 1, 2, 3) +/// +/// Function supports attribute arrays and when attribute is of array type `inSet` returns true if any of attribute values +/// equals the value in the query. If we have the attribute `code` with value `["A","B","C"]` all these constraints will +/// match: +/// +/// inSet("code","A","D") +/// inSet("code","A", "B") +/// +/// public class AttributeInSet : AbstractAttributeFilterConstraintLeaf { private AttributeInSet(params object?[] arguments) : base(arguments) @@ -13,4 +32,4 @@ public AttributeInSet(string attributeName, params T?[] attributeValues) : base( public object?[] AttributeValues => Arguments.Skip(1).ToArray(); public new bool Applicable => IsArgumentsNonNull() && Arguments.Length >= 2; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeIs.cs b/EvitaDB.Client/Queries/Filter/AttributeIs.cs index 8b65f6c..dd50ba9 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeIs.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeIs.cs @@ -1,5 +1,19 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `attributeIs` is query that checks attribute for "special" value or constant that cannot be compared +/// like comparable of attribute with name passed in first argument. +/// First argument must be . Second is one of the : +/// - +/// - +/// Function returns true if attribute has (explicitly or implicitly) passed special value. +/// Example: +/// +/// attributeIs("visible", NULL) +/// +/// Function supports attribute arrays in the same way as plain values. +/// +/// public class AttributeIs : AbstractAttributeFilterConstraintLeaf { private AttributeIs(params object?[] arguments) : base(arguments) @@ -13,4 +27,4 @@ public AttributeIs(string attributeName, AttributeSpecialValue value) : base(att public AttributeSpecialValue SpecialValue => (AttributeSpecialValue) Arguments[1]!; public override bool Applicable => Arguments.Length == 2 && IsArgumentsNonNull(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeLessThan.cs b/EvitaDB.Client/Queries/Filter/AttributeLessThan.cs index 18d91bc..8d85e56 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeLessThan.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeLessThan.cs @@ -1,5 +1,18 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `lessThan` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument. First argument must be , second argument may be any of comparable types. +/// Type of the attribute value and second argument must be convertible one to another otherwise `lessThan` function +/// returns false. +/// Function returns true if value in a filterable attribute of such a name is less than value in second argument. +/// Function currently doesn't support attribute arrays and when attribute is of array type. Query returns error when this +/// query is used in combination with array type attribute. This may however change in the future. +/// Example: +/// +/// lessThan("age", 20) +/// +/// public class AttributeLessThan : AbstractAttributeFilterConstraintLeaf where T : IComparable { private AttributeLessThan(params object?[] arguments) : base(arguments) @@ -13,4 +26,4 @@ public AttributeLessThan(string attributeName, T value) : base(attributeName, va public T AttributeValue => (T) Arguments[1]!; public override bool Applicable => Arguments.Length == 2 && IsArgumentsNonNull(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeLessThanEquals.cs b/EvitaDB.Client/Queries/Filter/AttributeLessThanEquals.cs index 46d5b47..60116f6 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeLessThanEquals.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeLessThanEquals.cs @@ -1,5 +1,19 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `lessThanEquals` is query that compares value of the attribute with name passed in first argument with the value passed +/// in the second argument. First argument must be , second argument may be any of comparable types. +/// Type of the attribute value and second argument must be convertible one to another otherwise `lessThanEquals` function +/// returns false. +/// Function returns true if value in a filterable attribute of such a name is lesser than value in second argument or +/// equal. +/// Function currently doesn't support attribute arrays and when attribute is of array type. Query returns error when this +/// query is used in combination with array type attribute. This may however change in the future. +/// Example: +/// +/// lessThanEquals("age", 20) +/// +/// public class AttributeLessThanEquals : AbstractAttributeFilterConstraintLeaf where T : IComparable { private AttributeLessThanEquals(params object[] arguments) : base(arguments) @@ -13,4 +27,4 @@ public AttributeLessThanEquals(string attributeName, T attributeValue) : base(at public T AttributeValue => (T) Arguments[1]!; public override bool Applicable => Arguments.Length == 2 && IsArgumentsNonNull(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeSpecialValue.cs b/EvitaDB.Client/Queries/Filter/AttributeSpecialValue.cs index 3cd5e25..b843c03 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeSpecialValue.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeSpecialValue.cs @@ -1,7 +1,13 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// Represents constant or "special" value attribute can have (or has it implicitly, e.g. missing value is represented by +/// `` that is not comparable by another ways. +/// +/// +/// public enum AttributeSpecialValue { Null, NotNull -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/AttributeStartsWith.cs b/EvitaDB.Client/Queries/Filter/AttributeStartsWith.cs index abb0e14..afa276f 100644 --- a/EvitaDB.Client/Queries/Filter/AttributeStartsWith.cs +++ b/EvitaDB.Client/Queries/Filter/AttributeStartsWith.cs @@ -1,5 +1,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// This `startsWith` is query that searches value of the attribute with name passed in first argument for presence of the +/// value passed in the second argument. +/// Function returns true if attribute value contains secondary argument (from first position). InSet other words attribute +/// value starts with string passed in second argument. Function is case-sensitive and comparison is executed using `UTF-8` +/// encoding (C# native). +/// Example: +/// +/// startsWith("code", "vid") +/// +/// Function supports attribute arrays and when attribute is of array type `startsWith` returns true if any of attribute +/// values starts with the value in the query. If we have the attribute `code` with value `["cat","mouse","dog"]` all +/// these constraints will match: +/// +/// contains("code","mou") +/// contains("code","do") +/// +/// public class AttributeStartsWith : AbstractAttributeFilterConstraintLeaf { private AttributeStartsWith(params object?[] arguments) : base(arguments) @@ -13,4 +31,4 @@ public AttributeStartsWith(string attributeName, string textToSearch) : base(att public string TextToSearch => (string) Arguments[1]!; public new bool Applicable => IsArgumentsNonNull() && Arguments.Length == 2; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/EntityHaving.cs b/EvitaDB.Client/Queries/Filter/EntityHaving.cs index 6fa0f23..abc599f 100644 --- a/EvitaDB.Client/Queries/Filter/EntityHaving.cs +++ b/EvitaDB.Client/Queries/Filter/EntityHaving.cs @@ -1,5 +1,20 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `entityHaving` constraint is used to examine the attributes or other filterable properties of the referenced +/// entity. It can only be used within the referenceHaving constraint, which defines the name of the entity reference +/// that identifies the target entity to be subjected to the filtering restrictions in the entityHaving constraint. +/// The filtering constraints for the entity can use entire range of filtering operators. +/// Example: +/// +/// referenceHaving( +/// "brand", +/// entityHaving( +/// attributeEquals("code", "apple") +/// ) +/// ) +/// +/// public class EntityHaving : AbstractFilterConstraintContainer { private EntityHaving() @@ -19,4 +34,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return children.Length == 0 ? new EntityHaving() : new EntityHaving(children[0]!); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/EntityLocaleEquals.cs b/EvitaDB.Client/Queries/Filter/EntityLocaleEquals.cs index d7cd4c1..4b17aea 100644 --- a/EvitaDB.Client/Queries/Filter/EntityLocaleEquals.cs +++ b/EvitaDB.Client/Queries/Filter/EntityLocaleEquals.cs @@ -2,6 +2,34 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// If any filter constraint of the query targets a localized attribute, the `entityLocaleEquals` must also be provided, +/// otherwise the query interpreter will return an error. Localized attributes must be identified by both their name and +/// in order to be used. +/// Only a single occurrence of entityLocaleEquals is allowed in the filter part of the query. Currently, there is no way +/// to switch context between different parts of the filter and build queries such as find a product whose name in en-US +/// is "screwdriver" or in cs is "šroubovák". +/// Also, it's not possible to omit the language specification for a localized attribute and ask questions like: find +/// a product whose name in any language is "screwdriver". +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "vouchers-for-shareholders") +/// ), +/// entityLocaleEquals("en") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code", "name") +/// ) +/// ) +/// ) +/// +/// public class EntityLocaleEquals : AbstractFilterConstraintLeaf { private EntityLocaleEquals(params object?[] arguments) : base(arguments) @@ -15,4 +43,4 @@ public EntityLocaleEquals(CultureInfo locale) : base(locale) public CultureInfo Locale => (Arguments[0] as CultureInfo)!; public new bool Applicable => IsArgumentsNonNull() && Arguments.Length == 1; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/EntityPrimaryKeyInSet.cs b/EvitaDB.Client/Queries/Filter/EntityPrimaryKeyInSet.cs index 4cd48b2..0b094e1 100644 --- a/EvitaDB.Client/Queries/Filter/EntityPrimaryKeyInSet.cs +++ b/EvitaDB.Client/Queries/Filter/EntityPrimaryKeyInSet.cs @@ -1,5 +1,13 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `entityPrimaryKeyInSet` constraint limits the list of returned entities by exactly specifying their entity +/// primary keys. +/// Example: +/// +/// primaryKey(1, 2, 3) +/// +/// public class EntityPrimaryKeyInSet : AbstractFilterConstraintLeaf { private EntityPrimaryKeyInSet(params object?[] arguments) : base(arguments) @@ -12,4 +20,4 @@ public EntityPrimaryKeyInSet(params int[] primaryKeys) : base(primaryKeys.Cast Arguments.Select(Convert.ToInt32).ToArray(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/FacetHaving.cs b/EvitaDB.Client/Queries/Filter/FacetHaving.cs index d7e571a..ee500c5 100644 --- a/EvitaDB.Client/Queries/Filter/FacetHaving.cs +++ b/EvitaDB.Client/Queries/Filter/FacetHaving.cs @@ -1,5 +1,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `facetHaving` filtering constraint is typically placed inside the constraint container and +/// represents the user's request to drill down the result set by a particular facet. The `facetHaving` constraint works +/// exactly like the referenceHaving constraint, but works in conjunction with the facetSummary requirement to correctly +/// calculate the facet statistics and impact predictions. When used outside the userFilter constraint container, +/// the `facetHaving` constraint behaves like the constraint. +/// Example: +/// +/// userFilter( +/// facetHaving( +/// "brand", +/// entityHaving( +/// attributeInSet("code", "amazon") +/// ) +/// ) +/// ) +/// +/// public class FacetHaving : AbstractFilterConstraintContainer { public string ReferenceName => (string) Arguments[0]!; @@ -22,4 +40,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return children.Length == 0 ? new FacetHaving(ReferenceName) : new FacetHaving(ReferenceName, children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/FilterBy.cs b/EvitaDB.Client/Queries/Filter/FilterBy.cs index 4bd2854..bb2ccbf 100644 --- a/EvitaDB.Client/Queries/Filter/FilterBy.cs +++ b/EvitaDB.Client/Queries/Filter/FilterBy.cs @@ -1,5 +1,20 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// Filtering constraints allow you to select only a few entities from many that exist in the target collection. It's +/// similar to the "where" clause in SQL. FilterBy container might contain one or more sub-constraints, that are combined +/// by logical disjunction (AND). +/// Example: +/// +/// filterBy( +/// isNotNull("code"), +/// or( +/// equals("code", "ABCD"), +/// startsWith("title", "Knife") +/// ) +/// ) +/// +/// public class FilterBy : AbstractFilterConstraintContainer { private FilterBy() : base() { @@ -17,4 +32,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch return children.Length > 0 ? new FilterBy(children[0]) : new FilterBy(); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/FilterGroupBy.cs b/EvitaDB.Client/Queries/Filter/FilterGroupBy.cs index c32a8a3..358d622 100644 --- a/EvitaDB.Client/Queries/Filter/FilterGroupBy.cs +++ b/EvitaDB.Client/Queries/Filter/FilterGroupBy.cs @@ -1,7 +1,25 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Filter; +/// +/// Filtering constraints allow you to select only a few entities from many that exist in the target collection. It's +/// similar to the "where" clause in SQL. FilterGroupBy container might contain one or more sub-constraints, that are +/// combined by logical disjunction (AND). +/// The `filterGroupBy` is equivalent to , but can be used only within container +/// and defines the filter constraints limiting the facet groups returned in facet summary. +/// Example: +/// +/// filterGroupBy( +/// isNotNull("code"), +/// or( +/// equals("code", "ABCD"), +/// startsWith("title", "Knife") +/// ) +/// ) +/// +/// public class FilterGroupBy : AbstractFilterConstraintContainer { private FilterGroupBy() @@ -19,4 +37,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch Assert.IsTrue(additionalChildren.Length == 0, "FilterGroupBy doesn't accept other than filtering constraints!"); return children.Length > 0 ? new FilterGroupBy(children) : new FilterGroupBy(); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyDirectRelation.cs b/EvitaDB.Client/Queries/Filter/HierarchyDirectRelation.cs index e5af13d..a6d8bde 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyDirectRelation.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyDirectRelation.cs @@ -1,5 +1,53 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `directRelation` is a constraint that can only be used within or +/// parent constraints. It simply makes no sense anywhere else because it changes the default +/// behavior of those constraints. Hierarchy constraints return all hierarchy children of the parent node or entities +/// that are transitively or directly related to them and the parent node itself. If the directRelation is used as +/// a sub-constraint, this behavior changes and only direct descendants or directly referencing entities are matched. +/// If the hierarchy constraint targets the hierarchy entity, the `directRelation` will cause only the children of +/// a direct parent node to be returned. In the case of the hierarchyWithinRoot constraint, the parent is an invisible +/// "virtual" top root - so only the top-level categories are returned. +/// +/// query( +/// collection('Category'), +/// filterBy( +/// hierarchyWithinRootSelf( +/// directRelation() +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent('code') +/// ) +/// ) +/// ) +/// +/// If the hierarchy constraint targets a non-hierarchical entity that references the hierarchical one (typical example +/// is a product assigned to a category), it can only be used in the hierarchyWithin parent constraint. +/// In the case of , the `directRelation` constraint makes no sense because no entity can be +/// assigned to a "virtual" top parent root. +/// So we can only list products that are directly related to a certain category. We can list products that have +/// Smartwatches category assigned: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "smartwatches"), +/// directRelation() +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// public class HierarchyDirectRelation : AbstractFilterConstraintLeaf, IHierarchySpecificationFilterConstraint { private const string ConstraintName = "directRelation"; @@ -13,4 +61,4 @@ public HierarchyDirectRelation() : base(ConstraintName) } public new bool Applicable => true; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyExcluding.cs b/EvitaDB.Client/Queries/Filter/HierarchyExcluding.cs index 1db959c..cdc46ea 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyExcluding.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyExcluding.cs @@ -2,6 +2,77 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `excluding` is a constraint that can only be used within or +/// parent constraints. It simply makes no sense anywhere else because it changes the default +/// behavior of those constraints. Hierarchy constraints return all hierarchy children of the parent node or entities +/// that are transitively or directly related to them, and the parent node itself. +/// The excluding constraint allows you to exclude one or more subtrees from the scope of the filter. This constraint is +/// the exact opposite of the having constraint. If the constraint is true for a hierarchy entity, it and all of its +/// children are excluded from the query. The excluding constraint is the same as declaring `having(not(expression))`, +/// but for the sake of readability it has its own constraint. +/// The constraint accepts following arguments: +/// - one or more mandatory constraints that must be satisfied by all returned hierarchy nodes and that mark the visible +/// part of the tree, the implicit relation between constraints is logical conjunction (boolean AND) +/// When the hierarchy constraint targets the hierarchy entity, the children that satisfy the inner constraints (and +/// their children, whether they satisfy them or not) are excluded from the result. +/// For demonstration purposes, let's list all categories within the Accessories category, but exclude exactly +/// the Wireless headphones subcategory. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories"), +/// excluding( +/// attributeEquals("code", "wireless-headphones") +/// ) +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// The category Wireless Headphones and all its subcategories will not be shown in the results list. +/// If the hierarchy constraint targets a non-hierarchical entity that references the hierarchical one (typical example +/// is a product assigned to a category), the excluding constraint is evaluated against the hierarchical entity +/// (category), but affects the queried non-hierarchical entities (products). It excludes all products referencing +/// categories that satisfy the excluding inner constraints. +/// Let's go back to our example query that excludes the Wireless Headphones category subtree. To list all products +/// available in the Accessories category except those related to the Wireless Headphones category or its subcategories, +/// issue the following query: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories"), +/// excluding( +/// attributeEquals("code", "wireless-headphones") +/// ) +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// You can see that wireless headphone products like Huawei FreeBuds 4, Jabra Elite 3 or Adidas FWD-02 Sport are not +/// present in the listing. +/// When the product is assigned to two categories - one excluded and one part of the visible category tree, the product +/// remains in the result. See the example. +/// The lookup stops at the first node that satisfies the constraint! +/// The hierarchical query traverses from the root nodes to the leaf nodes. For each of the nodes, the engine checks +/// whether the excluding constraint is satisfied valid, and if so, it excludes that hierarchy node and all of its child +/// nodes (entire subtree). +/// public class HierarchyExcluding : AbstractFilterConstraintContainer, IHierarchySpecificationFilterConstraint { private const string ConstraintName = "excluding"; @@ -23,4 +94,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch ); return new HierarchyExcluding(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyExcludingRoot.cs b/EvitaDB.Client/Queries/Filter/HierarchyExcludingRoot.cs index 7bb9e30..116738f 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyExcludingRoot.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyExcludingRoot.cs @@ -1,5 +1,61 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `excludingRoot` is a constraint that can only be used within or +/// parent constraints. It simply makes no sense anywhere else because it changes the default +/// behavior of those constraints. Hierarchy constraints return all hierarchy children of the parent node or entities +/// that are transitively or directly related to them and the parent node itself. When the excludingRoot is used as +/// a sub-constraint, this behavior changes and the parent node itself or the entities directly related to that parent +/// node are be excluded from the result. +/// If the hierarchy constraint targets the hierarchy entity, the `excludingRoot` will omit the requested parent node +/// from the result. In the case of the constraint, the parent is an invisible "virtual" top +/// root, and this constraint makes no sense. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories"), +/// excludingRoot() +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// If the hierarchy constraint targets a non-hierarchical entity that references the hierarchical one (typical example +/// is a product assigned to a category), the `excludingRoot` constraint can only be used in the +/// parent constraint. +/// In the case of , the `excludingRoot` constraint makes no sense because no entity can be +/// assigned to a "virtual" top parent root. +/// Because we learned that Accessories category has no directly assigned products, the `excludingRoot` constraint +/// presence would not affect the query result. Therefore, we choose Keyboard category for our example. When we list all +/// products in Keyboard category using constraint, we obtain 20 items. When the `excludingRoot` +/// constraint is used: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "keyboards"), +/// excludingRoot() +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// ... we get only 4 items, which means that 16 were assigned directly to Keyboards category and only 4 of them were +/// assigned to Exotic keyboards. +/// public class HierarchyExcludingRoot : AbstractFilterConstraintLeaf, IHierarchySpecificationFilterConstraint { private const string ConstraintName = "excludingRoot"; @@ -13,4 +69,4 @@ public HierarchyExcludingRoot() : base(ConstraintName) } public new bool Applicable => true; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyHaving.cs b/EvitaDB.Client/Queries/Filter/HierarchyHaving.cs index 940d86e..86c7839 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyHaving.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyHaving.cs @@ -2,6 +2,87 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `having` is a constraint that can only be used within or +/// parent constraints. It simply makes no sense anywhere else because it changes the default +/// behavior of those constraints. Hierarchy constraints return all hierarchy children of the parent node or entities +/// that are transitively or directly related to them, and the parent node itself. +/// The having constraint allows you to set a constraint that must be fulfilled by all categories in the category scope +/// in order to be accepted by hierarchy within filter. This constraint is especially useful if you want to conditionally +/// display certain parts of the tree. Imagine you have a category Christmas Sale that should only be available during +/// a certain period of the year, or a category B2B Partners that should only be accessible to a certain role of users. +/// All of these scenarios can take advantage of the having constraint (but there are other approaches to solving +/// the above use cases). +/// The constraint accepts following arguments: +/// - one or more mandatory constraints that must be satisfied by all returned hierarchy nodes and that mark the visible +/// part of the tree, the implicit relation between constraints is logical conjunction (boolean AND) +/// When the hierarchy constraint targets the hierarchy entity, the children that don't satisfy the inner constraints +/// (and their children, whether they satisfy them or not) are excluded from the result. +/// For demonstration purposes, let's list all categories within the Accessories category, but only those that are valid +/// at 01:00 AM on October 1, 2023. +/// +/// query( +/// collection('Category'), +/// filterBy( +/// hierarchyWithinSelf( +/// attributeEquals('code', 'accessories'), +/// having( +/// or( +/// attributeIsNull('validity'), +/// attributeInRange('validity', 2023-10-01T01:00:00-01:00) +/// ) +/// ) +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent('code') +/// ) +/// ) +/// ) +/// +/// Because the category Christmas electronics has its validity set to be valid only between December 1st and December +/// 24th, it will be omitted from the result. If it had subcategories, they would also be omitted (even if they had no +/// validity restrictions). +/// If the hierarchy constraint targets a non-hierarchical entity that references the hierarchical one (typical example +/// is a product assigned to a category), the having constraint is evaluated against the hierarchical entity (category), +/// but affects the queried non-hierarchical entities (products). It excludes all products referencing categories that +/// don't satisfy the having inner constraints. +/// Let's use again our example with Christmas electronics that is valid only between 1st and 24th December. To list all +/// products available at 01:00 AM on October 1, 2023, issue a following query: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories"), +/// having( +/// or( +/// attributeIsNull("validity"), +/// attributeInRange("validity", 2023-10-01T01:00:00-01:00) +/// ) +/// ) +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// You can see that Christmas products like Retlux Blue Christmas lightning, Retlux Warm white Christmas lightning or +/// Emos Candlestick are not present in the listing. +/// The lookup stops at the first node that doesn't satisfy the constraint! +/// The hierarchical query traverses from the root nodes to the leaf nodes. For each of the nodes, the engine checks +/// whether the having constraint is still valid, and if not, it excludes that hierarchy node and all of its child nodes +/// (entire subtree). +/// What if the product is linked to two categories - one that meets the constraint and one that does not? +/// In the situation where the single product, let's say Garmin Vivosmart 5, is in both the excluded category Christmas +/// Electronics and the included category Smartwatches, it will remain in the query result because there is at least one +/// product reference that is part of the visible part of the tree. +/// public class HierarchyHaving : AbstractFilterConstraintContainer, IHierarchySpecificationFilterConstraint { private const string ConstraintName = "having"; @@ -24,4 +105,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch ); return new HierarchyHaving(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyWithin.cs b/EvitaDB.Client/Queries/Filter/HierarchyWithin.cs index 3af9112..5c04023 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyWithin.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyWithin.cs @@ -3,6 +3,61 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `hierarchyWithin` allows you to restrict the search to only those entities that are part of +/// the hierarchy tree starting with the root node identified by the first argument of this constraint. In e-commerce +/// systems the typical representative of a hierarchical entity is a category, which will be used in all of our examples. +/// The constraint accepts following arguments: +/// - optional name of the queried entity reference schema that represents the relationship to the hierarchical entity +/// type, your entity may target different hierarchical entities in different reference types, or it may target +/// the same hierarchical entity through multiple semantically different references, and that is why the reference name +/// is used instead of the target entity type. +/// - a single mandatory filter constraint that identifies one or more hierarchy nodes that act as hierarchy roots; +/// multiple constraints must be enclosed in AND / OR containers +/// - optional constraints allow you to narrow the scope of the hierarchy; none or all of the constraints may be present: +/// +/// +/// +/// +/// +/// +/// The most straightforward usage is filtering the hierarchical entities themselves. +/// +/// query( +/// collection("Category"), +/// filterBy( +/// hierarchyWithinSelf( +/// attributeEquals("code", "accessories") +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// The `hierarchyWithin` constraint can also be used for entities that directly reference a hierarchical entity type. +/// The most common use case from the e-commerce world is a product that is assigned to one or more categories. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories") +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// Products assigned to two or more subcategories of Accessories category will only appear once in the response +/// (contrary to what you might expect if you have experience with SQL). +/// public class HierarchyWithin : AbstractFilterConstraintContainer, IHierarchyFilterConstraint, IConstraintContainerWithSuffix { private const string Suffix = "self"; @@ -57,4 +112,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return new HierarchyWithin(Arguments, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/HierarchyWithinRoot.cs b/EvitaDB.Client/Queries/Filter/HierarchyWithinRoot.cs index a807769..a32868d 100644 --- a/EvitaDB.Client/Queries/Filter/HierarchyWithinRoot.cs +++ b/EvitaDB.Client/Queries/Filter/HierarchyWithinRoot.cs @@ -3,6 +3,58 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The constraint `hierarchyWithinRoot` allows you to restrict the search to only those entities that are part of +/// the entire hierarchy tree. In e-commerce systems the typical representative of a hierarchical entity is a category. +/// The single difference to constraint is that it doesn't accept a root node specification. +/// Because evitaDB accepts multiple root nodes in your entity hierarchy, it may be helpful to imagine there is +/// an invisible "virtual" top root above all the top nodes (whose parent property remains NULL) you have in your entity +/// hierarchy and this virtual top root is targeted by this constraint. +/// The constraint accepts following arguments: +/// - optional name of the queried entity reference schema that represents the relationship to the hierarchical entity +/// type, your entity may target different hierarchical entities in different reference types, or it may target +/// the same hierarchical entity through multiple semantically different references, and that is why the reference name +/// is used instead of the target entity type. +/// - optional constraints allow you to narrow the scope of the hierarchy; none or all of the constraints may be present: +/// +/// +/// +/// +/// +/// The `hierarchyWithinRoot`, which targets the Category collection itself, returns all categories except those that +/// would point to non-existent parent nodes, such hierarchy nodes are called orphans and do not satisfy any hierarchy +/// query. +/// +/// query( +/// collection("Category"), +/// filterBy( +/// hierarchyWithinRootSelf() +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// The `hierarchyWithinRoot` constraint can also be used for entities that directly reference a hierarchical entity +/// type. The most common use case from the e-commerce world is a product that is assigned to one or more categories. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithinRoot("categories") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// +/// Products assigned to only one orphan category will be missing from the result. Products assigned to two or more +/// categories will only appear once in the response (contrary to what you might expect if you have experience with SQL). +/// public class HierarchyWithinRoot : AbstractFilterConstraintContainer, ISeparateEntityScopeContainer, IConstraintContainerWithSuffix { @@ -76,4 +128,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return new HierarchyWithinRoot(Arguments, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/Not.cs b/EvitaDB.Client/Queries/Filter/Not.cs index 566f067..e5036e5 100644 --- a/EvitaDB.Client/Queries/Filter/Not.cs +++ b/EvitaDB.Client/Queries/Filter/Not.cs @@ -1,5 +1,33 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `not` container represents a logical negation. +/// The following query: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// not( +/// entityPrimaryKeyInSet(110066, 106742, 110513) +/// ) +/// ) +/// ) +/// +/// ... returns thousands of results excluding the entities with primary keys mentioned in `entityPrimaryKeyInSet` +/// constraint. Because this situation is hard to visualize - let"s narrow our super set to only a few entities: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// entityPrimaryKeyInSet(110513, 66567, 106742, 66574, 66556, 110066), +/// not( +/// entityPrimaryKeyInSet(110066, 106742, 110513) +/// ) +/// ) +/// ) +/// +/// ... which returns only three products that were not excluded by the following `not` constraint. +/// public class Not : AbstractFilterConstraintContainer { private Not() @@ -15,4 +43,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return children.Length == 0 ? new Not() : new Not(children[0]); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/Or.cs b/EvitaDB.Client/Queries/Filter/Or.cs index f88ddcf..e470d91 100644 --- a/EvitaDB.Client/Queries/Filter/Or.cs +++ b/EvitaDB.Client/Queries/Filter/Or.cs @@ -1,5 +1,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `or` container represents a logical conjunction. +/// The following query: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// or( +/// entityPrimaryKeyInSet(110066, 106742, 110513), +/// entityPrimaryKeyInSet(110066, 106742), +/// entityPrimaryKeyInSet(107546, 106742, 107546) +/// ) +/// ) +/// ) +/// +/// ... returns four results representing a combination of all primary keys used in the `entityPrimaryKeyInSet` +/// constraints. +/// public class Or : AbstractFilterConstraintContainer { public Or(params IFilterConstraint?[] children) : base(children) @@ -10,4 +28,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return new Or(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/PriceBetween.cs b/EvitaDB.Client/Queries/Filter/PriceBetween.cs index 5715136..f943040 100644 --- a/EvitaDB.Client/Queries/Filter/PriceBetween.cs +++ b/EvitaDB.Client/Queries/Filter/PriceBetween.cs @@ -1,5 +1,18 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `priceBetween` constraint restricts the result set to items that have a price for sale within the specified price +/// range. This constraint is typically set by the user interface to allow the user to filter products by price, and +/// should be nested inside the userFilter constraint container so that it can be properly handled by the facet or +/// histogram computations. +/// Example: +/// +/// priceBetween(150.25, 220.0) +/// +/// Warning: Only a single occurrence of any of this constraint is allowed in the filter part of the query. +/// Currently, there is no way to switch context between different parts of the filter and build queries such as find +/// a product whose price is either in "CZK" or "EUR" currency at this or that time using this constraint. +/// public class PriceBetween : AbstractFilterConstraintLeaf { private PriceBetween(params object?[] arguments) : base(arguments) @@ -13,4 +26,4 @@ public PriceBetween(decimal? minPrice, decimal? maxPrice) : base(minPrice, maxPr public decimal? From => (decimal) Arguments[0]!; public decimal? To => (decimal) Arguments[1]!; public new bool Applicable => Arguments.Length == 2 && (From != null || To != null); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/PriceInCurrency.cs b/EvitaDB.Client/Queries/Filter/PriceInCurrency.cs index 8134150..d34eddb 100644 --- a/EvitaDB.Client/Queries/Filter/PriceInCurrency.cs +++ b/EvitaDB.Client/Queries/Filter/PriceInCurrency.cs @@ -2,6 +2,17 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `priceInCurrency` constraint can be used to limit the result set to entities that have a price in the specified +/// currency. Except for the standard use-case +/// you can also create query with this constraint only: +/// +/// priceInCurrency("EUR") +/// +/// Warning: Only a single occurrence of any of this constraint is allowed in the filter part of the query. +/// Currently, there is no way to switch context between different parts of the filter and build queries such as find +/// a product whose price is either in "CZK" or "EUR" currency at this or that time using this constraint. +/// public class PriceInCurrency : AbstractFilterConstraintLeaf { public Currency Currency => Arguments[0] as Currency ?? new Currency((string) Arguments[0]!); @@ -17,4 +28,4 @@ public PriceInCurrency(string currency) : base(null, currency) public PriceInCurrency(Currency currency) : base(currency) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/PriceInPriceLists.cs b/EvitaDB.Client/Queries/Filter/PriceInPriceLists.cs index 67524bd..6a25940 100644 --- a/EvitaDB.Client/Queries/Filter/PriceInPriceLists.cs +++ b/EvitaDB.Client/Queries/Filter/PriceInPriceLists.cs @@ -1,5 +1,26 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `priceInPriceLists` constraint defines the allowed set(s) of price lists that the entity must have to be included +/// in the result set. The order of the price lists in the argument is important for the final price for sale calculation +/// - see the price for sale calculation +/// algorithm documentation. Price list names are represented by plain String and are case-sensitive. Price lists +/// don't have to be stored in the database as an entity, and if they are, they are not currently associated with +/// the price list code defined in the prices of other entities. The pricing structure is simple and flat for now +/// (but this may change in the future). +/// Except for the standard use-case +/// you can also create query with this constraint only: +/// +/// priceInPriceLists( +/// "vip-group-1-level", +/// "vip-group-2-level", +/// "vip-group-3-level" +/// ) +/// +/// Warning: Only a single occurrence of any of this constraint is allowed in the filter part of the query. +/// Currently, there is no way to switch context between different parts of the filter and build queries such as find +/// a product whose price is either in "CZK" or "EUR" currency at this or that time using this constraint. +/// public class PriceInPriceLists : AbstractFilterConstraintLeaf { public string[] PriceLists => Arguments.Select(a => (string) a!).ToArray(); @@ -11,4 +32,4 @@ private PriceInPriceLists(params object?[] priceListNames) : base(priceListNames public PriceInPriceLists(params string[] priceListNames) : base(priceListNames) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/PriceValidIn.cs b/EvitaDB.Client/Queries/Filter/PriceValidIn.cs index 54328f7..9743c70 100644 --- a/EvitaDB.Client/Queries/Filter/PriceValidIn.cs +++ b/EvitaDB.Client/Queries/Filter/PriceValidIn.cs @@ -1,5 +1,16 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `priceValidIn` excludes all entities that don't have a valid price for sale at the specified date and time. If +/// the price doesn't have a validity property specified, it passes all validity checks. +/// Example: +/// +/// priceValidIn(2020-07-30T20:37:50+00:00) +/// +/// Warning: Only a single occurrence of any of this constraint is allowed in the filter part of the query. +/// Currently, there is no way to switch context between different parts of the filter and build queries such as find +/// a product whose price is either in "CZK" or "EUR" currency at this or that time using this constraint. +/// public class PriceValidIn : AbstractFilterConstraintLeaf, IConstraintWithSuffix { private const string Suffix = "now"; @@ -19,4 +30,4 @@ public PriceValidIn(DateTimeOffset theMoment) : base(theMoment) public string? SuffixIfApplied => Arguments.Length == 0 ? Suffix : null; bool IConstraintWithSuffix.ArgumentImplicitForSuffix(object argument) => false; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/ReferenceHaving.cs b/EvitaDB.Client/Queries/Filter/ReferenceHaving.cs index 964a19f..4ce2011 100644 --- a/EvitaDB.Client/Queries/Filter/ReferenceHaving.cs +++ b/EvitaDB.Client/Queries/Filter/ReferenceHaving.cs @@ -1,5 +1,29 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `referenceHaving` constraint eliminates entities which has no reference of particular name satisfying set of +/// filtering constraints. You can examine either the attributes specified on the relation itself or wrap the filtering +/// constraint in constraint to examine the attributes of the referenced entity. +/// The constraint is similar to SQL `EXISTS` operator. +/// Example (select entities having reference brand with category attribute equal to alternativeProduct): +/// +/// referenceHavingAttribute( +/// "brand", +/// attributeEquals("category", "alternativeProduct") +/// ) +/// +/// Example (select entities having any reference brand): +/// +/// referenceHavingAttribute("brand") +/// +/// Example (select entities having any reference brand of primary key 1): +/// +/// referenceHavingAttribute( +/// "brand", +/// entityPrimaryKeyInSet(1) +/// ) +/// +/// public class ReferenceHaving : AbstractFilterConstraintContainer { private ReferenceHaving(object[] arguments, params IFilterConstraint?[] children) : base(arguments, children) @@ -22,4 +46,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return Children.Length == 0 ? new ReferenceHaving(ReferenceName) : new ReferenceHaving(ReferenceName, children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Filter/UserFilter.cs b/EvitaDB.Client/Queries/Filter/UserFilter.cs index d257133..020a3e6 100644 --- a/EvitaDB.Client/Queries/Filter/UserFilter.cs +++ b/EvitaDB.Client/Queries/Filter/UserFilter.cs @@ -4,6 +4,23 @@ namespace EvitaDB.Client.Queries.Filter; +/// +/// The `userFilter` works identically to the and constraint, but it distinguishes the filter scope, which is controlled +/// by the user through some kind of user interface, from the rest of the query, which contains the mandatory constraints +/// on the result set. The user-defined scope can be modified during certain calculations (such as the facet or histogram +/// calculation), while the mandatory part outside of `userFilter` cannot. +/// Example: +/// +/// userFilter( +/// facetHaving( +/// "brand", +/// entityHaving( +/// attributeInSet("code", "amazon") +/// ) +/// ) +/// ) +/// +/// public class UserFilter : AbstractFilterConstraintContainer { private static readonly ISet ForbiddenTypes; @@ -42,4 +59,4 @@ public override IFilterConstraint GetCopyWithNewChildren(IFilterConstraint?[] ch { return new UserFilter(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Head/Collection.cs b/EvitaDB.Client/Queries/Head/Collection.cs index c8183de..ee1cde9 100644 --- a/EvitaDB.Client/Queries/Head/Collection.cs +++ b/EvitaDB.Client/Queries/Head/Collection.cs @@ -1,7 +1,13 @@ namespace EvitaDB.Client.Queries.Head; /// -/// Blabla +/// Each query must specify collection. This mandatory entity type controls what collection +/// the query will be applied on. +/// +/// Sample of the header is: +/// +/// collection('category') +/// /// public class Collection : ConstraintLeaf, IHeadConstraint { @@ -17,4 +23,4 @@ public override void Accept(IConstraintVisitor visitor) { visitor.Visit(this); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/IQueryConstraints.cs b/EvitaDB.Client/Queries/IQueryConstraints.cs index 0e95916..d98c428 100644 --- a/EvitaDB.Client/Queries/IQueryConstraints.cs +++ b/EvitaDB.Client/Queries/IQueryConstraints.cs @@ -14,62 +14,83 @@ public interface IQueryConstraints /// static Collection Collection(string entityType) => new(entityType); + /// static FilterBy? FilterBy(params IFilterConstraint?[]? constraint) => constraint == null ? null : new FilterBy(constraint); + /// static FilterGroupBy? FilterGroupBy(params IFilterConstraint?[]? constraints) => constraints == null ? null : new FilterGroupBy(constraints); + /// static And? And(params IFilterConstraint?[]? constraints) => constraints is null ? null : new And(constraints); + /// static Or? Or(params IFilterConstraint?[]? constraints) => constraints is null ? null : new Or(constraints); + /// static Not? Not(IFilterConstraint? constraint) => constraint is null ? null : new Not(constraint); + /// static ReferenceHaving? ReferenceHaving(string? referenceName, params IFilterConstraint?[]? constraints) => referenceName is null ? null : new ReferenceHaving(referenceName, constraints!); + /// static UserFilter? UserFilter(params IFilterConstraint?[]? constraints) => constraints is null ? null : new UserFilter(constraints); + /// static AttributeBetween? AttributeBetween(string attributeName, T? from, T? to) where T : IComparable => from is null && to is null ? null : new AttributeBetween(attributeName, from, to); + /// static AttributeContains? AttributeContains(string attributeName, string? textToSearch) => textToSearch is null ? null : new AttributeContains(attributeName, textToSearch); + /// static AttributeStartsWith? AttributeStartsWith(string attributeName, string? textToSearch) => textToSearch is null ? null : new AttributeStartsWith(attributeName, textToSearch); + /// static AttributeEndsWith? AttributeEndsWith(string attributeName, string? textToSearch) => textToSearch is null ? null : new AttributeEndsWith(attributeName, textToSearch); + /// static AttributeEquals? AttributeEquals(string attributeName, T? attributeValue) => attributeValue is null ? null : new AttributeEquals(attributeName, attributeValue); + /// static AttributeLessThan? AttributeLessThan(string attributeName, T? attributeValue) where T : IComparable => attributeValue is null ? null : new AttributeLessThan(attributeName, attributeValue); + /// static AttributeLessThanEquals? AttributeLessThanEquals(string attributeName, T? attributeValue) where T : IComparable => attributeValue is null ? null : new AttributeLessThanEquals(attributeName, attributeValue); + /// static AttributeGreaterThan? AttributeGreaterThan(string attributeName, T? attributeValue) where T : IComparable => attributeValue is null ? null : new AttributeGreaterThan(attributeName, attributeValue); + /// static AttributeGreaterThanEquals? AttributeGreaterThanEquals(string attributeName, T? attributeValue) where T : IComparable => attributeValue is null ? null : new AttributeGreaterThanEquals(attributeName, attributeValue); + + /// static PriceInPriceLists? PriceInPriceLists(params string[]? priceListNames) => priceListNames is null ? null : new PriceInPriceLists(priceListNames); + /// static PriceInCurrency? PriceInCurrency(string? currency) => currency is null ? null : new PriceInCurrency(currency); + /// static PriceInCurrency? PriceInCurrency(Currency? currency) => currency is null ? null : new PriceInCurrency(currency); + /// static HierarchyWithin? HierarchyWithinSelf(IFilterConstraint? ofParent, params IHierarchySpecificationFilterConstraint[]? with) { @@ -86,6 +107,7 @@ public interface IQueryConstraints return new HierarchyWithin(ofParent, with); } + /// static HierarchyWithin? HierarchyWithin(string referenceName, IFilterConstraint? ofParent, params IHierarchySpecificationFilterConstraint?[]? with) { @@ -101,53 +123,70 @@ public interface IQueryConstraints return new HierarchyWithin(referenceName, ofParent, with!); } + + /// static HierarchyWithinRoot HierarchyWithinRootSelf(params IHierarchySpecificationFilterConstraint?[]? with) => with is null ? new HierarchyWithinRoot() : new HierarchyWithinRoot(with); + /// static HierarchyWithinRoot HierarchyWithinRoot(string referenceName, params IHierarchySpecificationFilterConstraint?[]? with) => with is null ? new HierarchyWithinRoot() : new HierarchyWithinRoot(referenceName, with); + /// static HierarchyHaving? Having(params IFilterConstraint?[]? includeChildTreeConstraints) => ArrayUtils.IsEmpty(includeChildTreeConstraints) ? null : new HierarchyHaving(includeChildTreeConstraints!); + /// static HierarchyExcluding? Excluding(params IFilterConstraint[]? excludeChildTreeConstraints) => ArrayUtils.IsEmpty(excludeChildTreeConstraints) ? null : new HierarchyExcluding(excludeChildTreeConstraints!); + /// static HierarchyDirectRelation DirectRelation() => new HierarchyDirectRelation(); + /// static HierarchyExcludingRoot ExcludingRoot() => new HierarchyExcludingRoot(); + /// static EntityLocaleEquals? EntityLocaleEquals(CultureInfo? locale) => locale is null ? null : new EntityLocaleEquals(locale); + /// static EntityHaving? EntityHaving(IFilterConstraint? filterConstraint) => filterConstraint is null ? null : new EntityHaving(filterConstraint); + /// static AttributeInRange? AttributeInRange(string attributeName, DateTimeOffset? dateTimeOffsetValue) => dateTimeOffsetValue is null ? null : new AttributeInRange(attributeName, dateTimeOffsetValue.Value); + /// static AttributeInRange? AttributeInRange(string attributeName, int? intValue) => intValue is null ? null : new AttributeInRange(attributeName, intValue.Value); + /// static AttributeInRange? AttributeInRange(string attributeName, long? longValue) => longValue is null ? null : new AttributeInRange(attributeName, longValue.Value); + /// static AttributeInRange? AttributeInRange(string attributeName, short? shortValue) => shortValue is null ? null : new AttributeInRange(attributeName, shortValue.Value); + /// static AttributeInRange? AttributeInRange(string attributeName, byte? byteValue) => byteValue is null ? null : new AttributeInRange(attributeName, byteValue.Value); + /// static AttributeInRange? AttributeInRange(string attributeName, decimal? decimalValue) => decimalValue is null ? null : new AttributeInRange(attributeName, decimalValue.Value); + /// static AttributeInRange AttributeInRangeNow(string attributeName) => new(attributeName); + /// static AttributeInSet? AttributeInSet(string attributeName, params T[]? set) { if (set is null) @@ -175,104 +214,144 @@ dateTimeOffsetValue is null return new AttributeInSet(attributeName, limitedSet); } + /// static AttributeEquals AttributeEqualsFalse(string attributeName) => new AttributeEquals(attributeName, false); + /// static AttributeEquals AttributeEqualsTrue(string attributeName) => new AttributeEquals(attributeName, true); + /// static AttributeIs? AttributeIs(string attributeName, AttributeSpecialValue? specialValue) => specialValue is null ? null : new AttributeIs(attributeName, specialValue.Value); + /// static AttributeIs AttributeIsNull(string attributeName) => new(attributeName, AttributeSpecialValue.Null); + /// static AttributeIs AttributeIsNotNull(string attributeName) => new(attributeName, AttributeSpecialValue.NotNull); + /// static PriceBetween? PriceBetween(decimal? minPrice, decimal? maxPrice) => minPrice is null && maxPrice is null ? null : new PriceBetween(minPrice, maxPrice); - + + /// static PriceValidIn? PriceValidIn(DateTimeOffset? theMoment) => theMoment is null ? null : new PriceValidIn(theMoment.Value); + /// static PriceValidIn PriceValidInNow() => new(); - + + /// static FacetHaving? FacetHaving(string referenceName, params IFilterConstraint?[]? constraints) => ArrayUtils.IsEmpty(constraints) ? null : new FacetHaving(referenceName, constraints!); + /// static EntityPrimaryKeyInSet? EntityPrimaryKeyInSet(params int[]? primaryKeys) => primaryKeys == null ? null : new EntityPrimaryKeyInSet(primaryKeys); + /// static OrderBy? OrderBy(params IOrderConstraint?[]? constraints) => constraints is null ? null : new OrderBy(constraints); + /// static OrderGroupBy? OrderGroupBy(params IOrderConstraint?[]? constraints) => constraints is null ? null : new OrderGroupBy(constraints); - + + /// + static EntityPrimaryKeyNatural EntityPrimaryKeyNatural(OrderDirection? direction) { + return new EntityPrimaryKeyNatural(direction ?? OrderDirection.Asc); + } + + /// static EntityPrimaryKeyInFilter EntityPrimaryKeyInFilter() => new EntityPrimaryKeyInFilter(); + /// static EntityPrimaryKeyExact? EntityPrimaryKeyExact(params int[]? primaryKeys) => ArrayUtils.IsEmpty(primaryKeys) ? null : new EntityPrimaryKeyExact(primaryKeys!); + /// static AttributeSetInFilter? AttributeSetInFilter(string? attributeName) => string.IsNullOrEmpty(attributeName) ? null : new AttributeSetInFilter(attributeName); + /// static AttributeSetExact? AttributeSetExact(string? attributeName, params object[]? attributeValues) => ArrayUtils.IsEmpty(attributeValues) || string.IsNullOrEmpty(attributeName) ? null : new AttributeSetExact(attributeName, attributeValues!); - + + /// static ReferenceProperty? ReferenceProperty(string propertyName, params IOrderConstraint?[]? constraints) => constraints is null ? null : new ReferenceProperty(propertyName, constraints); + /// static EntityProperty? EntityProperty(params IOrderConstraint?[]? constraints) => constraints is null ? null : new EntityProperty(constraints); + /// static EntityGroupProperty? EntityGroupProperty(params IOrderConstraint?[]? constraints) => constraints == null ? null : new EntityGroupProperty(constraints); + /// static AttributeNatural AttributeNatural(string attributeName) => new(attributeName); + /// static AttributeNatural AttributeNatural(string attributeName, OrderDirection orderDirection) => new AttributeNatural(attributeName, orderDirection); + /// static PriceNatural PriceNatural() => new(); + /// static PriceNatural PriceNatural(OrderDirection orderDirection) => new(orderDirection); + /// static Random Random() => new(); + /// static Require? Require(params IRequireConstraint?[]? constraints) => constraints is null ? null : new Require(constraints); + /// static AttributeHistogram? AttributeHistogram(int requestedBucketCount, params string[]? attributeNames) => ArrayUtils.IsEmpty(attributeNames) ? null : new AttributeHistogram(requestedBucketCount, attributeNames!); + /// static PriceHistogram PriceHistogram(int requestedBucketCount) => new(requestedBucketCount); - static FacetGroupsConjunction? FacetGroupsConjunction(string referenceName, FilterBy? filterBy) => - filterBy is null || !filterBy.Applicable ? null : new FacetGroupsConjunction(referenceName, filterBy); - - static FacetGroupsDisjunction? FacetGroupsDisjunction(string referenceName, FilterBy? filterBy) => - filterBy is null || !filterBy.Applicable ? null : new FacetGroupsDisjunction(referenceName, filterBy); + /// + static FacetGroupsConjunction? FacetGroupsConjunction(string? referenceName, FilterBy? filterBy = null) => + referenceName is null ? null : new FacetGroupsConjunction(referenceName, filterBy); - static FacetGroupsNegation? FacetGroupsNegation(string referenceName, FilterBy? filterBy) => - filterBy is null || !filterBy.Applicable ? null : new FacetGroupsNegation(referenceName, filterBy); + /// + static FacetGroupsDisjunction? FacetGroupsDisjunction(string? referenceName, FilterBy? filterBy = null) => + referenceName is null ? null : new FacetGroupsDisjunction(referenceName, filterBy); + /// + static FacetGroupsNegation? FacetGroupsNegation(string? referenceName, FilterBy? filterBy = null) => + referenceName is null ? null : new FacetGroupsNegation(referenceName, filterBy); + + /// static HierarchyOfSelf? HierarchyOfSelf(params IHierarchyRequireConstraint?[]? requirements) => ArrayUtils.IsEmpty(requirements) ? null : new HierarchyOfSelf(null, requirements!); + /// static HierarchyOfSelf? HierarchyOfSelf(OrderBy? orderBy, params IHierarchyRequireConstraint?[]? requirements) => ArrayUtils.IsEmpty(requirements) ? null : new HierarchyOfSelf(orderBy, requirements!); + /// static HierarchyOfReference? HierarchyOfReference(string referenceName, params IHierarchyRequireConstraint?[]? requirements) => HierarchyOfReference(referenceName, null, null, requirements!); + /// static HierarchyOfReference? HierarchyOfReference(string referenceName, OrderBy orderBy, params IHierarchyRequireConstraint?[]? requirements) => HierarchyOfReference(referenceName, null, orderBy, requirements!); + /// static HierarchyOfReference? HierarchyOfReference(string? referenceName, EmptyHierarchicalEntityBehaviour? emptyHierarchicalEntityBehaviour, params IHierarchyRequireConstraint?[]? requirements) => @@ -281,6 +360,7 @@ static AttributeNatural AttributeNatural(string attributeName, OrderDirection or : new HierarchyOfReference(referenceName, emptyHierarchicalEntityBehaviour ?? EmptyHierarchicalEntityBehaviour.RemoveEmpty, requirements!); + /// static HierarchyOfReference? HierarchyOfReference(string? referenceName, EmptyHierarchicalEntityBehaviour? emptyHierarchicalEntityBehaviour, OrderBy? orderBy, @@ -294,14 +374,17 @@ static AttributeNatural AttributeNatural(string attributeName, OrderDirection or requirements! ); + /// static HierarchyOfReference? HierarchyOfReference(string[]? referenceNames, params IHierarchyRequireConstraint[] requirements) => HierarchyOfReference(referenceNames, null, null, requirements); - + + /// static HierarchyOfReference? HierarchyOfReference(string[]? referenceNames, OrderBy? orderBy, params IHierarchyRequireConstraint[] requirements) => HierarchyOfReference(referenceNames, null, orderBy, requirements); + /// static HierarchyOfReference? HierarchyOfReference(string[]? referenceNames, EmptyHierarchicalEntityBehaviour? emptyHierarchicalEntityBehaviour, params IHierarchyRequireConstraint?[]? requirements) => @@ -310,6 +393,7 @@ static AttributeNatural AttributeNatural(string attributeName, OrderDirection or : new HierarchyOfReference(referenceNames!, emptyHierarchicalEntityBehaviour ?? EmptyHierarchicalEntityBehaviour.RemoveEmpty, requirements!); + /// static HierarchyOfReference? HierarchyOfReference(string[]? referenceNames, EmptyHierarchicalEntityBehaviour? emptyHierarchicalEntityBehaviour, OrderBy? orderBy, params IHierarchyRequireConstraint?[]? requirements) => @@ -319,57 +403,69 @@ static AttributeNatural AttributeNatural(string attributeName, OrderDirection or emptyHierarchicalEntityBehaviour ?? EmptyHierarchicalEntityBehaviour.RemoveEmpty, orderBy, requirements!); + /// static HierarchyFromRoot? FromRoot(string? outputName, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : requirements is null ? new HierarchyFromRoot(outputName) : new HierarchyFromRoot(outputName, requirements); + /// static HierarchyFromRoot? FromRoot(string? outputName, EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : requirements is null ? new HierarchyFromRoot(outputName, entityFetch) : new HierarchyFromRoot(outputName, entityFetch, requirements); + /// static HierarchyFromNode? FromNode(string? outputName, HierarchyNode? node, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null || node is null ? null : requirements is null ? new HierarchyFromNode(outputName, node) : new HierarchyFromNode(outputName, node, requirements!); + /// static HierarchyFromNode? FromNode(string? outputName, HierarchyNode? node, EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null || node is null ? null : entityFetch is null ? new HierarchyFromNode(outputName, node) : new HierarchyFromNode(outputName, node, entityFetch, requirements!); - + + /// static HierarchyChildren? Children(string? outputName, EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : new HierarchyChildren(outputName, entityFetch, requirements!); + /// static HierarchyChildren? Children(string? outputName, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : new HierarchyChildren(outputName, requirements!); + /// static HierarchySiblings? Siblings(string? outputName, EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : new HierarchySiblings(outputName, entityFetch, requirements!); + /// static HierarchySiblings? Siblings(string? outputName, params IHierarchyOutputRequireConstraint[]? requirements) => outputName is null ? null : new HierarchySiblings(outputName, requirements!); + /// static HierarchySiblings Siblings(EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => new(null, entityFetch, requirements!); + /// static HierarchySiblings Siblings(params IHierarchyOutputRequireConstraint?[]? requirements) => new(null, requirements!); + /// static HierarchyParents? Parents(string? outputName, EntityFetch? entityFetch, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : new HierarchyParents(outputName, entityFetch, requirements!); + /// static HierarchyParents? Parents(string? outputName, EntityFetch? entityFetch, HierarchySiblings? siblings, params IHierarchyOutputRequireConstraint?[]? requirements) { @@ -390,11 +486,13 @@ static HierarchySiblings Siblings(params IHierarchyOutputRequireConstraint?[]? r : new HierarchyParents(outputName, entityFetch, siblings, requirements!); } + /// static HierarchyParents? Parents(string? outputName, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null : new HierarchyParents(outputName, requirements!); + /// static HierarchyParents? Parents(string? outputName, HierarchySiblings? siblings, params IHierarchyOutputRequireConstraint?[]? requirements) => outputName is null ? null @@ -402,21 +500,27 @@ static HierarchySiblings Siblings(params IHierarchyOutputRequireConstraint?[]? r ? new HierarchyParents(outputName, requirements!) : new HierarchyParents(outputName, siblings, requirements!); + /// static HierarchyStopAt? StopAt(IHierarchyStopAtRequireConstraint? stopConstraint) => stopConstraint is null ? null : new HierarchyStopAt(stopConstraint); + /// static HierarchyNode? Node(FilterBy? filterBy) => filterBy is null ? null : new HierarchyNode(filterBy); + /// static HierarchyLevel? Level(int? level) => level is null ? null : new HierarchyLevel(level.Value); + /// static HierarchyDistance? Distance(int? distance) => distance is null ? null : new HierarchyDistance(distance.Value); + /// static HierarchyStatistics Statistics(params StatisticsType[]? types) => types is null ? new HierarchyStatistics(StatisticsBase.WithoutUserFilter) : new HierarchyStatistics(StatisticsBase.WithoutUserFilter, types); + /// static HierarchyStatistics? Statistics(StatisticsBase? statisticsBase, params StatisticsType[]? types) => statisticsBase is null ? null @@ -424,43 +528,55 @@ statisticsBase is null ? new HierarchyStatistics(statisticsBase.Value) : new HierarchyStatistics(statisticsBase.Value, types); + /// static EntityFetch EntityFetch(params IEntityContentRequire?[]? requirements) => requirements is null ? new EntityFetch() : new EntityFetch(requirements); + /// static EntityGroupFetch EntityGroupFetch(params IEntityContentRequire?[]? requirements) => requirements is null ? new EntityGroupFetch() : new EntityGroupFetch(requirements); + /// static AttributeContent AttributeContentAll() => new(); + /// static AttributeContent AttributeContent(params string[]? attributeNames) => attributeNames is null ? new AttributeContent() : new AttributeContent(attributeNames); + /// static AssociatedDataContent AssociatedDataContentAll() => new(); + /// static AssociatedDataContent AssociatedDataContent(params string[]? associatedDataNames) => associatedDataNames is null ? new AssociatedDataContent() : new AssociatedDataContent(associatedDataNames); + /// static DataInLocales DataInLocalesAll() => new(); + /// static DataInLocales DataInLocales(params CultureInfo[]? locales) => locales is null ? new DataInLocales() : new DataInLocales(locales); - + + /// static ReferenceContent ReferenceContentAll() { return new ReferenceContent(); } + /// static ReferenceContent ReferenceContentAllWithAttributes() { return new ReferenceContent((AttributeContent?) null); } + /// static ReferenceContent ReferenceContentAllWithAttributes(AttributeContent? attributeContent) { return new ReferenceContent(attributeContent); } - + + /// static ReferenceContent ReferenceContent(string? referencedEntityType) { if (referencedEntityType == null) @@ -471,6 +587,7 @@ static ReferenceContent ReferenceContent(string? referencedEntityType) return new ReferenceContent(referencedEntityType); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, params string[]? attributeNames) { return new ReferenceContent( @@ -479,11 +596,13 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType) { return new ReferenceContent(referencedEntityType, (FilterBy?) null, null, null, null, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, AttributeContent? attributeContent) { @@ -493,6 +612,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy ); } + /// static ReferenceContent ReferenceContent(string[]? referencedEntityType) { if (referencedEntityType == null) @@ -503,6 +623,7 @@ static ReferenceContent ReferenceContent(string[]? referencedEntityType) return new ReferenceContent(referencedEntityType); } + /// static ReferenceContent ReferenceContent(string? referencedEntityType, EntityFetch? entityRequirement) { if (referencedEntityType == null && entityRequirement == null) @@ -520,6 +641,7 @@ static ReferenceContent ReferenceContent(string? referencedEntityType, EntityFet ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, EntityFetch? entityRequirement) { return new ReferenceContent( @@ -528,6 +650,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, AttributeContent? attributeContent, EntityFetch? entityRequirement) { @@ -537,6 +660,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy ); } + /// static ReferenceContent ReferenceContent(string? referencedEntityType, EntityGroupFetch? groupEntityRequirement) { if (referencedEntityType == null && groupEntityRequirement == null) @@ -552,6 +676,7 @@ static ReferenceContent ReferenceContent(string? referencedEntityType, EntityGro return new ReferenceContent(referencedEntityType, null, null, null, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, EntityGroupFetch? groupEntityRequirement) { @@ -561,6 +686,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referencedEntityType, AttributeContent? attributeContent, EntityGroupFetch? groupEntityRequirement) { @@ -569,7 +695,8 @@ static ReferenceContent ReferenceContentWithAttributes(string referencedEntityTy attributeContent, null, groupEntityRequirement ); } - + + /// static ReferenceContent ReferenceContent(string? referencedEntityType, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -581,6 +708,7 @@ static ReferenceContent ReferenceContent(string? referencedEntityType, EntityFet return new ReferenceContent(referencedEntityType, null, null, entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes( string referencedEntityType, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement ) @@ -591,6 +719,7 @@ static ReferenceContent ReferenceContentWithAttributes( ); } + /// static ReferenceContent ReferenceContentWithAttributes( string referencedEntityType, AttributeContent? attributeContent, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement @@ -602,6 +731,7 @@ static ReferenceContent ReferenceContentWithAttributes( ); } + /// static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, EntityFetch? entityRequirement) { if (referencedEntityTypes == null && entityRequirement == null) @@ -621,6 +751,7 @@ static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, Entity ); } + /// static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, EntityGroupFetch? groupEntityRequirement) { if (referencedEntityTypes == null && groupEntityRequirement == null) @@ -640,6 +771,7 @@ static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, Entity ); } + /// static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -651,27 +783,32 @@ static ReferenceContent ReferenceContent(string[]? referencedEntityTypes, Entity return new ReferenceContent(entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy) { return new ReferenceContent(referenceName, filterBy, null, null, null); } - + + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy) { return new ReferenceContent(referenceName, filterBy, null, null, null, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, AttributeContent? attributeContent) { return new ReferenceContent(referenceName, filterBy, null, attributeContent, null, null); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, EntityFetch? entityRequirement) { return new ReferenceContent(referenceName, filterBy, null, entityRequirement, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, EntityFetch? entityRequirement) { @@ -681,6 +818,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, AttributeContent? attributeContent, EntityFetch? entityRequirement) { @@ -690,12 +828,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, filterBy, null, null, groupEntityRequirement); } - + + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, EntityGroupFetch? groupEntityRequirement) { @@ -705,6 +845,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, AttributeContent? attributeContent, EntityGroupFetch? groupEntityRequirement) { @@ -714,12 +855,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, filterBy, null, entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -729,6 +872,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, AttributeContent? attributeContent, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -738,11 +882,13 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, OrderBy? orderBy) { return new ReferenceContent(referenceName, null, orderBy, null, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy) { return new ReferenceContent( @@ -751,6 +897,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, AttributeContent? attributeContent) { @@ -760,11 +907,13 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContent(string referenceName, OrderBy? orderBy, EntityFetch? entityRequirement) { return new ReferenceContent(referenceName, null, orderBy, entityRequirement, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, EntityFetch? entityRequirement) { @@ -774,6 +923,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, AttributeContent? attributeContent, EntityFetch? entityRequirement) { @@ -783,12 +933,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContent(string referenceName, OrderBy? orderBy, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, null, orderBy, null, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, EntityGroupFetch? groupEntityRequirement) { @@ -798,6 +950,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, AttributeContent? attributeContent, EntityGroupFetch? groupEntityRequirement) { @@ -807,12 +960,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContent(string referenceName, OrderBy? orderBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, null, orderBy, entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -822,6 +977,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, OrderBy? orderBy, AttributeContent? attributeContent, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -831,11 +987,13 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Ord ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, OrderBy? orderBy) { return new ReferenceContent(referenceName, filterBy, orderBy, null, null); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy) { return new ReferenceContent( @@ -844,6 +1002,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, AttributeContent? attributeContent) { @@ -853,12 +1012,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityFetch? entityRequirement) { return new ReferenceContent(referenceName, filterBy, orderBy, entityRequirement, null); } - + + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityFetch? entityRequirement) { @@ -868,6 +1029,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, AttributeContent? attributeContent, EntityFetch? entityRequirement) { @@ -877,12 +1039,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, filterBy, orderBy, null, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityGroupFetch? groupEntityRequirement) { @@ -892,6 +1056,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, AttributeContent? attributeContent, EntityGroupFetch? groupEntityRequirement) { @@ -901,12 +1066,14 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContent(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(referenceName, filterBy, orderBy, entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -916,6 +1083,7 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil ); } + /// static ReferenceContent ReferenceContentWithAttributes(string referenceName, FilterBy? filterBy, OrderBy? orderBy, AttributeContent? attributeContent, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { @@ -924,66 +1092,78 @@ static ReferenceContent ReferenceContentWithAttributes(string referenceName, Fil attributeContent, entityRequirement, groupEntityRequirement ); } - + + /// static ReferenceContent ReferenceContentAll(EntityFetch? entityRequirement) { return new ReferenceContent(entityRequirement, null); } + /// static ReferenceContent ReferenceContentAllWithAttributes(EntityFetch? entityRequirement) { return new ReferenceContent((AttributeContent?) null, entityRequirement, null); } + /// static ReferenceContent ReferenceContentAllWithAttributes(AttributeContent? attributeContent, EntityFetch? entityRequirement) { return new ReferenceContent(attributeContent, entityRequirement, null); } + /// static ReferenceContent ReferenceContentAll(EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(null, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentAllWithAttributes(EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent((AttributeContent?) null, null, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentAllWithAttributes(AttributeContent? attributeContent, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(attributeContent, null, groupEntityRequirement); } - - + + /// static ReferenceContent ReferenceContentAll(EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentAllWithAttributes(EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent((AttributeContent?) null, entityRequirement, groupEntityRequirement); } + /// static ReferenceContent ReferenceContentAllWithAttributes(AttributeContent? attributeContent, EntityFetch? entityRequirement, EntityGroupFetch? groupEntityRequirement) { return new ReferenceContent(attributeContent, entityRequirement, groupEntityRequirement); } + /// static HierarchyContent HierarchyContent() => new HierarchyContent(); - + + /// static HierarchyContent HierarchyContent(HierarchyStopAt? stopAt) => stopAt is null ? new HierarchyContent() : new HierarchyContent(stopAt); + /// static HierarchyContent HierarchyContent(EntityFetch? entityFetch) => entityFetch is null ? new HierarchyContent() : new HierarchyContent(entityFetch); + /// static HierarchyContent HierarchyContent(HierarchyStopAt? stopAt, EntityFetch? entityFetch) { if (stopAt is null && entityFetch is null) @@ -999,37 +1179,141 @@ static HierarchyContent HierarchyContent(HierarchyStopAt? stopAt, EntityFetch? e return new HierarchyContent(stopAt!); } + /// static PriceContent? PriceContent(PriceContentMode? contentMode, params string[]? priceLists) => contentMode is null ? null : ArrayUtils.IsEmpty(priceLists) ? new PriceContent(contentMode.Value) : new PriceContent(contentMode.Value, priceLists!); - + + /// static PriceContent PriceContentAll() => Requires.PriceContent.All(); + /// static PriceContent PriceContentRespectingFilter(params string[] priceLists) => Requires.PriceContent.RespectingFilter(priceLists); + /// static PriceType PriceType(QueryPriceMode priceMode) => new(priceMode); - + + /// static Page Page(int? pageNumber, int? pageSize) => new(pageNumber, pageSize); + /// static Strip Strip(int? offset, int? limit) => new(offset, limit); + /// static FacetSummary FacetSummary() => new(); - + + /// static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth) => statisticsDepth is null ? new FacetSummary(FacetStatisticsDepth.Counts) : new FacetSummary(statisticsDepth.Value); + + /// + static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth, params IEntityRequire?[]? requirements) => + statisticsDepth is null + ? new FacetSummary(FacetStatisticsDepth.Counts, requirements!) + : new FacetSummary(statisticsDepth.Value, requirements!); + /// static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth, FilterBy? facetFilterBy, OrderBy? facetOrderBy, params IEntityRequire?[]? requirements) => FacetSummary(statisticsDepth, facetFilterBy, null, facetOrderBy, null, requirements!); + /// static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth, FilterGroupBy? facetFilterGroupBy, OrderGroupBy? facetOrderGroupBy, params IEntityRequire?[]? requirements) => FacetSummary(statisticsDepth, null, facetFilterGroupBy, null, facetOrderGroupBy, requirements!); - + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + FilterGroupBy? facetGroupFilterBy, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, filterBy, facetGroupFilterBy, orderBy, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + FilterGroupBy? facetGroupFilterBy, + params IEntityRequire[] requirements + ) { + return FacetSummary(statisticsDepth, filterBy, facetGroupFilterBy, null, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + OrderBy? orderBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, null, null, orderBy, facetGroupOrderBy, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, filterBy, null, null, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, null, null, orderBy, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterGroupBy? facetGroupFilterBy, + params IEntityRequire[] requirements + ) { + return FacetSummary(statisticsDepth, null, facetGroupFilterBy, null, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, null, null, null, facetGroupOrderBy, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterGroupBy? facetGroupFilterBy, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, null, facetGroupFilterBy, orderBy, null, requirements); + } + + /// + static FacetSummary FacetSummary( + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return FacetSummary(statisticsDepth, filterBy, null, null, facetGroupOrderBy, requirements); + } + + /// static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth, FilterBy? facetFilterBy, FilterGroupBy? facetGroupFilterBy, OrderBy? facetOrderBy, OrderGroupBy? facetGroupOrderBy, params IEntityRequire?[]? requirements) @@ -1046,24 +1330,174 @@ static FacetSummary FacetSummary(FacetStatisticsDepth? statisticsDepth, FilterBy facetGroupOrderBy, requirements!); } - static FacetSummaryOfReference - FacetSummaryOfReference(string referenceName, params IEntityRequire[] requirements) => + /// + static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, params IEntityRequire[] requirements) => new(referenceName, FacetStatisticsDepth.Counts, requirements); + /// static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, FacetStatisticsDepth? statisticsDepth, params IEntityRequire[] requirements) => statisticsDepth is null ? new FacetSummaryOfReference(referenceName, FacetStatisticsDepth.Counts, requirements) : new FacetSummaryOfReference(referenceName, statisticsDepth.Value, requirements); + /// static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, FacetStatisticsDepth? statisticsDepth, FilterBy? facetFilterBy, OrderBy? facetOrderBy, params IEntityRequire[]? requirements) => FacetSummaryOfReference(referenceName, statisticsDepth, facetFilterBy, null, facetOrderBy, null, requirements); + /// static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, FacetStatisticsDepth? statisticsDepth, FilterGroupBy? facetGroupFilterBy, OrderGroupBy? facetGroupOrderBy, params IEntityRequire[]? requirements) => FacetSummaryOfReference(referenceName, statisticsDepth, null, facetGroupFilterBy, null, facetGroupOrderBy, requirements); + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + FilterGroupBy? facetGroupFilterBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, facetGroupFilterBy, null, facetGroupOrderBy, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterGroupBy? facetGroupFilterBy, + OrderBy? orderBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, facetGroupFilterBy, orderBy, facetGroupOrderBy, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + OrderBy? orderBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, null, orderBy, facetGroupOrderBy, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + FilterGroupBy? facetGroupFilterBy, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, facetGroupFilterBy, orderBy, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + FilterGroupBy? facetGroupFilterBy, + params IEntityRequire[] requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, facetGroupFilterBy, null, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + OrderBy? orderBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, null, orderBy, facetGroupOrderBy, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, null, null, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, null, orderBy, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterGroupBy? facetGroupFilterBy, + params IEntityRequire[] requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, facetGroupFilterBy, null, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, null, null, facetGroupOrderBy, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterGroupBy? facetGroupFilterBy, + OrderBy? orderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null ? null : + FacetSummaryOfReference(referenceName, statisticsDepth, null, facetGroupFilterBy, orderBy, null, requirements); + } + + /// + static FacetSummaryOfReference? FacetSummaryOfReference( + string? referenceName, + FacetStatisticsDepth? statisticsDepth, + FilterBy? filterBy, + OrderGroupBy? facetGroupOrderBy, + params IEntityRequire[]? requirements + ) { + return referenceName == null + ? null + : FacetSummaryOfReference(referenceName, statisticsDepth, filterBy, null, null, facetGroupOrderBy, + requirements); + } + /// static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, FacetStatisticsDepth? statisticsDepth, FilterBy? facetFilterBy, FilterGroupBy? facetGroupFilterBy, OrderBy? facetOrderBy, OrderGroupBy? facetGroupOrderBy, params IEntityRequire[]? requirements) @@ -1080,12 +1514,18 @@ static FacetSummaryOfReference FacetSummaryOfReference(string referenceName, Fac facetOrderBy, facetGroupOrderBy, requirements!); } + /// static QueryTelemetry QueryTelemetry() => new(); + /// static EntityFetch EntityFetchAll() => EntityFetch(AttributeContentAll(), AssociatedDataContentAll(), PriceContentAll(), ReferenceContentAllWithAttributes(), DataInLocales()); + /// + /// This method returns array of all requirements that are necessary to load full content of the entity including + /// all language specific attributes, all prices, all references and all associated data. + /// static IRequireConstraint?[] EntityFetchAllAnd(params IRequireConstraint?[] combineWith) { if (ArrayUtils.IsEmpty(combineWith)) @@ -1096,39 +1536,56 @@ static EntityFetch EntityFetchAll() => EntityFetch(AttributeContentAll(), Associ return EntityFetchAll().Concat(combineWith).ToArray(); } + /// + /// This interface marks all requirements that can be used for loading additional data to existing entity. + /// static IEntityContentRequire[] EntityFetchAllContent() => new IEntityContentRequire[] { AttributeContent(), AssociatedDataContent(), PriceContentAll(), ReferenceContentAllWithAttributes(), DataInLocales() }; + /// static Query Query(FilterBy? filter) => new(null, filter, null, null); + /// static Query Query(FilterBy? filter, OrderBy? order) => new(null, filter, order, null); + /// static Query Query(FilterBy? filter, OrderBy? order, Require? require) => new(null, filter, order, require); + /// static Query Query(FilterBy? filter, Require? require) => new(null, filter, null, require); + /// static Query Query(Collection? entityType) => new(entityType, null, null, null); + + /// static Query Query(Collection? entityType, FilterBy? filter) => new(entityType, filter, null, null); + /// static Query Query(Collection? entityType, FilterBy? filter, OrderBy? order) => new(entityType, filter, order, null); + /// static Query Query(Collection? entityType, FilterBy? filter, OrderBy? order, Require? require) => new(entityType, filter, order, require); + /// static Query Query(Collection? entityType, FilterBy? filter, Require? require, OrderBy? order) => new(entityType, filter, order, require); + /// static Query Query(Collection? entityType, OrderBy? order) => new(entityType, null, order, null); + /// static Query Query(Collection? entityType, OrderBy? order, Require? require) => new(entityType, null, order, require); + /// static Query Query(Collection? entityType, FilterBy? filter, Require? require) => new(entityType, filter, null, require); + /// static Query Query(Collection? entityType, Require? require) => new(entityType, null, null, require); } diff --git a/EvitaDB.Client/Queries/Order/AttributeNatural.cs b/EvitaDB.Client/Queries/Order/AttributeNatural.cs index e131924..ed46a63 100644 --- a/EvitaDB.Client/Queries/Order/AttributeNatural.cs +++ b/EvitaDB.Client/Queries/Order/AttributeNatural.cs @@ -1,5 +1,40 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The constraint allows output entities to be sorted by their attributes in their natural order (numeric, alphabetical, +/// temporal). It requires specification of a single attribute and the direction of the ordering (default ordering is +/// . +/// Ordering is executed by natural order of the Java's Comparable interface. +/// Example: +/// +/// query( +/// collection("Product"), +/// orderBy( +/// attributeNatural("orderedQuantity", DESC) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code", "orderedQuantity") +/// ) +/// ) +/// ) +/// +/// If you want to sort products by their name, which is a localized attribute, you need to specify the +/// constraint in the part of the query. The correct collator is used to +/// order the localized attribute string, so that the order is consistent with the national customs of the language. +/// The sorting mechanism of evitaDB is somewhat different from what you might be used to. If you sort entities by two +/// attributes in an `orderBy` clause of the query, evitaDB sorts them first by the first attribute (if present) and then +/// by the second (but only those where the first attribute is missing). If two entities have the same value of the first +/// attribute, they are not sorted by the second attribute, but by the primary key (in ascending order). +/// If we want to use fast "pre-sorted" indexes, there is no other way to do it, because the secondary order would not be +/// known until a query time. If you want to sort by multiple attributes in the conventional way, you need to define the +/// sortable attribute compound in advance and use its name instead of the default attribute name. The sortable attribute +/// compound will cover multiple attributes and prepares a special sort index for this particular combination of +/// attributes, respecting the predefined order and NULL values behaviour. In the query, you can then use the compound +/// name instead of the default attribute name and achieve the expected results. +/// public class AttributeNatural : AbstractOrderConstraintLeaf { public string AttributeName => (string) Arguments[0]!; @@ -16,4 +51,4 @@ public AttributeNatural(string attributeName) : base(attributeName, OrderDirecti public AttributeNatural(string attributeName, OrderDirection direction) : base(attributeName, direction) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/AttributeSetExact.cs b/EvitaDB.Client/Queries/Order/AttributeSetExact.cs index c2d1806..65a8413 100644 --- a/EvitaDB.Client/Queries/Order/AttributeSetExact.cs +++ b/EvitaDB.Client/Queries/Order/AttributeSetExact.cs @@ -1,5 +1,24 @@ namespace EvitaDB.Client.Queries.Order; +/// +/// The constraint allows output entities to be sorted by attribute values in the exact order specified in the 2nd through +/// Nth arguments of this constraint. +/// Example usage: +/// +/// query( +/// filterBy( +/// attributeEqualsTrue("shortcut") +/// ), +/// orderBy( +/// attributeSetExact("code", "t-shirt", "sweater", "pants") +/// ) +/// ) +/// +/// The example will return the selected entities (if present) in the exact order of their `code` attributes that is +/// stated in the second to Nth argument of this ordering constraint. If there are entities, that have not the attribute +/// `code` , then they will be present at the end of the output in ascending order of their primary keys (or they will be +/// sorted by additional ordering constraint in the chain). +/// public class AttributeSetExact : AbstractOrderConstraintLeaf { private AttributeSetExact(params object?[] args) : base(args) @@ -16,4 +35,4 @@ public AttributeSetExact(string attributeName, params object[] attributeValues) public object[] AttributeValues => Arguments.Skip(1).ToArray()!; public new bool Applicable => IsArgumentsNonNull() && Arguments.Length > 1; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/AttributeSetInFilter.cs b/EvitaDB.Client/Queries/Order/AttributeSetInFilter.cs index 455d475..4226da6 100644 --- a/EvitaDB.Client/Queries/Order/AttributeSetInFilter.cs +++ b/EvitaDB.Client/Queries/Order/AttributeSetInFilter.cs @@ -1,5 +1,27 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The constraint allows to sort output entities by attribute values in the exact order that was used for filtering +/// them. The constraint requires presence of exactly one constraint in filter part of the query +/// that relates to the attribute with the same name as is used in the first argument of this constraint. +/// It uses array for sorting the output of the query. +/// Example usage: +///
+/// query(
+///    filterBy(
+///       attributeInSet("code", "t-shirt", "sweater", "pants")
+///    ),
+///    orderBy(
+///       attributeSetInFilter()
+///    )
+/// )
+/// 
+/// The example will return the selected entities (if present) in the exact order of their attribute `code` that was used +/// for array filtering them. The ordering constraint is particularly useful when you have sorted set of attribute values +/// from an external system which needs to be maintained (for example, it represents a relevancy of those entities). +///
public class AttributeSetInFilter : AbstractOrderConstraintLeaf { private AttributeSetInFilter(params object?[] args) : base(args) @@ -11,4 +33,4 @@ public AttributeSetInFilter(string attributeName) : base(attributeName) } public string AttributeName => (string) Arguments[0]!; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/EntityGroupProperty.cs b/EvitaDB.Client/Queries/Order/EntityGroupProperty.cs index 0cd962e..916e8b4 100644 --- a/EvitaDB.Client/Queries/Order/EntityGroupProperty.cs +++ b/EvitaDB.Client/Queries/Order/EntityGroupProperty.cs @@ -1,5 +1,70 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Requires; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The `entityGroupProperty` ordering constraint can only be used within the requirement. It +/// allows to change the context of the reference ordering from attributes of the reference itself to attributes of +/// the entity group the reference is aggregated within. +/// In other words, if the `Product` entity has multiple references to `Parameter` entities (blue/red/yellow) grouped +/// within `ParameterType` (color) entity, you can sort those references by, for example, the `priority` or `name` +/// attribute of the `ParameterType` entity. +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// attributeEquals("code", "garmin-vivoactive-4") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code"), +/// referenceContent( +/// "parameterValues", +/// orderBy( +/// entityGroupProperty( +/// attributeNatural("code", DESC) +/// ) +/// ), +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// Most of the time, you will want to group primarily by a group property and secondarily by a referenced entity +/// property, which can be achieved in the following way: +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// attributeEquals("code", "garmin-vivoactive-4") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code"), +/// referenceContent( +/// "parameterValues", +/// orderBy( +/// entityGroupProperty( +/// attributeNatural("code", DESC) +/// ), +/// entityProperty( +/// attributeNatural("code", DESC) +/// ) +/// ), +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// public class EntityGroupProperty : AbstractOrderConstraintContainer { public new bool Necessary => Children.Length >= 1; @@ -16,4 +81,4 @@ public override IOrderConstraint GetCopyWithNewChildren(IOrderConstraint?[] chil { return new EntityGroupProperty(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/EntityPrimaryKeyExact.cs b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyExact.cs index 0cebea7..0bf7846 100644 --- a/EvitaDB.Client/Queries/Order/EntityPrimaryKeyExact.cs +++ b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyExact.cs @@ -1,5 +1,24 @@ namespace EvitaDB.Client.Queries.Order; +/// +/// The constraint allows to sort output entities by primary key values in the exact order that is specified in +/// the arguments of this constraint. +/// Example usage: +/// +/// query( +/// filterBy( +/// attributeEqualsTrue("shortcut") +/// ), +/// orderBy( +/// entityPrimaryKeyExact(5, 1, 8) +/// ) +/// ) +/// +/// The example will return the selected entities (if present) in the exact order that is stated in the argument of +/// this ordering constraint. If there are entities, whose primary keys are not present in the argument, then they +/// will be present at the end of the output in ascending order of their primary keys (or they will be sorted by +/// additional ordering constraint in the chain). +/// public class EntityPrimaryKeyExact : AbstractOrderConstraintLeaf { private EntityPrimaryKeyExact(params object?[] args) : base(args) @@ -11,4 +30,4 @@ public EntityPrimaryKeyExact(params int[] primaryKeys) : base(primaryKeys.Cast Arguments.Select(x=> (int) x!).ToArray(); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/EntityPrimaryKeyInFilter.cs b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyInFilter.cs index b35fedc..44c501f 100644 --- a/EvitaDB.Client/Queries/Order/EntityPrimaryKeyInFilter.cs +++ b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyInFilter.cs @@ -1,5 +1,26 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The constraint allows to sort output entities by primary key values in the exact order that was used for filtering +/// them. The constraint requires presence of exactly one constraint in filter part of +/// the query. It uses array for sorting the output of the query. +/// Example usage: +/// +/// query( +/// filterBy( +/// entityPrimaryKeyInSet(5, 1, 8) +/// ), +/// orderBy( +/// entityPrimaryKeyInFilter() +/// ) +/// ) +/// +/// The example will return the selected entities (if present) in the exact order that was used for array filtering them. +/// The ordering constraint is particularly useful when you have sorted set of entity primary keys from an external +/// system which needs to be maintained (for example, it represents a relevancy of those entities). +/// public class EntityPrimaryKeyInFilter : AbstractOrderConstraintLeaf { private EntityPrimaryKeyInFilter(params object?[] args) : base(args) @@ -11,4 +32,4 @@ public EntityPrimaryKeyInFilter() : base() } public new bool Applicable => true; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/EntityPrimaryKeyNatural.cs b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyNatural.cs new file mode 100644 index 0000000..71e910c --- /dev/null +++ b/EvitaDB.Client/Queries/Order/EntityPrimaryKeyNatural.cs @@ -0,0 +1,34 @@ +namespace EvitaDB.Client.Queries.Order; + +/// +/// The constraint allows to sort output entities by primary key values in the exact order. +/// +/// Example usage: +/// +/// query( +/// orderBy( +/// entityPrimaryKeyNatural(DESC) +/// ) +/// ) +/// +/// The example will return the selected entities (if present) in the descending order of their primary keys. Since +/// the entities are by default ordered by their primary key in ascending order, it has no sense to use this constraint +/// with direction. +/// +public class EntityPrimaryKeyNatural : AbstractOrderConstraintLeaf +{ + private EntityPrimaryKeyNatural(params object[] arguments) : base(arguments) + { + } + + public EntityPrimaryKeyNatural(OrderDirection orderDirection) : base(orderDirection) + { + } + + public OrderDirection Direction => (OrderDirection)Arguments[0]!; + + public IOrderConstraint CloneWithArguments(params object[] newArguments) + { + return new EntityPrimaryKeyNatural(newArguments); + } +} diff --git a/EvitaDB.Client/Queries/Order/EntityProperty.cs b/EvitaDB.Client/Queries/Order/EntityProperty.cs index 128a51b..0c1ce40 100644 --- a/EvitaDB.Client/Queries/Order/EntityProperty.cs +++ b/EvitaDB.Client/Queries/Order/EntityProperty.cs @@ -1,5 +1,39 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Requires; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The `entityProperty` ordering constraint can only be used within the requirement. It allows +/// to change the context of the reference ordering from attributes of the reference itself to attributes of the entity +/// the reference points to. +/// In other words, if the `Product` entity has multiple references to `Parameter` entities, you can sort those references +/// by, for example, the `priority` or `name` attribute of the `Parameter` entity. +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// attributeEquals("code", "garmin-vivoactive-4") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code"), +/// referenceContent( +/// "parameterValues", +/// orderBy( +/// entityProperty( +/// attributeNatural("code", DESC) +/// ) +/// ), +/// entityFetch( +/// attributeContent("code") +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// public class EntityProperty : AbstractOrderConstraintContainer { public new bool Necessary => Children.Length >= 1; @@ -14,4 +48,4 @@ public override IOrderConstraint GetCopyWithNewChildren(IOrderConstraint?[] chil { return new EntityProperty(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/OrderBy.cs b/EvitaDB.Client/Queries/Order/OrderBy.cs index 27ac27e..b8f0f30 100644 --- a/EvitaDB.Client/Queries/Order/OrderBy.cs +++ b/EvitaDB.Client/Queries/Order/OrderBy.cs @@ -1,5 +1,30 @@ namespace EvitaDB.Client.Queries.Order; +/// +/// This `orderBy` is container for ordering. It is mandatory container when any ordering is to be used. +/// evitaDB requires a previously prepared sort index to be able to sort entities. This fact makes sorting much faster +/// than ad-hoc sorting by attribute value. Also, the sorting mechanism of evitaDB is somewhat different from what you +/// might be used to. If you sort entities by two attributes in an orderBy clause of the query, evitaDB sorts them first +/// by the first attribute (if present) and then by the second (but only those where the first attribute is missing). +/// If two entities have the same value of the first attribute, they are not sorted by the second attribute, but by the +/// primary key (in ascending order). If we want to use fast "pre-sorted" indexes, there is no other way to do it, +/// because the secondary order would not be known until a query time. +/// This default sorting behavior by multiple attributes is not always desirable, so evitaDB allows you to define +/// a sortable attribute compound, which is a virtual attribute composed of the values of several other attributes. +/// evitaDB also allows you to specify the order of the "pre-sorting" behavior (ascending/descending) for each of these +/// attributes, and also the behavior for NULL values (first/last) if the attribute is completely missing in the entity. +/// The sortable attribute compound is then used in the orderBy clause of the query instead of specifying the multiple +/// individual attributes to achieve the expected sorting behavior while maintaining the speed of the "pre-sorted" +/// indexes. +/// Example: +/// +/// orderBy( +/// ascending("code"), +/// ascending("create"), +/// priceDescending() +/// ) +/// +/// public class OrderBy : AbstractOrderConstraintContainer, IOrderConstraint { public new bool Necessary => Applicable; @@ -11,4 +36,4 @@ public override IOrderConstraint GetCopyWithNewChildren(IOrderConstraint?[] chil { return new OrderBy(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/OrderDirection.cs b/EvitaDB.Client/Queries/Order/OrderDirection.cs index 217b624..efc9479 100644 --- a/EvitaDB.Client/Queries/Order/OrderDirection.cs +++ b/EvitaDB.Client/Queries/Order/OrderDirection.cs @@ -1,7 +1,10 @@ namespace EvitaDB.Client.Queries.Order; +/// +/// Used in order constraints to specify ordering direction. +/// public enum OrderDirection { Asc, Desc -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/OrderGroupBy.cs b/EvitaDB.Client/Queries/Order/OrderGroupBy.cs index 7d9f865..6443b78 100644 --- a/EvitaDB.Client/Queries/Order/OrderGroupBy.cs +++ b/EvitaDB.Client/Queries/Order/OrderGroupBy.cs @@ -1,5 +1,47 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Requires; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The `entityGroupProperty` ordering constraint can only be used within the requirement. +/// It allows the context of the reference ordering to be changed from attributes of the reference itself to attributes +/// of the group entity within which the reference is aggregated. +/// In other words, if the Product entity has multiple references to ParameterValue entities that are grouped by their +/// assignment to the Parameter entity, you can sort those references primarily by the name attribute of the grouping +/// entity, and secondarily by the name attribute of the referenced entity. Let's look at an example: +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// attributeEquals("code", "garmin-vivoactive-4"), +/// entityLocaleEquals("en") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code"), +/// referenceContent( +/// "parameterValues", +/// orderBy( +/// entityGroupProperty( +/// attributeNatural("name", ASC) +/// ), +/// entityProperty( +/// attributeNatural("name", ASC) +/// ) +/// ), +/// entityFetch( +/// attributeContent("name") +/// ), +/// entityGroupFetch( +/// attributeContent("name") +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// public class OrderGroupBy : AbstractOrderConstraintContainer { public new bool Necessary => Applicable; @@ -14,4 +56,4 @@ public override IOrderConstraint GetCopyWithNewChildren(IOrderConstraint?[] chil { return new OrderGroupBy(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/PriceNatural.cs b/EvitaDB.Client/Queries/Order/PriceNatural.cs index 518ac71..ba897e0 100644 --- a/EvitaDB.Client/Queries/Order/PriceNatural.cs +++ b/EvitaDB.Client/Queries/Order/PriceNatural.cs @@ -1,5 +1,20 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Requires; +namespace EvitaDB.Client.Queries.Order; + +/// +/// The `priceNatural` constraint allows output entities to be sorted by their selling price in their natural numeric +/// order. It requires only the order direction and the price constraints in the `filterBy` section of the query. +/// The price variant (with or without tax) is determined by the requirement of the query (price with +/// tax is used by default). +/// Please read the price for sale +/// calculation algorithm documentation to understand how the price for sale is calculated. +/// Example: +/// +/// priceNatural() +/// priceNatural(Desc) +/// +/// public class PriceNatural : AbstractOrderConstraintLeaf { public new bool Applicable => IsArgumentsNonNull() && Arguments.Length == 1; @@ -15,4 +30,4 @@ public PriceNatural() : base(OrderDirection.Asc) public PriceNatural(OrderDirection direction) : base(direction) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/Random.cs b/EvitaDB.Client/Queries/Order/Random.cs index fb67b3e..64e7188 100644 --- a/EvitaDB.Client/Queries/Order/Random.cs +++ b/EvitaDB.Client/Queries/Order/Random.cs @@ -1,5 +1,14 @@ namespace EvitaDB.Client.Queries.Order; +/// +/// Random ordering is useful in situations where you want to present the end user with the unique entity listing every +/// time he/she accesses it. The constraint makes the order of the entities in the result random and does not take any +/// arguments. +/// Example: +/// +/// random() +/// +/// public class Random : AbstractOrderConstraintLeaf { public new bool Applicable => true; @@ -10,4 +19,4 @@ private Random(params object?[] arguments) : base(arguments) public Random() : base() { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Order/ReferenceProperty.cs b/EvitaDB.Client/Queries/Order/ReferenceProperty.cs index 6dde727..19622de 100644 --- a/EvitaDB.Client/Queries/Order/ReferenceProperty.cs +++ b/EvitaDB.Client/Queries/Order/ReferenceProperty.cs @@ -1,5 +1,68 @@ -namespace EvitaDB.Client.Queries.Order; +using EvitaDB.Client.Queries.Requires; +namespace EvitaDB.Client.Queries.Order; + +/// +/// Sorting by reference attribute is not as common as sorting by entity attributes, but it allows you to sort entities +/// that are in a particular category or have a particular brand specifically by the priority/order for that particular +/// relationship. +/// To sort products related to a "Sony" brand by the `priority` attribute set on the reference, you need to use the +/// following constraint: +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// referenceHaving( +/// "brand", +/// entityHaving( +/// attributeEquals("code","sony") +/// ) +/// ) +/// ), +/// orderBy( +/// referenceProperty( +/// "brand", +/// attributeNatural("orderInBrand", ASC) +/// ) +/// ), +/// require( +/// entityFetch( +/// attributeContent("code"), +/// referenceContentWithAttributes( +/// "brand", +/// attributeContent("orderInBrand") +/// ) +/// ) +/// ) +/// ) +/// +/// **The `referenceProperty` is implicit in requirement `referenceContent`** +/// In the `orderBy` clause within the requirement, +/// the `referenceProperty` constraint is implicit and must not be repeated. All attribute order constraints +/// in `referenceContent` automatically refer to the reference attributes, unless the or +/// container is used there. +/// The example is based on a simple one-to-zero-or-one reference (a product can have at most one reference to a brand +/// entity). The response will only return the products that have a reference to the "Sony" brand, all of which contain the +/// `orderInBrand` attribute (since it's marked as a non-nullable attribute). Because the example is so simple, the returned +/// result can be anticipated. +/// ## Behaviour of zero or one to many references ordering +/// The situation is more complicated when the reference is one-to-many. What is the expected result of a query that +/// involves ordering by a property on a reference attribute? Is it wise to allow such ordering query in this case? +/// We decided to allow it and bind it with the following rules: +/// ### Non-hierarchical entity +/// If the referenced entity is **non-hierarchical**, and the returned entity references multiple entities, only +/// the reference with the lowest primary key of the referenced entity, while also having the order property set, will be +/// used for ordering. +/// ### Hierarchical entity +/// If the referenced entity is **hierarchical** and the returned entity references multiple entities, the reference used +/// for ordering is the one that contains the order property and is the closest hierarchy node to the root of the filtered +/// hierarchy node. +/// It sounds complicated, but it's really quite simple. If you list products of a certain category and at the same time +/// order them by a property "priority" set on the reference to the category, the first products will be those directly +/// related to the category, ordered by "priority", followed by the products of the first child category, and so on, +/// maintaining the depth-first order of the category tree. +/// public class ReferenceProperty : AbstractOrderConstraintContainer { public string ReferenceName => (string) Arguments[0]!; @@ -18,4 +81,4 @@ public override IOrderConstraint GetCopyWithNewChildren(IOrderConstraint?[] chil { return new ReferenceProperty(ReferenceName, children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Query.cs b/EvitaDB.Client/Queries/Query.cs index 0b058d5..32a45c2 100644 --- a/EvitaDB.Client/Queries/Query.cs +++ b/EvitaDB.Client/Queries/Query.cs @@ -7,15 +7,31 @@ namespace EvitaDB.Client.Queries; +/// +/// Main transfer object for Evita Query Language. Contains all data and conditions that query what entities will +/// be queried, in what order and how rich the returned results will be. +/// evitaDB query language is composed of nested set of functions. Each function has its name and set of arguments inside +/// round brackets. Arguments and functions are delimited by a comma. Strings are enveloped inside apostrophes. This language +/// is expected to be used by human operators, on the code level query is represented by a query object tree, that can +/// be constructed directly without intermediate string language form. For the sake of documentation human readable form +/// is used here. +/// Query has these four parts: +/// +/// : contains collection (mandatory) specification +/// : contains constraints limiting entities being returned (optional, if missing all are returned) +/// : defines in what order will the entities return (optional, if missing entities are ordered by primary integer key in ascending order) +/// : contains additional information for the query engine, may hold pagination settings, richness of the entities and so on (optional, if missing only primary keys of all the entities are returned) +/// +/// public class Query { - public Collection? Entities { get; } + public Collection? Collection { get; } public FilterBy? FilterBy { get; } public OrderBy? OrderBy { get; } public Require? Require { get; } internal Query(Collection? header, FilterBy? filterBy, OrderBy? orderBy, Require? require) { - Entities = header; + Collection = header; FilterBy = filterBy; OrderBy = orderBy; Require = require; @@ -26,4 +42,4 @@ internal Query(Collection? header, FilterBy? filterBy, OrderBy? orderBy, Require public StringWithParameters ToStringWithParametersExtraction() => ToStringWithParameterExtraction(this); -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/AssociatedDataContent.cs b/EvitaDB.Client/Queries/Requires/AssociatedDataContent.cs index 294ec76..c1ceb5c 100644 --- a/EvitaDB.Client/Queries/Requires/AssociatedDataContent.cs +++ b/EvitaDB.Client/Queries/Requires/AssociatedDataContent.cs @@ -1,5 +1,19 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// This `associatedData` requirement changes default behaviour of the query engine returning only entity primary keys in +/// the result. When this requirement is used result contains entity bodies along with associated data with names +/// specified in one or more arguments of this requirement. +/// This requirement implicitly triggers requirement because attributes cannot be returned without entity. +/// Localized associated data is returned according to query. Requirement might be combined +/// with requirement. +/// Example: +/// +/// associatedData("description", "gallery-3d") +/// +/// public class AssociatedDataContent : AbstractRequireConstraintLeaf, IEntityContentRequire, IConstraintWithSuffix { public string[] AssociatedDataNames => Arguments.Select(obj => (string) obj!).ToArray(); @@ -21,4 +35,4 @@ public AssociatedDataContent(params string[] associatedDataNames) : base(associa public string? SuffixIfApplied => AllRequested ? Suffix : null; public bool ArgumentImplicitForSuffix(object argument) => false; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/AttributeContent.cs b/EvitaDB.Client/Queries/Requires/AttributeContent.cs index cae82d4..8e945bc 100644 --- a/EvitaDB.Client/Queries/Requires/AttributeContent.cs +++ b/EvitaDB.Client/Queries/Requires/AttributeContent.cs @@ -1,5 +1,21 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `attributeContent` requirement is used to retrieve one or more entity or reference attributes. Localized attributes +/// are only fetched if there is a locale context in the query, either by using the filter +/// constraint or the dataInLocales require constraint. +/// All entity attributes are fetched from disk in bulk, so specifying only a few of them in the `attributeContent` +/// requirement only reduces the amount of data transferred over the network. It's not bad to fetch all the attributes +/// of an entity using `attributeContentAll`. +/// Example: +/// +/// entityFetch( +/// attributeContent("code", "name") +/// ) +/// +/// public class AttributeContent : AbstractRequireConstraintLeaf, IEntityContentRequire, IConstraintWithSuffix { public static readonly AttributeContent AllAttributes = new(); @@ -28,4 +44,4 @@ public string[] GetAttributeNames() public string? SuffixIfApplied => AllRequested ? Suffix : null; public bool ArgumentImplicitForSuffix(object argument) => false; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/AttributeHistogram.cs b/EvitaDB.Client/Queries/Requires/AttributeHistogram.cs index a83512a..f1d4059 100644 --- a/EvitaDB.Client/Queries/Requires/AttributeHistogram.cs +++ b/EvitaDB.Client/Queries/Requires/AttributeHistogram.cs @@ -1,5 +1,18 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `attributeHistogram` can be computed from any filterable attribute whose type is numeric. The histogram is +/// computed only from the attributes of elements that match the current mandatory part of the filter. The interval +/// related constraints - i.e. and in the userFilter part are excluded for +/// the sake of histogram calculation. If this weren't the case, the user narrowing the filtered range based on +/// the histogram results would be driven into a narrower and narrower range and eventually into a dead end. +/// Example: +/// +/// attributeHistogram(5, "width", "height") +/// +/// public class AttributeHistogram : AbstractRequireConstraintLeaf, IExtraResultRequireConstraint { private AttributeHistogram(params object[] arguments) : base(arguments) @@ -15,4 +28,4 @@ public AttributeHistogram(int requestedBucketCount, params string[] attributeNam public string[] AttributeNames => Arguments.Skip(1).Select(obj => (string) obj!).ToArray(); public new bool Applicable => IsArgumentsNonNull() && Arguments.Length > 1; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/DataInLocales.cs b/EvitaDB.Client/Queries/Requires/DataInLocales.cs index 96acbe2..7e5a33f 100644 --- a/EvitaDB.Client/Queries/Requires/DataInLocales.cs +++ b/EvitaDB.Client/Queries/Requires/DataInLocales.cs @@ -1,7 +1,27 @@ using System.Globalization; +using EvitaDB.Client.Queries.Filter; namespace EvitaDB.Client.Queries.Requires; +/// +/// This `dataInLocales` query is require query that accepts zero or more arguments. When this +/// require query is used, result contains [entity attributes and associated data](../model/entity_model.md) +/// localized in required languages as well as global ones. If query contains no argument, global data and data +/// localized to all languages are returned. If query is not present in the query, only global attributes and +/// associated data are returned. +/// **Note:** if is used in the filter part of the query and `dataInLanguage` +/// require query is missing, the system implicitly uses `dataInLanguage` matching the language in filter query. +/// Only single `dataInLanguage` query can be used in the query. +/// Example that fetches only global and `en-US` localized attributes and associated data (considering there are multiple +/// language localizations): +/// +/// dataInLocales("en-US") +/// +/// Example that fetches all available global and localized data: +/// +/// dataInLocalesAll() +/// +/// public class DataInLocales : AbstractRequireConstraintLeaf, IEntityContentRequire, IConstraintWithSuffix { public CultureInfo?[] Locales => Arguments.Select(obj => (CultureInfo?) obj).ToArray(); @@ -18,4 +38,4 @@ public DataInLocales(params CultureInfo[] infos) : base(infos) } public string? SuffixIfApplied => AllRequested ? SuffixAll : null; public bool ArgumentImplicitForSuffix(object argument) => false; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/EmptyHierarchicalEntityBehaviour.cs b/EvitaDB.Client/Queries/Requires/EmptyHierarchicalEntityBehaviour.cs index 3cb9493..5207abf 100644 --- a/EvitaDB.Client/Queries/Requires/EmptyHierarchicalEntityBehaviour.cs +++ b/EvitaDB.Client/Queries/Requires/EmptyHierarchicalEntityBehaviour.cs @@ -1,7 +1,11 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The enumeration controls behaviour whether the hierarchical nodes that are not referred +/// by any of the queried entities should be part of the result hierarchy statistics tree. +/// public enum EmptyHierarchicalEntityBehaviour { LeaveEmpty, RemoveEmpty -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/EntityFetch.cs b/EvitaDB.Client/Queries/Requires/EntityFetch.cs index 5e457e0..52d7165 100644 --- a/EvitaDB.Client/Queries/Requires/EntityFetch.cs +++ b/EvitaDB.Client/Queries/Requires/EntityFetch.cs @@ -1,5 +1,33 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `entityFetch` requirement is used to trigger loading one or more entity data containers from the disk by its +/// primary key. This operation requires a disk access unless the entity is already loaded in the database cache +/// (frequently fetched entities have higher chance to stay in the cache). +/// Example: +/// +/// query( +/// collection("Brand"), +/// filterBy( +/// entityPrimaryKeyInSet(64703), +/// entityLocaleEquals("en") +/// ), +/// require( +/// entityFetch( +/// attributeContent("code", "name") +/// ) +/// ) +/// ) +/// +/// See internal contents available for fetching in : +/// + /// + /// + /// + /// + /// +/// +/// public class EntityFetch : AbstractRequireConstraintContainer, IEntityFetchRequire { protected EntityFetch(IRequireConstraint?[] requireConstraints) : base(requireConstraints) @@ -22,4 +50,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] public IEntityContentRequire?[] Requirements => Children.Select(x=>x as IEntityContentRequire).ToArray(); public new bool Necessary => true; public new bool Applicable => true; -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/EntityGroupFetch.cs b/EvitaDB.Client/Queries/Requires/EntityGroupFetch.cs index 4d98c7e..d09190f 100644 --- a/EvitaDB.Client/Queries/Requires/EntityGroupFetch.cs +++ b/EvitaDB.Client/Queries/Requires/EntityGroupFetch.cs @@ -1,5 +1,38 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `entityGroupFetch` requirement is similar to but is used to trigger loading one or more +/// referenced group entities in the parent. +/// +/// Example: +/// +/// query( +/// collection("Brand"), +/// filterBy( +/// entityPrimaryKeyInSet(64703), +/// entityLocaleEquals("en") +/// ), +/// require( +/// entityFetch( +/// referenceContent( +/// "parameterValues", +/// entityGroupFetch( +/// attributeContent("code", "name") +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// See internal contents available for fetching in : +/// +/// +/// +/// +/// +/// +/// +/// public class EntityGroupFetch : AbstractRequireConstraintContainer, IEntityFetchRequire { private EntityGroupFetch(IRequireConstraint?[] requireConstraints) : base(requireConstraints) @@ -24,4 +57,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new EntityGroupFetch(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/FacetGroupsConjunction.cs b/EvitaDB.Client/Queries/Requires/FacetGroupsConjunction.cs index b79f4fe..c4224d7 100644 --- a/EvitaDB.Client/Queries/Requires/FacetGroupsConjunction.cs +++ b/EvitaDB.Client/Queries/Requires/FacetGroupsConjunction.cs @@ -3,6 +3,46 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// This `facetGroupsConjunction` require allows specifying inter-facet relation inside facet groups of certain primary ids. +/// First mandatory argument specifies entity type of the facet group, secondary argument allows to define one more facet +/// group ids which inner facets should be considered conjunctive. +/// This require constraint changes default behaviour stating that all facets inside same facet group are combined by OR +/// relation (eg. disjunction). Constraint has sense only when [facet](#facet) constraint is part of the query. +/// Example: +/// +/// query( +/// entities("product"), +/// filterBy( +/// userFilter( +/// facet("group", 1, 2), +/// facet( +/// "parameterType", +/// entityPrimaryKeyInSet(11, 12, 22) +/// ) +/// ) +/// ), +/// require( +/// facetGroupsConjunction("parameterType", 1, 8, 15) +/// ) +/// ) +/// +/// This statement means, that facets in `parameterType` groups `1`, `8`, `15` will be joined with boolean AND relation when +/// selected. +/// Let's have this facet/group situation: +/// Color `parameterType` (group id: 1): +/// - blue (facet id: 11) +/// - red (facet id: 12) +/// Size `parameterType` (group id: 2): +/// - small (facet id: 21) +/// - large (facet id: 22) +/// Flags `tag` (group id: 3): +/// - action products (facet id: 31) +/// - new products (facet id: 32) +/// When user selects facets: blue (11), red (12) by default relation would be: get all entities that have facet blue(11) OR +/// facet red(12). If require `facetGroupsConjunction('parameterType', 1)` is passed in the query filtering condition will +/// be composed as: blue(11) AND red(12) +/// public class FacetGroupsConjunction : AbstractRequireConstraintContainer { public string ReferenceName => (string) Arguments[0]!; diff --git a/EvitaDB.Client/Queries/Requires/FacetGroupsDisjunction.cs b/EvitaDB.Client/Queries/Requires/FacetGroupsDisjunction.cs index c591746..0e97ce3 100644 --- a/EvitaDB.Client/Queries/Requires/FacetGroupsDisjunction.cs +++ b/EvitaDB.Client/Queries/Requires/FacetGroupsDisjunction.cs @@ -3,6 +3,46 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// This `facetGroupsDisjunction` require constraint allows specifying facet relation among different facet groups of certain +/// primary ids. First mandatory argument specifies entity type of the facet group, secondary argument allows to define one +/// more facet group ids that should be considered disjunctive. +/// This require constraint changes default behaviour stating that facets between two different facet groups are combined by +/// AND relation and changes it to the disjunction relation instead. +/// Example: +/// +/// query( +/// entities("product"), +/// filterBy( +/// userFilter( +/// facet("group", 1, 2), +/// facet( +/// "parameterType", +/// entityPrimaryKeyInSet(11, 12, 22) +/// ) +/// ) +/// ), +/// require( +/// facetGroupsDisjunction("parameterType", 1, 2) +/// ) +/// ) +/// +/// This statement means, that facets in `parameterType` facet groups `1`, `2` will be joined with the rest of the query by +/// boolean OR relation when selected. +/// Let's have this facet/group situation: +/// Color `parameterType` (group id: 1): +/// - blue (facet id: 11) +/// - red (facet id: 12) +/// Size `parameterType` (group id: 2): +/// - small (facet id: 21) +/// - large (facet id: 22) +/// Flags `tag` (group id: 3): +/// - action products (facet id: 31) +/// - new products (facet id: 32) +/// When user selects facets: blue (11), large (22), new products (31) - the default meaning would be: get all entities that +/// have facet blue as well as facet large and action products tag (AND). If require `facetGroupsDisjunction('tag', 3)` +/// is passed in the query, filtering condition will be composed as: (`blue(11)` AND `large(22)`) OR `new products(31)` +/// public class FacetGroupsDisjunction : AbstractRequireConstraintContainer { public string ReferenceName => (string) Arguments[0]!; diff --git a/EvitaDB.Client/Queries/Requires/FacetGroupsNegation.cs b/EvitaDB.Client/Queries/Requires/FacetGroupsNegation.cs index 2f97d58..01208ac 100644 --- a/EvitaDB.Client/Queries/Requires/FacetGroupsNegation.cs +++ b/EvitaDB.Client/Queries/Requires/FacetGroupsNegation.cs @@ -4,6 +4,36 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `facetGroupsNegation` changes the behavior of the facet option in all facet groups specified in the filterBy +/// constraint. Instead of returning only those items that have a reference to that particular faceted entity, the query +/// result will return only those items that don't have a reference to it. +/// Example: +/// +/// query( +/// collection("Product"), +/// require( +/// facetSummaryOfReference( +/// "parameterValues", +/// IMPACT, +/// filterBy(attributeContains("code", "4")), +/// filterGroupBy(attributeInSet("code", "ram-memory", "rom-memory")), +/// entityFetch(attributeContent("code")), +/// entityGroupFetch(attributeContent("code")) +/// ), +/// facetGroupsNegation( +/// "parameterValues", +/// filterBy( +/// attributeInSet("code", "ram-memory") +/// ) +/// ) +/// ) +/// ) +/// +/// The predicted results in the negated groups are far greater than the numbers produced by the default behavior. +/// Selecting any option in the RAM facet group predicts returning thousands of results, while the ROM facet group with +/// default behavior predicts only a dozen of them. +/// public class FacetGroupsNegation : AbstractRequireConstraintContainer { public string ReferenceName => (string) Arguments[0]!; diff --git a/EvitaDB.Client/Queries/Requires/FacetStatisticsDepth.cs b/EvitaDB.Client/Queries/Requires/FacetStatisticsDepth.cs index 710d928..d12c0fb 100644 --- a/EvitaDB.Client/Queries/Requires/FacetStatisticsDepth.cs +++ b/EvitaDB.Client/Queries/Requires/FacetStatisticsDepth.cs @@ -1,7 +1,17 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// This enum controls whether should contain only basic statistics about facets - e.g. count only, +/// or whether the selection impact should be computed as well. +/// public enum FacetStatisticsDepth { + /// + /// Only counts of facets will be computed. + /// Counts, + /// + /// Counts and selection impact for non-selected facets will be computed. + /// Impact -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/FacetSummary.cs b/EvitaDB.Client/Queries/Requires/FacetSummary.cs index e42128a..b48b561 100644 --- a/EvitaDB.Client/Queries/Requires/FacetSummary.cs +++ b/EvitaDB.Client/Queries/Requires/FacetSummary.cs @@ -4,6 +4,79 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `facetSummary` request triggers the calculation of the FacetSummary containing the facet summary calculation. +/// The calculated facet summary will contain all entity references marked as faceted in the entity schema. The facet +/// summary can be further modified by the facet summary of reference constraint, which allows you to override +/// the general facet summary behavior specified in the generic facet summary require constraint. +/// The faceted property affects the size of the indexes kept in memory and the scale / complexity of the general facet +/// summary (i.e. the summary generated by the facetSummary request). It is recommended to mark only the references used +/// for faceted filtering as faceted to keep the indexes small and the calculation of the facet summary in the user +/// interface fast and simple. The combinatorial complexity of the facet summary is quite high for large datasets, and +/// you may be forced to optimize it by narrowing the summary using the filtering facility or selecting only a few +/// references for the summary. +/// ## Facet calculation rules +/// 1. The facet summary is calculated only for entities that are returned in the current query result. +/// 2. The calculation respects any filter constraints placed outside the 'userFilter' container. +/// 3. The default relation between facets within a group is logical disjunction (logical OR). +/// 4. The default relation between facets in different groups / references is a logical AND. +/// The `facetSummary` requirement triggers the calculation of the extra result. The facet summary +/// is always computed as a side result of the main entity query and respects any filtering constraints placed on the +/// queried entities. +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "e-readers") +/// ) +/// entityLocaleEquals("en") +/// ), +/// require( +/// facetSummary( +/// COUNTS, +/// entityFetch( +/// attributeContent("name") +/// ), +/// entityGroupFetch( +/// attributeContent("name") +/// ) +/// ) +/// ) +/// ) +/// +/// +/// +/// +/// Filtering facet summary: +/// The facet summary sometimes gets very big, and besides the fact that it is not very useful to show all facet options +/// in the user interface, it also takes a lot of time to calculate it. To limit the facet summary, you can use the +/// and (which is the same as filterBy, but it filters the entire facet group +/// instead of individual facets) constraints. +/// If you add the filtering constraints to the facetSummary requirement, you can only refer to filterable properties +/// that are shared by all referenced entities. This may not be feasible in some cases, and you will need to split +/// the generic facetSummary requirement into multiple individual requirements with +/// specific filters for each reference type. +/// The filter conditions can only target properties on the target entity and cannot target reference attributes in +/// the source entity that are specific to a relationship with the target entity. +/// +/// +/// Ordering facet summary: +/// Typically, the facet summary is ordered in some way to present the most relevant facet options first. The same is +/// true for ordering facet groups. To sort the facet summary items the way you like, you can use the and +/// (which is the same as orderBy but it sorts the facet groups instead of the individual facets) +/// constraints. +/// If you add the ordering constraints to the facetSummary requirement, you can only refer to sortable properties that +/// are shared by all referenced entities. This may not be feasible in some cases, and you will need to split the generic +/// facetSummary requirement into multiple individual facetSummaryOfReference requirements with specific ordering +/// constraints for each reference type. +/// The ordering constraints can only target properties on the target entity and cannot target reference attributes in +/// the source entity that are specific to a relationship with the target entity. +/// +/// +/// public class FacetSummary : AbstractRequireConstraintContainer, IExtraResultRequireConstraint, ISeparateEntityContentRequireContainer { public FacetStatisticsDepth FacetStatisticsDepth => (FacetStatisticsDepth) Arguments[0]!; @@ -76,4 +149,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new FacetSummary(Arguments, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/FacetSummaryOfReference.cs b/EvitaDB.Client/Queries/Requires/FacetSummaryOfReference.cs index b0ab3c5..f9c8cde 100644 --- a/EvitaDB.Client/Queries/Requires/FacetSummaryOfReference.cs +++ b/EvitaDB.Client/Queries/Requires/FacetSummaryOfReference.cs @@ -4,6 +4,76 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `facetSummaryOfReference` requirement triggers the calculation of the for a specific +/// reference. When a generic requirement is specified, this require constraint overrides +/// the default constraints from the generic requirement to constraints specific to this particular reference. +/// By combining the generic facetSummary and facetSummaryOfReference, you define common requirements for the facet +/// summary calculation, and redefine them only for references where they are insufficient. +/// The `facetSummaryOfReference` requirements redefine all constraints from the generic facetSummary requirement. +/// +/// Facet calculation rules +/// 1. The facet summary is calculated only for entities that are returned in the current query result. +/// 2. The calculation respects any filter constraints placed outside the 'userFilter' container. +/// 3. The default relation between facets within a group is logical disjunction (logical OR). +/// 4. The default relation between facets in different groups / references is a logical AND. +/// The `facetSummary` requirement triggers the calculation of the extra result. The facet summary +/// is always computed as a side result of the main entity query and respects any filtering constraints placed on the +/// queried entities. +/// Example: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "e-readers") +/// ) +/// entityLocaleEquals("en") +/// ), +/// require( +/// facetSummary( +/// COUNTS, +/// entityFetch( +/// attributeContent("name") +/// ), +/// entityGroupFetch( +/// attributeContent("name") +/// ) +/// ) +/// ) +/// ) +/// +/// +/// +/// +/// Filtering facet summary: +/// The facet summary sometimes gets very big, and besides the fact that it is not very useful to show all facet options +/// in the user interface, it also takes a lot of time to calculate it. To limit the facet summary, you can use the +/// and (which is the same as filterBy, but it filters the entire facet group +/// instead of individual facets) constraints. +/// If you add the filtering constraints to the facetSummary requirement, you can only refer to filterable properties +/// that are shared by all referenced entities. This may not be feasible in some cases, and you will need to split +/// the generic facetSummary requirement into multiple individual requirements with +/// specific filters for each reference type. +/// The filter conditions can only target properties on the target entity and cannot target reference attributes in +/// the source entity that are specific to a relationship with the target entity. +/// +/// +/// Ordering facet summary: +/// Typically, the facet summary is ordered in some way to present the most relevant facet options first. The same is +/// true for ordering facet groups. To sort the facet summary items the way you like, you can use the and +/// (which is the same as orderBy but it sorts the facet groups instead of the individual facets) +/// constraints. +/// If you add the ordering constraints to the facetSummary requirement, you can only refer to sortable properties that +/// are shared by all referenced entities. This may not be feasible in some cases, and you will need to split the generic +/// facetSummary requirement into multiple individual facetSummaryOfReference requirements with specific ordering +/// constraints for each reference type. +/// The ordering constraints can only target properties on the target entity and cannot target reference attributes in +/// the source entity that are specific to a relationship with the target entity. +/// +/// +/// public class FacetSummaryOfReference : AbstractRequireConstraintContainer, ISeparateEntityContentRequireContainer, IExtraResultRequireConstraint { @@ -78,4 +148,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new FacetSummaryOfReference(Arguments, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyChildren.cs b/EvitaDB.Client/Queries/Requires/HierarchyChildren.cs index 9d7a614..299d286 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyChildren.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyChildren.cs @@ -1,7 +1,60 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Requires; +/// +/// The children requirement computes the hierarchy tree starting at the same hierarchy node that is targeted by +/// the filtering part of the same query using the or constraints. +/// The scope of the calculated information can be controlled by the stopAt constraint. By default, the traversal goes +/// all the way to the bottom of the hierarchy tree unless you tell it to stop at anywhere. If you need to access +/// statistical data, use the statistics constraint. +/// The constraint accepts following arguments: +/// - mandatory String argument specifying the output name for the calculated data structure +/// - optional one or more constraints that allow you to define the completeness of the hierarchy entities, the scope of +/// the traversed hierarchy tree, and the statistics computed along the way; any or all of the constraints may be +/// present: +/// +/// +/// +/// +/// +/// The following query lists products in category Audio and its subcategories. Along with the products returned, it also +/// returns a computed subcategories data structure that lists the flat category list the currently focused category +/// Audio with a computed count of child categories for each menu item and an aggregated count of all products that would +/// fall into the given category. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// children( +/// "subcategories", +/// entityFetch(attributeContent("code")), +/// stopAt(distance(1)), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// The calculated result for children is connected with the pivot hierarchy node (or +/// the "virtual" invisible top root referred to by the hierarchyWithinRoot constraint). If the +/// contains inner constraints or , the children will respect them as +/// well. The reason is simple: when you render a menu for the query result, you want the calculated statistics to +/// respect the rules that apply to the hierarchyWithin so that the calculated number remains consistent for the end +/// user. +/// public class HierarchyChildren : AbstractRequireConstraintContainer, IHierarchyRequireConstraint { private const string ConstraintName = "children"; @@ -39,4 +92,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new HierarchyChildren(OutputName, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyContent.cs b/EvitaDB.Client/Queries/Requires/HierarchyContent.cs index 1471756..8cc7f6b 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyContent.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyContent.cs @@ -2,6 +2,25 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `hierarchyContent` requirement allows you to access the information about the hierarchical placement of +/// the entity. +/// If no additional constraints are specified, entity will contain a full chain of parent primary keys up to the root +/// of a hierarchy tree. You can limit the size of the chain by using a stopAt constraint - for example, if you're only +/// interested in a direct parent of each entity returned, you can use a stopAt(distance(1)) constraint. The result is +/// similar to using a parents constraint, but is limited in that it doesn't provide information about statistics and +/// the ability to list siblings of the entity parents. On the other hand, it's easier to use - since the hierarchy +/// placement is directly available in the retrieved entity object. +/// If you provide a nested entityFetch constraint, the hierarchy information will contain the bodies of the parent +/// entities in the required width. The attributeContent inside the entityFetch allows you to access the attributes +/// of the parent entities, etc. +/// Example: +/// +/// entityFetch( +/// hierarchyContent() +/// ) +/// +/// public class HierarchyContent : AbstractRequireConstraintContainer, ISeparateEntityContentRequireContainer, IEntityContentRequire { public HierarchyStopAt? StopAt => Children.FirstOrDefault(x => x is HierarchyStopAt) as HierarchyStopAt; @@ -35,4 +54,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] Assert.IsTrue(additionalChildren.Length == 0, "Additional children are not supported for HierarchyContent!"); return new HierarchyContent(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyDistance.cs b/EvitaDB.Client/Queries/Requires/HierarchyDistance.cs index 1d7c1d4..ba57295 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyDistance.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyDistance.cs @@ -3,6 +3,41 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The distance constraint can only be used within the container and limits the hierarchy +/// traversal to stop when the number of levels traversed reaches the specified constant. The distance is always relative +/// to the pivot node (the node where the hierarchy traversal starts) and is the same whether we are traversing +/// the hierarchy top-down or bottom-up. The distance between any two nodes in the hierarchy can be calculated as +/// `abs(level(nodeA) - level(nodeB))`. +/// The constraint accepts single integer argument `distance`, which defines a maximum relative distance from the pivot +/// node that can be traversed; the pivot node itself is at distance zero, its direct child or direct parent is +/// at distance one, each additional step adds a one to the distance. +/// See the following figure when the pivot node is Audio: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// children( +/// "subcategories", +/// entityFetch(attributeContent("code")), +/// stopAt(distance(1)) +/// ) +/// ) +/// ) +/// ) +/// +/// The following query lists products in category Audio and its subcategories. Along with the products returned, it +/// also returns a computed subcategories data structure that lists the flat category list the currently focused category +/// Audio. +/// public class HierarchyDistance : AbstractRequireConstraintLeaf, IHierarchyStopAtRequireConstraint { private const string ConstraintName = "distance"; @@ -18,4 +53,4 @@ public HierarchyDistance(int distance) : base(ConstraintName, distance) { Assert.IsTrue(distance > 0, () => new EvitaInvalidUsageException("Distance must be greater than zero.")); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyFromNode.cs b/EvitaDB.Client/Queries/Requires/HierarchyFromNode.cs index 00e752d..bb97443 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyFromNode.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyFromNode.cs @@ -1,7 +1,83 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Requires; +/// +/// The `fromNode` requirement computes the hierarchy tree starting from the pivot node of the hierarchy, that is +/// identified by the node inner constraint. The fromNode calculates the result regardless of the potential use of +/// the constraint in the filtering part of the query. The scope of the calculated information +/// can be controlled by the constraint. By default, the traversal goes all the way to the bottom +/// of the hierarchy tree unless you tell it to stop at anywhere. Calculated data is not affected by +/// the filter constraint - the query can filter entities using from +/// category Accessories, while still allowing you to correctly compute menu at different node defined in a `fromNode` +/// requirement. If you need to access statistical data, use statistics constraint. +/// The constraint accepts following arguments: +/// - mandatory String argument specifying the output name for the calculated data structure +/// - mandatory require constraint node that must match exactly one pivot hierarchical entity that represents the root +/// node of the traversed hierarchy subtree. +/// - optional one or more constraints that allow you to define the completeness of the hierarchy entities, the scope +/// of the traversed hierarchy tree, and the statistics computed along the way; any or all of the constraints may be +/// present: +/// +/// +/// +/// +/// +/// The following query lists products in category Audio and its subcategories. Along with the products returned, it +/// also returns a computed sideMenu1 and sideMenu2 data structure that lists the flat category list for the categories +/// Portables and Laptops with a computed count of child categories for each menu item and an aggregated count of all +/// products that would fall into the given category. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// fromNode( +/// "sideMenu1", +/// node( +/// filterBy( +/// attributeEquals("code", "portables") +/// ) +/// ), +/// entityFetch(attributeContent("code")), +/// stopAt(distance(1)), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ), +/// fromNode( +/// "sideMenu2", +/// node( +/// filterBy( +/// attributeEquals("code", "laptops") +/// ) +/// ), +/// entityFetch(attributeContent("code")), +/// stopAt(distance(1)), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// The calculated result for `fromNode` is not affected by the pivot hierarchy node. +/// If the contains inner constraints or , +/// the `fromNode` respects them. The reason is simple: when you render a menu for the query result, you want +/// the calculated statistics to respect the rules that apply to the hierarchyWithin so that the calculated number +/// remains consistent for the end user. +/// public class HierarchyFromNode : AbstractRequireConstraintContainer, IHierarchyRequireConstraint { private const string ConstraintName = "fromNode"; @@ -62,4 +138,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new HierarchyFromNode(OutputName, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyFromRoot.cs b/EvitaDB.Client/Queries/Requires/HierarchyFromRoot.cs index 58dfab9..62482e6 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyFromRoot.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyFromRoot.cs @@ -1,7 +1,64 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Requires; +/// +/// The `fromRoot` requirement computes the hierarchy tree starting from the "virtual" invisible top root of +/// the hierarchy, regardless of the potential use of the constraint in the filtering part of +/// the query. The scope of the calculated information can be controlled by the stopAt constraint. By default, +/// the traversal goes all the way to the bottom of the hierarchy tree unless you tell it to stop at anywhere. +/// If you need to access statistical data, use statistics constraint. Calculated data is not affected by +/// the filter constraint - the query can filter entities using from +/// category Accessories, while still allowing you to correctly compute menu at root level. +/// Please keep in mind that the full statistic calculation can be particularly expensive in the case of the fromRoot +/// requirement - it usually requires aggregation for the entire queried dataset (see more information about +/// the calculation). +/// The constraint accepts following arguments: +/// - mandatory String argument specifying the output name for the calculated data structure +/// - optional one or more constraints that allow you to define the completeness of the hierarchy entities, the scope of +/// the traversed hierarchy tree, and the statistics computed along the way; any or all of the constraints may be +/// present: +/// +/// +/// +/// +/// +/// The following query lists products in category Audio and its subcategories. Along with the returned products, it also +/// requires a computed megaMenu data structure that lists the top 2 levels of the Category hierarchy tree with +/// a computed count of child categories for each menu item and an aggregated count of all filtered products that would +/// fall into the given category. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// fromRoot( +/// "megaMenu", +/// entityFetch(attributeContent("code")), +/// stopAt(level(2)), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// The calculated result for `fromRoot` is not affected by the pivot hierarchy node. +/// If the contains inner constraints or , +/// the `fromRoot` respects them. The reason is simple: when you render a menu for the query result, you want +/// the calculated statistics to respect the rules that apply to the so that the calculated +/// number remains consistent for the end user. +/// public class HierarchyFromRoot : AbstractRequireConstraintContainer, IHierarchyRequireConstraint { private const string ConstraintName = "fromRoot"; @@ -42,4 +99,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new HierarchyFromRoot(OutputName, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyLevel.cs b/EvitaDB.Client/Queries/Requires/HierarchyLevel.cs index 1cf6aa4..0741208 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyLevel.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyLevel.cs @@ -3,6 +3,35 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The level constraint can only be used within the stopAt container and limits the hierarchy traversal to stop when +/// the actual level of the traversed node is equal to a specified constant. The "virtual" top invisible node has level +/// zero, the top nodes (nodes with NULL parent) have level one, their children have level two, and so on. +/// See the following figure: +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// fromRoot( +/// "megaMenu", +/// entityFetch(attributeContent("code")), +/// stopAt(level(2)) +/// ) +/// ) +/// ) +/// ) +/// +/// The query lists products in Audio category and its subcategories. Along with the products returned, it +/// also returns a computed megaMenu data structure that lists top two levels of the entire hierarchy. +/// public class HierarchyLevel : AbstractRequireConstraintLeaf, IHierarchyStopAtRequireConstraint { private const string ConstraintName = "level"; @@ -18,4 +47,4 @@ public HierarchyLevel(int level) : base(ConstraintName, level) { Assert.IsTrue(level > 0, () => new EvitaInvalidUsageException("Level must be greater than zero. Level 1 represents root node.")); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyNode.cs b/EvitaDB.Client/Queries/Requires/HierarchyNode.cs index c953505..b02ce0e 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyNode.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyNode.cs @@ -3,6 +3,44 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The node filtering container is an alternative to the and +/// termination constraints, which is much more dynamic and can produce hierarchy trees of non-uniform depth. Because +/// the filtering constraint can be satisfied by nodes of widely varying depths, traversal can be highly dynamic. +/// Constraint children define a criterion that determines the point in a hierarchical structure where the traversal +/// should stop. The traversal stops at the first node that satisfies the filter condition specified in this container. +/// The situations where you'd need this dynamic behavior are few and far between. Unfortunately, we do not have +/// a meaningful example of this in the demo dataset, so our example query will be slightly off. But for the sake of +/// demonstration, let's list the entire Accessories hierarchy, but stop traversing at the nodes whose code starts with +/// the letter `w`. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "accessories") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// children( +/// "subMenu", +/// entityFetch(attributeContent("code")), +/// stopAt( +/// node( +/// filterBy( +/// attributeStartsWith("code", "w") +/// ) +/// ) +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// public class HierarchyNode : AbstractRequireConstraintContainer, IHierarchyStopAtRequireConstraint { private const string ConstraintName = "node"; @@ -24,4 +62,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] } return new HierarchyNode((FilterBy) additionalChildren[0]!); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyOfReference.cs b/EvitaDB.Client/Queries/Requires/HierarchyOfReference.cs index 15d8156..dd198e5 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyOfReference.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyOfReference.cs @@ -4,6 +4,37 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The requirement triggers the calculation of the Hierarchy data structure for the hierarchies of the referenced entity +/// type. +/// The hierarchy of reference can still be combined with if the queried entity is a hierarchical +/// entity that is also connected to another hierarchical entity. Such situations are rather sporadic in reality. +/// The `hierarchyOfReference` can be repeated multiple times in a single query if you need different calculation +/// settings for different reference types. +/// The constraint accepts following arguments: +/// - specification of one or more reference names that identify the reference to the target hierarchical entity for +/// which the menu calculation should be performed; usually only one reference name makes sense, but to adapt +/// the constraint to the behavior of other similar constraints, evitaQL accepts multiple reference names for the case +/// that the same requirements apply to different references of the queried entity. +/// - optional argument of type EmptyHierarchicalEntityBehaviour enum allowing you to specify whether or not to return +/// empty hierarchical entities (e.g., those that do not have any queried entities that satisfy the current query +/// filter constraint assigned to them - either directly or transitively): +/// - : empty hierarchical nodes will remain in computed data +/// structures +/// - : empty hierarchical nodes are omitted from computed data +/// structures (default behavior) +/// - optional ordering constraint that allows you to specify an order of Hierarchy LevelInfo elements in the result +/// hierarchy data structure +/// - mandatory one or more constraints allowing you to instruct evitaDB to calculate menu components; one or all of +/// the constraints may be present: +/// +/// +/// +/// +/// +/// +/// +/// public class HierarchyOfReference : AbstractRequireConstraintContainer, IRootHierarchyConstraint, ISeparateEntityContentRequireContainer, IExtraResultRequireConstraint { @@ -90,4 +121,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new HierarchyOfReference(Arguments, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyOfSelf.cs b/EvitaDB.Client/Queries/Requires/HierarchyOfSelf.cs index d68b2d9..e462082 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyOfSelf.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyOfSelf.cs @@ -3,6 +3,34 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The requirement triggers the calculation of the Hierarchy data structure for the hierarchy of which it is a part. +/// The hierarchy of self can still be combined with if the queried entity is a hierarchical +/// entity that is also connected to another hierarchical entity. Such situations are rather sporadic in reality. +/// The constraint accepts following arguments: +/// - specification of one or more reference names that identify the reference to the target hierarchical entity for +/// which the menu calculation should be performed; usually only one reference name makes sense, but to adapt +/// the constraint to the behavior of other similar constraints, evitaQL accepts multiple reference names for the case +/// that the same requirements apply to different references of the queried entity. +/// - optional argument of type EmptyHierarchicalEntityBehaviour enum allowing you to specify whether or not to return +/// empty hierarchical entities (e.g., those that do not have any queried entities that satisfy the current query +/// filter constraint assigned to them - either directly or transitively): +/// - : empty hierarchical nodes will remain in computed data +/// structures +/// - : empty hierarchical nodes are omitted from computed data +/// structures +/// - optional ordering constraint that allows you to specify an order of Hierarchy LevelInfo elements in the result +/// hierarchy data structure +/// - mandatory one or more constraints allowing you to instruct evitaDB to calculate menu components; one or all of +/// the constraints may be present: +/// +/// +/// +/// +/// +/// +/// +/// public class HierarchyOfSelf : AbstractRequireConstraintContainer, IRootHierarchyConstraint, ISeparateEntityContentRequireContainer, IExtraResultRequireConstraint { @@ -42,4 +70,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new HierarchyOfSelf(children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyParents.cs b/EvitaDB.Client/Queries/Requires/HierarchyParents.cs index d042fdd..f3d4d63 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyParents.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyParents.cs @@ -1,7 +1,59 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Requires; +/// +/// The parents requirement computes the hierarchy tree starting at the same hierarchy node that is targeted by +/// the filtering part of the same query using the hierarchyWithin constraint towards the root of the hierarchy. +/// The scope of the calculated information can be controlled by the stopAt constraint. By default, the traversal goes +/// all the way to the top of the hierarchy tree unless you tell it to stop at anywhere. If you need to access +/// statistical data, use the statistics constraint. +/// The constraint accepts following arguments: +/// - mandatory String argument specifying the output name for the calculated data structure +/// - optional one or more constraints that allow you to define the completeness of the hierarchy entities, the scope +/// of the traversed hierarchy tree, and the statistics computed along the way; any or all of the constraints may be +/// present: +/// +/// +/// +/// +/// +/// +/// The following query lists products in the category Audio and its subcategories. Along with the products returned, +/// it also returns a computed parentAxis data structure that lists all the parent nodes of the currently focused +/// category True wireless with a computed count of child categories for each menu item and an aggregated count of all +/// products that would fall into the given category. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "true-wireless") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// parents( +/// "parentAxis", +/// entityFetch(attributeContent("code")), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// The calculated result for parents is connected with the pivot hierarchy node. +/// If the contains inner constraints or , +/// the parents will respect them as well during child nodes / queried entities statistics calculation. The reason is +/// simple: when you render a menu for the query result, you want the calculated statistics to respect the rules that +/// apply to the so that the calculated number remains consistent for the end user. +/// public class HierarchyParents : AbstractRequireConstraintContainer, IHierarchyRequireConstraint { private const string ConstraintName = "parents"; @@ -56,4 +108,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] "Inner constraints of different type than `require` are not expected."); return new HierarchyParents(OutputName, children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchySiblings.cs b/EvitaDB.Client/Queries/Requires/HierarchySiblings.cs index 8265341..bdbc067 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchySiblings.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchySiblings.cs @@ -1,7 +1,68 @@ -using EvitaDB.Client.Utils; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Utils; namespace EvitaDB.Client.Queries.Requires; +/// +/// The siblings requirement computes the hierarchy tree starting at the same hierarchy node that is targeted by +/// the filtering part of the same query using the hierarchyWithin. It lists all sibling nodes to the node that is +/// requested by hierarchyWithin constraint (that's why the siblings has no sense with +/// constraint - "virtual" top level node cannot have any siblings). Siblings will produce a flat list of siblings unless +/// the constraint is used as an inner constraint. The constraint +/// triggers a top-down hierarchy traversal from each of the sibling nodes until the is +/// satisfied. If you need to access statistical data, use the statistics constraint. +/// The constraint accepts following arguments: +/// - mandatory String argument specifying the output name for the calculated data structure +/// - optional one or more constraints that allow you to define the completeness of the hierarchy entities, the scope +/// of the traversed hierarchy tree, and the statistics computed along the way; any or all of the constraints may +/// be present: +/// +/// +/// +/// +/// +/// The following query lists products in category Audio and its subcategories. Along with the products returned, it also +/// returns a computed audioSiblings data structure that lists the flat category list the currently focused category +/// Audio with a computed count of child categories for each menu item and an aggregated count of all products that would +/// fall into the given category. +/// +/// query( +/// collection("Product"), +/// filterBy( +/// hierarchyWithin( +/// "categories", +/// attributeEquals("code", "audio") +/// ) +/// ), +/// require( +/// hierarchyOfReference( +/// "categories", +/// siblings( +/// "audioSiblings", +/// entityFetch(attributeContent("code")), +/// statistics( +/// CHILDREN_COUNT, +/// QUERIED_ENTITY_COUNT +/// ) +/// ) +/// ) +/// ) +/// ) +/// +/// The calculated result for siblings is connected with the pivot hierarchy node. If +/// the contains inner constraints or , +/// the children will respect them as well. The reason is simple: when you render a menu for the query result, you want +/// the calculated statistics to respect the rules that apply to the hierarchyWithin so that the calculated number +/// remains consistent for the end user. +/// +/// Different siblings syntax when used within parents parent constraint +/// +/// The siblings constraint can be used separately as a child of or , +/// or it can be used as a child constraint of . In such a case, the siblings constraint lacks +/// the first string argument that defines the name for the output data structure. The reason is that this name is +/// already defined on the enclosing parents constraint, and the siblings constraint simply extends the data available +/// in its data structure. +/// public class HierarchySiblings : AbstractRequireConstraintContainer, IHierarchyRequireConstraint { private const string ConstraintName = "siblings"; @@ -43,4 +104,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyStatistics.cs b/EvitaDB.Client/Queries/Requires/HierarchyStatistics.cs index 41418aa..c992145 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyStatistics.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyStatistics.cs @@ -2,6 +2,40 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The statistics constraint allows you to retrieve statistics about the hierarchy nodes that are returned by the +/// current query. When used it triggers computation of the queriedEntityCount, childrenCount statistics, or both for +/// each hierarchy node in the returned hierarchy tree. +/// It requires mandatory argument of type enum that specifies which statistics to compute: +/// - : triggers calculation of the count of child hierarchy nodes that exist in +/// the hierarchy tree below the given node; the count is correct regardless of whether the children themselves are +/// requested/traversed by the constraint definition, and respects hierarchyOfReference settings for automatic removal +/// of hierarchy nodes that would contain empty result set of queried entities () +/// - : triggers the calculation of the total number of queried entities that +/// will be returned if the current query is focused on this particular hierarchy node using the hierarchyWithin filter +/// constraint (the possible refining constraint in the form of directRelation and excluding-root is not taken into +/// account). +/// And optional argument of type enum allowing you to specify the base queried entity set that +/// is the source for statistics calculations: +/// - : complete filtering query constraint +/// - : filtering query constraint where the contents of optional userFilter +/// are ignored +/// The calculation always ignores hierarchyWithin because the focused part of the hierarchy tree is defined on +/// the requirement constraint level, but including having/excluding constraints. The having/excluding constraints are +/// crucial for the calculation of queriedEntityCount (and therefore also affects the value of childrenCount +/// transitively). +/// +/// Computational complexity of statistical data calculation +/// The performance price paid for calculating statistics is not negligible. The calculation of +/// is cheaper because it allows to eliminate "dead branches" early and thus conserve the computation cycles. +/// The calculation of the is more expensive because it requires counting +/// items up to the last one and must be precise. +/// We strongly recommend that you avoid using for root hierarchy nodes for +/// large datasets. +/// This query actually has to filter and aggregate all the records in the database, which is obviously quite expensive, +/// even considering that all the indexes are in-memory. Caching is probably the only way out if you really need +/// to crunch these numbers. +/// public class HierarchyStatistics : AbstractRequireConstraintLeaf, IHierarchyOutputRequireConstraint { private const string ConstraintName = "statistics"; @@ -33,4 +67,4 @@ public HierarchyStatistics(StatisticsBase statisticsBase, params StatisticsType[ : new object[] {statisticsBase}.Concat(statisticsTypes.Cast()).ToArray()) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/HierarchyStopAt.cs b/EvitaDB.Client/Queries/Requires/HierarchyStopAt.cs index de3690b..a0028b7 100644 --- a/EvitaDB.Client/Queries/Requires/HierarchyStopAt.cs +++ b/EvitaDB.Client/Queries/Requires/HierarchyStopAt.cs @@ -2,6 +2,18 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The stopAt container constraint is a service wrapping constraint container that only makes sense in combination with +/// one of the allowed nested constraints. See the usage examples for specific nested constraints. +/// It accepts one of the following inner constraints: +/// +/// +/// +/// +/// +/// which define the constraint that stops traversing the hierarchy tree when it's satisfied by a currently traversed +/// node. +/// public class HierarchyStopAt : AbstractRequireConstraintContainer, IHierarchyOutputRequireConstraint { private const string ConstraintName = "stopAt"; @@ -31,4 +43,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] } return new HierarchyStopAt(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/Page.cs b/EvitaDB.Client/Queries/Requires/Page.cs index 9e18260..3fec316 100644 --- a/EvitaDB.Client/Queries/Requires/Page.cs +++ b/EvitaDB.Client/Queries/Requires/Page.cs @@ -1,5 +1,21 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.DataTypes; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `page` requirement controls the number and slice of entities returned in the query response. If no page +/// requirement is used in the query, the default page 1 with the default page size 20 is used. If the requested page +/// exceeds the number of available pages, a result with the first page is returned. An empty result is only returned if +/// the query returns no result at all or the page size is set to zero. By automatically returning the first page result +/// when the requested page is exceeded, we try to avoid the need to issue a secondary request to fetch the data. +/// The information about the actual returned page and data statistics can be found in the query response, which is +/// wrapped in a so-called data chunk object. In case of the page constraint, the is used as data +/// chunk object. +/// Example: +/// +/// page(1, 24) +/// +/// public class Page : AbstractRequireConstraintLeaf { public int Number => (int) Arguments[0]!; @@ -12,4 +28,4 @@ private Page(params object?[] arguments) : base(arguments) public Page(int? number, int? size) : base(number ?? 1, size ?? 20) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/PriceContent.cs b/EvitaDB.Client/Queries/Requires/PriceContent.cs index 433c6dc..8fec8f5 100644 --- a/EvitaDB.Client/Queries/Requires/PriceContent.cs +++ b/EvitaDB.Client/Queries/Requires/PriceContent.cs @@ -1,5 +1,25 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `priceContent` requirement allows you to access the information about the prices of the entity. +/// If the mode is used, the `priceContent` requirement will only retrieve +/// the prices selected by the constraint. If the enum is +/// specified, no prices are returned at all, if the enum is specified, all prices of +/// the entity are returned regardless of the priceInPriceLists constraint in the filter (the constraint still controls +/// whether the entity is returned at all). +/// You can also add additional price lists to the list of price lists passed in the priceInPriceLists constraint by +/// specifying the price list names as string arguments to the `priceContent` requirement. This is useful if you want to +/// fetch non-indexed prices of the entity that cannot (and are not intended to) be used to filter the entities, but you +/// still want to fetch them to display in the UI for the user. +/// Example: +/// +/// priceContentRespectingFilter() +/// priceContentRespectingFilter("reference") +/// priceContentAll() +/// +/// public class PriceContent : AbstractRequireConstraintLeaf, IEntityContentRequire, IConstraintWithSuffix { public static readonly string[] EmptyPriceLists = Array.Empty(); @@ -48,4 +68,4 @@ public PriceContent(PriceContentMode fetchMode, params string[] priceLists) : ba new object[] {fetchMode}.Concat(priceLists).ToArray()) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/PriceContentMode.cs b/EvitaDB.Client/Queries/Requires/PriceContentMode.cs index b8af288..35b81b5 100644 --- a/EvitaDB.Client/Queries/Requires/PriceContentMode.cs +++ b/EvitaDB.Client/Queries/Requires/PriceContentMode.cs @@ -1,8 +1,11 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// Determines which prices will be fetched along with entity. +/// public enum PriceContentMode { None, RespectingFilter, All -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/PriceHistogram.cs b/EvitaDB.Client/Queries/Requires/PriceHistogram.cs index 29d7f5f..0913ec1 100644 --- a/EvitaDB.Client/Queries/Requires/PriceHistogram.cs +++ b/EvitaDB.Client/Queries/Requires/PriceHistogram.cs @@ -1,5 +1,19 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `priceHistogram` is computed from the price for sale. The interval related constraints - i.e. +/// and in the userFilter part are excluded for the sake of histogram calculation. If this weren't +/// the case, the user narrowing the filtered range based on the histogram results would be driven into a narrower and +/// narrower range and eventually into a dead end. +/// The priceType requirement the source price property for the histogram computation. If no requirement, the histogram +/// visualizes the price with tax. +/// Example: +/// +/// priceHistogram(20) +/// +/// public class PriceHistogram : AbstractRequireConstraintLeaf, IExtraResultRequireConstraint { public int RequestedBucketCount => (int) Arguments[0]!; @@ -11,4 +25,4 @@ private PriceHistogram(params object?[] arguments) : base(arguments) public PriceHistogram(int requestedBucketCount) : base(requestedBucketCount) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/PriceType.cs b/EvitaDB.Client/Queries/Requires/PriceType.cs index 831dac2..d3ab360 100644 --- a/EvitaDB.Client/Queries/Requires/PriceType.cs +++ b/EvitaDB.Client/Queries/Requires/PriceType.cs @@ -1,5 +1,20 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.Queries.Filter; +using EvitaDB.Client.Queries.Order; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// This `useOfPrice` require query can be used to control the form of prices that will be used for computation in +/// filtering, and , +/// ordering. Also is sensitive to this setting. +/// By default, end customer form of price (e.g. price with tax) is used in all above-mentioned constraints. This could +/// be changed by using this requirement query. It has single argument that can have one of the following values: +/// [WithTax] and [WithoutTax]. +/// Example: +/// +/// useOfPrice(WITH_TAX) +/// +/// public class PriceType : AbstractRequireConstraintLeaf { public QueryPriceMode QueryPriceMode => (QueryPriceMode) Arguments[0]!; @@ -8,4 +23,4 @@ public class PriceType : AbstractRequireConstraintLeaf public PriceType(QueryPriceMode queryPriceMode) : base(queryPriceMode) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/QueryPriceMode.cs b/EvitaDB.Client/Queries/Requires/QueryPriceMode.cs index b75301d..34ea0d8 100644 --- a/EvitaDB.Client/Queries/Requires/QueryPriceMode.cs +++ b/EvitaDB.Client/Queries/Requires/QueryPriceMode.cs @@ -1,6 +1,9 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// Determines which price will be used for filtering. +/// public enum QueryPriceMode { WithTax, WithoutTax -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/QueryTelemetry.cs b/EvitaDB.Client/Queries/Requires/QueryTelemetry.cs index f90de19..d5c699a 100644 --- a/EvitaDB.Client/Queries/Requires/QueryTelemetry.cs +++ b/EvitaDB.Client/Queries/Requires/QueryTelemetry.cs @@ -1,5 +1,13 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// This `queryTelemetry` requirement triggers creation of the DTO and including it the evitaDB +/// response. +/// Example: +/// +/// queryTelemetry() +/// +/// public class QueryTelemetry : AbstractRequireConstraintLeaf { private QueryTelemetry(params object?[] arguments) : base(arguments) @@ -9,4 +17,4 @@ private QueryTelemetry(params object?[] arguments) : base(arguments) public QueryTelemetry() : base() { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/ReferenceContent.cs b/EvitaDB.Client/Queries/Requires/ReferenceContent.cs index 906a664..e90c513 100644 --- a/EvitaDB.Client/Queries/Requires/ReferenceContent.cs +++ b/EvitaDB.Client/Queries/Requires/ReferenceContent.cs @@ -4,6 +4,87 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The `referenceContent` requirement allows you to access the information about the references the entity has towards +/// other entities (either managed by evitaDB itself or by any other external system). This variant of referenceContent +/// doesn't return the attributes set on the reference itself - if you need those attributes, use the +/// `referenceContentWithAttributes` variant of it. +/// Example: +/// +/// entityFetch( +/// attributeContent("code"), +/// referenceContent("brand"), +/// referenceContent("categories") +/// ) +/// +/// ## Referenced entity (group) fetching +/// In many scenarios, you'll need to fetch not only the primary keys of the referenced entities, but also their bodies +/// and the bodies of the groups the references refer to. One such common scenario is fetching the parameters of +/// a product: +/// +/// referenceContent( +/// "parameterValues", +/// entityFetch( +/// attributeContent("code") +/// ), +/// entityGroupFetch( +/// attributeContent("code") +/// ) +/// ) +/// +/// ## Filtering references +/// Sometimes your entities have a lot of references and you don't need all of them in certain scenarios. In this case, +/// you can use the filter constraint to filter out the references you don't need. +/// The referenceContent filter implicitly targets the attributes on the same reference it points to, so you don't need +/// to specify a referenceHaving constraint. However, if you need to declare constraints on referenced entity attributes, +/// you must wrap them in the container constraint. +/// For example, your product has got a lot of parameters, but on product detail page you need to fetch only those that +/// are part of group which contains an attribute `isVisibleInDetail` set to TRUE.To fetch only those parameters, +/// use the following query: +/// +/// referenceContent( +/// "parameterValues", +/// filterBy( +/// entityHaving( +/// referenceHaving( +/// "parameter", +/// entityHaving( +/// attributeEquals("isVisibleInDetail", true) +/// ) +/// ) +/// ) +/// ), +/// entityFetch( +/// attributeContent("code") +/// ), +/// entityGroupFetch( +/// attributeContent("code", "isVisibleInDetail") +/// ) +/// ) +/// +/// ##Ordering references +/// By default, the references are ordered by the primary key of the referenced entity. If you want to order +/// the references by a different property - either the attribute set on the reference itself or the property of the +/// referenced entity - you can use the order constraint inside the referenceContent requirement. +/// The `referenceContent` filter implicitly targets the attributes on the same reference it points to, so you don't need +/// to specify a referenceHaving constraint. However, if you need to declare constraints on referenced entity attributes, +/// you must wrap them in the entityHaving container constraint. +/// Let's say you want your parameters to be ordered by an English name of the parameter. To do this, use the following +/// query: +/// +/// referenceContent( +/// "parameterValues", +/// orderBy( +/// entityProperty( +/// attributeNatural("name", ASC) +/// ) +/// ), +/// entityFetch( +/// attributeContent("name") +/// ) +/// ) +/// +/// public class ReferenceContent : AbstractRequireConstraintContainer, IEntityContentRequire, ISeparateEntityContentRequireContainer, IConstraintContainerWithSuffix { @@ -145,4 +226,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] return new ReferenceContent(ReferencedNames, children, additionalChildren); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/Require.cs b/EvitaDB.Client/Queries/Requires/Require.cs index 2395dcd..29a9340 100644 --- a/EvitaDB.Client/Queries/Requires/Require.cs +++ b/EvitaDB.Client/Queries/Requires/Require.cs @@ -1,5 +1,19 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// Requirements have no direct parallel in other database languages. They define sideway calculations, paging, +/// the amount of data fetched for each returned entity, and so on, but never affect the number or order of returned +/// entities. They also allow to compute additional calculations that relate to the returned entities, but contain +/// other contextual data - for example hierarchy data for creating menus, facet summary for parametrized filter, +/// histograms for charts, and so on. +/// Example: +/// +/// require( +/// page(1, 2), +/// entityFetch() +/// ) +/// +/// public class Require : AbstractRequireConstraintContainer, IRequireConstraint { public new bool Necessary => Applicable; @@ -12,4 +26,4 @@ public override IRequireConstraint GetCopyWithNewChildren(IRequireConstraint?[] { return new Require(children); } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/StatisticsBase.cs b/EvitaDB.Client/Queries/Requires/StatisticsBase.cs index ed87320..0bd25ae 100644 --- a/EvitaDB.Client/Queries/Requires/StatisticsBase.cs +++ b/EvitaDB.Client/Queries/Requires/StatisticsBase.cs @@ -1,7 +1,11 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The enum specifies whether the hierarchy statistics cardinality will be based on a complete query filter by +/// constraint or only the part without user defined filter. +/// public enum StatisticsBase { CompleteFilter, WithoutUserFilter -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/StatisticsType.cs b/EvitaDB.Client/Queries/Requires/StatisticsType.cs index 6e001d1..842ff54 100644 --- a/EvitaDB.Client/Queries/Requires/StatisticsType.cs +++ b/EvitaDB.Client/Queries/Requires/StatisticsType.cs @@ -1,7 +1,11 @@ namespace EvitaDB.Client.Queries.Requires; +/// +/// The enum specifies whether the should produce the hierarchy children count or referenced +/// entity count. +/// public enum StatisticsType { ChildrenCount, QueriedEntityCount -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Requires/Strip.cs b/EvitaDB.Client/Queries/Requires/Strip.cs index 5b2c745..913087f 100644 --- a/EvitaDB.Client/Queries/Requires/Strip.cs +++ b/EvitaDB.Client/Queries/Requires/Strip.cs @@ -1,5 +1,21 @@ -namespace EvitaDB.Client.Queries.Requires; +using EvitaDB.Client.DataTypes; +namespace EvitaDB.Client.Queries.Requires; + +/// +/// The `strip` requirement controls the number and slice of entities returned in the query response. If the requested +/// strip exceeds the number of available records, a result from the zero offset with retained limit is returned. +/// An empty result is only returned if the query returns no result at all or the limit is set to zero. By automatically +/// returning the first strip result when the requested page is exceeded, we try to avoid the need to issue a secondary +/// request to fetch the data. +/// The information about the actual returned page and data statistics can be found in the query response, which is +/// wrapped in a so-called data chunk object. In case of the strip constraint, the is used as data +/// chunk object. +/// Example: +/// +/// strip(52, 24) +/// +/// public class Strip : AbstractRequireConstraintLeaf { public int Offset => (int) Arguments[0]!; @@ -12,4 +28,4 @@ private Strip(params object?[] arguments) : base(arguments) public Strip(int? offset, int? limit) : base(offset ?? 0, limit ?? 20) { } -} \ No newline at end of file +} diff --git a/EvitaDB.Client/Queries/Visitor/PrettyPrintingVisitor.cs b/EvitaDB.Client/Queries/Visitor/PrettyPrintingVisitor.cs index 8d280f6..d906672 100644 --- a/EvitaDB.Client/Queries/Visitor/PrettyPrintingVisitor.cs +++ b/EvitaDB.Client/Queries/Visitor/PrettyPrintingVisitor.cs @@ -65,9 +65,9 @@ public void Traverse(Query query) { _result.Append("query" + QueryUtils.ArgOpening).Append(NewLine()); Level = 1; - if (query.Entities is not null) + if (query.Collection is not null) { - query.Entities.Accept(this); + query.Collection.Accept(this); _result.Append(','); } diff --git a/EvitaDB.QueryValidator/Program.cs b/EvitaDB.QueryValidator/Program.cs index d8f7fe3..abe3e81 100644 --- a/EvitaDB.QueryValidator/Program.cs +++ b/EvitaDB.QueryValidator/Program.cs @@ -21,7 +21,7 @@ public static partial class Program { private const string TempFolderName = "evita-query-validator"; private const string QueryReplacementFileName = "evita-csharp-query-template.txt"; - private static readonly Regex TheQueryReplacement = ReplacementRegex(); + private static readonly Regex TheQueryReplacement = QueryReplacementRegex(); private static readonly JsonSerializerSettings JsonSettings = new() { @@ -30,7 +30,8 @@ public static partial class Program new EntitySerializer(), new OrderedJsonSerializer(), new StripListSerializer(), - new PaginatedListSerializer() + new PaginatedListSerializer(), + new DecimalConverter() }, TypeNameHandling = TypeNameHandling.None, ContractResolver = new OrderPropertiesResolver(), @@ -55,8 +56,9 @@ public static partial class Program public static void Main(string[] args) { string queryCode = args.Length > 0 ? args[0] : throw new ArgumentException("Query code is required!"); - string outputFormat = args.Length > 1 ? args[1] : throw new ArgumentException("Output format is required!"); - string? sourceVariable = args.Length > 2 ? args[2] : null; + string host = args.Length > 1 ? args[1] : throw new ArgumentException("Host is required!"); + string outputFormat = args.Length > 2 ? args[2] : throw new ArgumentException("Output format is required!"); + string? sourceVariable = args.Length > 3 ? args[3] : null; if (!File.Exists(QueryReplacementPath)) { @@ -69,8 +71,14 @@ public static void Main(string[] args) string code = string.Join('\n', templateLines .Select(theLine => { - Match replacementMatcher = TheQueryReplacement.Match(theLine); - return replacementMatcher.Success ? queryCode : theLine; + Match queryReplacementMatcher = TheQueryReplacement.Match(theLine); + string result = theLine; + if (queryReplacementMatcher.Success) + { + result = queryCode; + } + result = result.Replace("#HOST#", host); + return result; })); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code); @@ -94,7 +102,7 @@ public static void Main(string[] args) CSharpCompilation compilation = CSharpCompilation.Create( assemblyName, - syntaxTrees: new[] {syntaxTree}, + syntaxTrees: new[] { syntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.ConsoleApplication)); @@ -120,7 +128,7 @@ public static void Main(string[] args) if (snippetClass is not null && method is not null) { var responseAndEntitySchema = ((EvitaResponse? response, IEntitySchema entitySchema)) - method.Invoke(null, new object?[] {FindCatalogName(queryCode)})!; + method.Invoke(null, new object?[] { FindCatalogName(queryCode) })!; if (responseAndEntitySchema.response is not null) { string serializedOutput; @@ -139,7 +147,33 @@ public static void Main(string[] args) ResponseSerializerUtils.ExtractValueFrom(responseAndEntitySchema.response, sourceVariable.Split('.')); string stringSerialized = JsonConvert.SerializeObject(value, JsonSettings); - serializedOutput = WrapSerializedOutputInCodeBlock(stringSerialized); + serializedOutput = WrapSerializedOutputInCodeBlock("json", stringSerialized); + break; + } + case "string": + { + object? theValue; + if (string.IsNullOrEmpty(sourceVariable)) + { + theValue = responseAndEntitySchema.response; + } + else + { + theValue = ResponseSerializerUtils.ExtractValueFrom( + responseAndEntitySchema.response, + sourceVariable.Split('.')); + } + + string json; + if (theValue is not null) + { + json = theValue is IPrettyPrintable pp ? pp.PrettyPrint() : theValue.ToString()!; + } + else + { + json = ""; + } + serializedOutput = WrapSerializedOutputInCodeBlock("md", json); break; } default: @@ -185,11 +219,11 @@ private static void DownloadQueryTemplate() contentStream.CopyTo(stream); } - private static string WrapSerializedOutputInCodeBlock(string serializedOutput) + private static string WrapSerializedOutputInCodeBlock(string codeBlockLang, string serializedOutput) { - return $"```json\n{serializedOutput}\n```"; + return $"```{codeBlockLang}\n{serializedOutput}\n```"; } - [GeneratedRegex("^(\\s*)#QUERY#.*$", RegexOptions.Singleline)] - private static partial Regex ReplacementRegex(); -} \ No newline at end of file + [GeneratedRegex(@"^(\s*)#QUERY#.*$", RegexOptions.Singleline)] + private static partial Regex QueryReplacementRegex(); +} diff --git a/EvitaDB.QueryValidator/Serialization/Json/Converters/DecimalConverter.cs b/EvitaDB.QueryValidator/Serialization/Json/Converters/DecimalConverter.cs new file mode 100644 index 0000000..4780d20 --- /dev/null +++ b/EvitaDB.QueryValidator/Serialization/Json/Converters/DecimalConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace EvitaDB.QueryValidator.Serialization.Json.Converters; + +public class DecimalConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is decimal decimalValue) + { + writer.WriteValue(decimal.Parse(decimalValue.ToString("F2"))); // Example: Two decimal places + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + // Implement if needed for deserialization + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(decimal); + } +} diff --git a/EvitaDB.QueryValidator/Serialization/Json/Converters/EntitySerializer.cs b/EvitaDB.QueryValidator/Serialization/Json/Converters/EntitySerializer.cs index d8414fd..473a555 100644 --- a/EvitaDB.QueryValidator/Serialization/Json/Converters/EntitySerializer.cs +++ b/EvitaDB.QueryValidator/Serialization/Json/Converters/EntitySerializer.cs @@ -3,6 +3,7 @@ using EvitaDB.Client.Models.Data; using EvitaDB.Client.Models.Schemas; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace EvitaDB.QueryValidator.Serialization.Json.Converters; @@ -344,7 +345,14 @@ private static void WriteAssociatedData(JsonWriter writer, IAssociatedData value ComplexDataObjectToJsonConverter converter = new ComplexDataObjectToJsonConverter(); complexDataObject.Accept(converter); writer.WritePropertyName(fieldName); - serializer.Serialize(writer, converter.RootNode); + if (converter.RootNode is JObject jObject) + { + serializer.Serialize(writer, ConvertJObjectToDictionary(jObject)); + } + else + { + serializer.Serialize(writer, converter.RootNode); + } } else { @@ -356,6 +364,41 @@ private static void WriteAssociatedData(JsonWriter writer, IAssociatedData value }); } } + + static Dictionary ConvertJObjectToDictionary(JObject jsonObject) + { + var dictionary = new Dictionary(); + + foreach (var property in jsonObject.Properties()) + { + var propertyName = property.Name; + var propertyValue = ConvertJTokenToObject(property.Value); + + dictionary[propertyName] = propertyValue; + } + + return dictionary; + } + + static object? ConvertJTokenToObject(JToken token) + { + switch (token.Type) + { + case JTokenType.Object: + return ConvertJObjectToDictionary((JObject)token); + case JTokenType.Array: + return ((JArray)token).Select(ConvertJTokenToObject).ToList(); + case JTokenType.Integer: + case JTokenType.Float: + case JTokenType.String: + case JTokenType.Boolean: + case JTokenType.Null: + return ((JValue)token).Value; + default: + // Handle other token types as needed + throw new ArgumentException($"Unsupported token type: {token.Type}"); + } + } /** * Writes all reference from {@link ReferenceContract} to a JSON. @@ -488,4 +531,4 @@ private static void WritePrice(JsonWriter writer, IPrice value) writer.WriteEndObject(); }); } -} \ No newline at end of file +} diff --git a/EvitaDB.QueryValidator/Serialization/Markdown/MarkdownConverter.cs b/EvitaDB.QueryValidator/Serialization/Markdown/MarkdownConverter.cs index 1259970..7ff0889 100644 --- a/EvitaDB.QueryValidator/Serialization/Markdown/MarkdownConverter.cs +++ b/EvitaDB.QueryValidator/Serialization/Markdown/MarkdownConverter.cs @@ -4,6 +4,7 @@ using EvitaDB.Client.Models; using EvitaDB.Client.Models.Data; using EvitaDB.Client.Models.Schemas; +using EvitaDB.Client.Models.Schemas.Dtos; using EvitaDB.Client.Queries; using EvitaDB.Client.Queries.Filter; using EvitaDB.Client.Queries.Requires; @@ -20,7 +21,15 @@ public static partial class MarkdownConverter { new CultureInfo("en"), "\uD83C\uDDEC\uD83C\uDDE7" }, { new CultureInfo("de"), "\uD83C\uDDE9\uD83C\uDDEA" } }; - + + private static readonly IDictionary CurrencySymbols = new Dictionary + { + { "CZK", "Kč" }, + { "USD", "$" }, + { "GBP", "£" }, + { "EUR", "€" } + }; + private const string PredecessorHeadSymbol = "⎆"; private const string PredecessorSymbol = "↻ "; @@ -55,7 +64,7 @@ query.Require is not null && query.Require QueryUtils.FindConstraint(query.FilterBy) is not null; // collect headers for the MarkDown table - var headers = new List { EntityPrimaryKey }; + List headers = new List { EntityPrimaryKey }; if (entityFetch is not null) { headers.AddRange(GetEntityHeaders(entityFetch, () => response.RecordData, @@ -129,22 +138,24 @@ query.Require is not null && query.Require ); // define the table with header line - var tableBuilder = new Table.Builder() + Table.Builder tableBuilder = new Table.Builder() .WithAlignment(Table.AlignLeft) // ReSharper disable once CoVariantArrayConversion .AddRow(headers.ToArray()); // prepare price formatter - var locale = query.FilterBy? + CultureInfo? locale = query.FilterBy? .Select(QueryUtils.FindConstraint) .Select(f => f?.Locale) - .FirstOrDefault() ?? new CultureInfo("en"); - - var currency = query.FilterBy? + .FirstOrDefault() ?? Locales.Keys.FirstOrDefault(x=>x.Name == "en-US"); + + string currency = query.FilterBy? .Select(QueryUtils.FindConstraint) - .Select(f => f?.Currency.CurrencyCode) - .FirstOrDefault() ?? new CultureInfo("de-DE").NumberFormat.CurrencySymbol; - var priceFormatter = new CultureInfo(locale.Name) { NumberFormat = { CurrencySymbol = currency } }; + .Select(f => + f is not null + ? CurrencySymbols[f.Currency.CurrencyCode] + : CurrencySymbols["EUR"]) + .FirstOrDefault()!; // add rows foreach (var sealedEntity in response.RecordData) @@ -203,11 +214,10 @@ query.Require is not null && query.Require { return sealedEntity.PriceForSale is not null ? PriceLink + - string.Format(priceFormatter, "{0:C}", sealedEntity.PriceForSale.PriceWithTax) + + $"{currency}{sealedEntity.PriceForSale.PriceWithTax:N2}" + " (with " + decimal.Parse(sealedEntity.PriceForSale.TaxRate.ToString("0.#########")) + "%" + - " tax) / " + string.Format(priceFormatter, "{0:C}", - sealedEntity.PriceForSale.PriceWithoutTax) + " tax) / " + $"{currency}{sealedEntity.PriceForSale.PriceWithoutTax:N2}" : "N/A"; } @@ -221,7 +231,7 @@ query.Require is not null && query.Require return string.Join(", ", prices.Take(3).Select(price => - PriceLink + string.Format(priceFormatter, "{0:C}", price.PriceWithTax))) + + PriceLink + $"{currency}{price.PriceWithTax:N2}")) + (prices.Count > 3 ? $" ... and {prices.Count - 3} other prices" : ""); } @@ -299,7 +309,9 @@ private static IEnumerable GetEntityHeaders(EntityFetch entityFetch, { if (attributeContent.AllRequested) { - IEnumerable attributes = entitySchema.GetAttributes().Values; + IEnumerable attributes = entitySchema is EntitySchemaDecorator schema + ? schema.OrderedAttributes + : entitySchema.GetAttributes().Values; return (localizedQuery ? attributes.Where(x => x.Localized) : attributes) .Select(x => x.Name) .Where(attrName => diff --git a/EvitaDB.QueryValidator/csharp_query_template.txt b/EvitaDB.QueryValidator/evita-csharp-query-template.txt similarity index 88% rename from EvitaDB.QueryValidator/csharp_query_template.txt rename to EvitaDB.QueryValidator/evita-csharp-query-template.txt index a0b6507..58e2f89 100644 --- a/EvitaDB.QueryValidator/csharp_query_template.txt +++ b/EvitaDB.QueryValidator/evita-csharp-query-template.txt @@ -23,7 +23,7 @@ using static EvitaDB.Client.Queries.Requires.EmptyHierarchicalEntityBehaviour; public class DynamicClass { private static readonly EvitaClientConfiguration EvitaClientConfiguration = new EvitaClientConfiguration.Builder() - .SetHost("demo.evitadb.io") + .SetHost("#HOST#") .SetPort(5556) .SetUseGeneratedCertificate(false) .SetUsingTrustedRootCaCertificate(true) @@ -35,9 +35,10 @@ public class DynamicClass public static (EvitaResponse, IEntitySchema) Run(string catalogName) { + Environment.SetEnvironmentVariable(IDevelopmentConstants.TestRun, "true"); EvitaClient evita = new EvitaClient(EvitaClientConfiguration); #QUERY# - IEntitySchema entitySchema = evita.CreateReadOnlySession(catalogName).GetEntitySchemaOrThrow(entities.Query.Entities.EntityType); + IEntitySchema entitySchema = evita.CreateReadOnlySession(catalogName).GetEntitySchemaOrThrow(entities.Query.Collection.EntityType); return (entities, entitySchema); } } \ No newline at end of file diff --git a/EvitaDB.Test/Tests/EvitaDataTypesTest.cs b/EvitaDB.Test/Tests/EvitaDataTypesTest.cs index e5deeb3..33b8cf1 100644 --- a/EvitaDB.Test/Tests/EvitaDataTypesTest.cs +++ b/EvitaDB.Test/Tests/EvitaDataTypesTest.cs @@ -24,16 +24,3 @@ public void ShouldFormatDateTimeOffsetsCorrectly() Assert.Equal(dateTimeOffset4, EvitaDataTypes.FormatValue(DateTimeOffset.Parse(dateTimeOffset5))); } } - -public class DateTimeOffsetData -{ - public static IEnumerable Data => - new List - { - new object[] { DateTimeOffset.Parse("2023-09-08T14:08:26+02:00") }, - new object[] { DateTimeOffset.Parse("2023-10-21T11:44:03.6+02:00") }, - new object[] { DateTimeOffset.Parse("2023-10-21T11:44:03.68+02:00") }, - new object[] { DateTimeOffset.Parse("2023-10-21T11:44:03.681+02:00") }, - new object[] { DateTimeOffset.Parse("2023-10-21T11:44:03.6812+02:00") } - }; -} \ No newline at end of file