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