From af8afd58e76047e74434a99bd417706083024743 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 28 Jun 2026 22:17:56 -0400 Subject: [PATCH] fix(indexing): validate index definitions Signed-off-by: Yordis Prieto --- .../Storage/Indexing/IndexDefinitionTests.cs | 105 ++++++++++++++++++ .../Storage/Indexing/IndexDefinition.cs | 84 ++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/EventStore.Core.XUnit.Tests/Services/Storage/Indexing/IndexDefinitionTests.cs create mode 100644 src/EventStore.Core/Services/Storage/Indexing/IndexDefinition.cs diff --git a/src/EventStore.Core.XUnit.Tests/Services/Storage/Indexing/IndexDefinitionTests.cs b/src/EventStore.Core.XUnit.Tests/Services/Storage/Indexing/IndexDefinitionTests.cs new file mode 100644 index 000000000..f78dfd6f9 --- /dev/null +++ b/src/EventStore.Core.XUnit.Tests/Services/Storage/Indexing/IndexDefinitionTests.cs @@ -0,0 +1,105 @@ +using System; +using EventStore.Core.Services.Storage.Indexing; +using Xunit; + +namespace EventStore.Core.XUnit.Tests.Services.Storage.Indexing; + +public class IndexDefinitionTests +{ + [Fact] + public void constructor_rejects_missing_fields() + { + var exception = Assert.Throws(() => new IndexDefinition(new IndexEventFilter("event.type == 'order'"), null!)); + + Assert.Equal("fields", exception.ParamName); + } + + [Fact] + public void constructor_rejects_null_field() + { + var exception = Assert.Throws(() => new IndexDefinition(new IndexEventFilter("event.type == 'order'"), [null!])); + + Assert.Equal("fields", exception.ParamName); + } + + [Fact] + public void constructor_requires_filter_or_field() + { + var exception = Assert.Throws(() => new IndexDefinition(null, [])); + + Assert.Equal("fields", exception.ParamName); + } + + [Fact] + public void constructor_accepts_filter_without_fields() + { + var filter = new IndexEventFilter("event.type == 'order'"); + var definition = new IndexDefinition(filter, []); + + Assert.Same(filter, definition.Filter); + Assert.Empty(definition.Fields); + } + + [Fact] + public void constructor_accepts_field_without_filter() + { + var field = new IndexFieldDefinition("customerId", new IndexFieldSelector("event.body.customerId")); + var definition = new IndexDefinition(null, [field]); + + Assert.Null(definition.Filter); + Assert.Equal([field], definition.Fields); + } + + [Fact] + public void constructor_copies_fields() + { + var field = new IndexFieldDefinition("customerId", new IndexFieldSelector("event.body.customerId")); + var fields = new[] { field }; + var definition = new IndexDefinition(null, fields); + + fields[0] = new IndexFieldDefinition("tenantId", new IndexFieldSelector("event.body.tenantId")); + + Assert.Equal([field], definition.Fields); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void filter_rejects_empty_value(string value) + { + var exception = Assert.Throws(() => new IndexEventFilter(value)); + + Assert.Equal("value", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void field_rejects_empty_name(string name) + { + var exception = Assert.Throws(() => new IndexFieldDefinition(name, new IndexFieldSelector("event.body.customerId"))); + + Assert.Equal("name", exception.ParamName); + } + + [Fact] + public void field_rejects_missing_selector() + { + var exception = Assert.Throws(() => new IndexFieldDefinition("customerId", null!)); + + Assert.Equal("selector", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void field_selector_rejects_empty_value(string value) + { + var exception = Assert.Throws(() => new IndexFieldSelector(value)); + + Assert.Equal("value", exception.ParamName); + } +} diff --git a/src/EventStore.Core/Services/Storage/Indexing/IndexDefinition.cs b/src/EventStore.Core/Services/Storage/Indexing/IndexDefinition.cs new file mode 100644 index 000000000..ac6301450 --- /dev/null +++ b/src/EventStore.Core/Services/Storage/Indexing/IndexDefinition.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EventStore.Core.Services.Storage.Indexing; + +public sealed record IndexDefinition +{ + public IndexEventFilter Filter { get; } + + public IReadOnlyList Fields { get; } + + public IndexDefinition(IndexEventFilter filter, IReadOnlyList fields) + { + ArgumentNullException.ThrowIfNull(fields); + + if (fields.Any(static field => field is null)) + { + throw new ArgumentException("Index fields cannot contain null.", nameof(fields)); + } + + if (filter is null && fields.Count == 0) + { + throw new ArgumentException("Index definition must specify at least one filter or field.", nameof(fields)); + } + + Filter = filter; + Fields = fields.ToArray(); + } +} + +public sealed record IndexEventFilter +{ + public string Value { get; } + + public IndexEventFilter(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Index filter cannot be empty.", nameof(value)); + } + + Value = value; + } + + public override string ToString() => Value; +} + +public sealed record IndexFieldDefinition +{ + public string Name { get; } + + public IndexFieldSelector Selector { get; } + + public IndexFieldDefinition(string name, IndexFieldSelector selector) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Index field name cannot be empty.", nameof(name)); + } + + ArgumentNullException.ThrowIfNull(selector); + + Name = name; + Selector = selector; + } +} + +public sealed record IndexFieldSelector +{ + public string Value { get; } + + public IndexFieldSelector(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Index field selector cannot be empty.", nameof(value)); + } + + Value = value; + } + + public override string ToString() => Value; +}