File: Otlp\Storage\SqliteTelemetryRepository.Metrics.Reads.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Dapper;
using Microsoft.Data.Sqlite;
 
namespace Aspire.Dashboard.Otlp.Storage;
 
public sealed partial class SqliteTelemetryRepository
{
    private const int MaxMetricReadBatchSize = 500;
    // Buckets are only an index prefilter. Keep exact interval checks for windows within a bucket.
    private static readonly string s_metricPointRangeFilterSql = string.Create(CultureInfo.InvariantCulture, $"p.start_time_unix_nano <= @EndUnixNano AND p.end_time_unix_nano >= r.start_time_unix_nano AND p.end_time_bucket >= r.start_time_unix_nano / {MetricTimeBucketWidthUnixNano}");
    private static readonly string s_metricSourcePointRangeFilterSql = string.Create(CultureInfo.InvariantCulture, $"source.start_time_unix_nano <= @EndUnixNano AND source.end_time_unix_nano >= r.start_time_unix_nano AND source.end_time_bucket >= r.start_time_unix_nano / {MetricTimeBucketWidthUnixNano}");
    private static readonly string s_selectedMetricPointsCteSql = $"""
        selected_metric_points AS (
            SELECT
                p.point_id,
                p.dimension_id,
                p.point_type,
                p.start_time_unix_nano,
                p.end_time_unix_nano,
                p.repeat_count,
                p.integer_value,
                p.double_value,
                p.histogram_sum,
                p.histogram_count,
                p.histogram_aggregation_start_unix_nano,
                p.histogram_aggregation_id,
                i.aggregation_temporality
            FROM telemetry_metric_points p
            JOIN telemetry_metric_dimensions d ON d.dimension_id = p.dimension_id
            JOIN telemetry_metric_instruments i ON i.instrument_id = d.instrument_id
            JOIN metric_dimension_query_ranges r ON r.dimension_id = p.dimension_id
            WHERE {s_metricPointRangeFilterSql}
        )
        """;
    private static readonly string s_effectiveMetricPointsCteSql = $"""
        {s_selectedMetricPointsCteSql},
        ranked_metric_points AS (
            SELECT
                p.*,
                ROW_NUMBER() OVER (
                    PARTITION BY p.start_time_unix_nano, p.dimension_id, p.histogram_aggregation_id
                    ORDER BY p.point_id DESC) AS point_rank
            FROM selected_metric_points p
        ),
        effective_metric_points AS (
            SELECT *
            FROM ranked_metric_points
            WHERE point_rank = 1
        )
        """;
    // An aggregation identity separates cumulative resets within one rollup. Delta points each
    // have their own identity and retain their interval start, so no delta observations are discarded.
    // Cumulative representatives start at their earliest source interval, not the rounded bucket
    // boundary: rounding a reset at 3.5s to 3s would lower the preceding window's count.
    private static readonly string s_rolledUpMetricPointsCteSql = $"""
        {s_effectiveMetricPointsCteSql},
        bucketed_metric_points AS (
            SELECT
                p.*,
                (p.start_time_unix_nano / @PointIntervalUnixNano) * @PointIntervalUnixNano AS rollup_start_time_unix_nano
            FROM effective_metric_points p
        ),
        ranked_rollup_metric_points AS (
            SELECT
                p.*,
                MIN(p.start_time_unix_nano) OVER (
                    PARTITION BY p.dimension_id, p.point_type, p.rollup_start_time_unix_nano, p.histogram_aggregation_id) AS rollup_source_start_time_unix_nano,
                MAX(p.end_time_unix_nano) OVER (
                    PARTITION BY p.dimension_id, p.point_type, p.rollup_start_time_unix_nano, p.histogram_aggregation_id) AS rollup_end_time_unix_nano,
                SUM(p.repeat_count) OVER (
                    PARTITION BY p.dimension_id, p.point_type, p.rollup_start_time_unix_nano, p.histogram_aggregation_id) AS rollup_repeat_count,
                ROW_NUMBER() OVER (
                    PARTITION BY p.dimension_id, p.point_type, p.rollup_start_time_unix_nano, p.histogram_aggregation_id
                    ORDER BY
                        CASE WHEN p.point_type = {HistogramPointType} THEN p.start_time_unix_nano END DESC,
                        p.integer_value DESC,
                        p.double_value DESC,
                        p.point_id DESC) AS rollup_rank
            FROM bucketed_metric_points p
        ),
        rolled_up_metric_points AS (
            SELECT
                p.point_id,
                p.dimension_id,
                p.point_type,
                CASE
                    WHEN p.point_type = {HistogramPointType} AND p.aggregation_temporality = {(int)OtlpAggregationTemporality.Delta} THEN p.start_time_unix_nano
                    WHEN p.point_type = {HistogramPointType} THEN p.rollup_source_start_time_unix_nano
                    ELSE p.rollup_start_time_unix_nano
                END AS start_time_unix_nano,
                p.rollup_end_time_unix_nano AS end_time_unix_nano,
                p.rollup_repeat_count AS repeat_count,
                p.integer_value,
                p.double_value,
                p.histogram_sum,
                p.histogram_count,
                p.histogram_aggregation_start_unix_nano,
                p.histogram_aggregation_id,
                p.aggregation_temporality
            FROM ranked_rollup_metric_points p
            WHERE p.rollup_rank = 1
        )
        """;
    private static readonly string s_fullFidelityMetricPointsCteSql = $"""
        {s_effectiveMetricPointsCteSql},
        rolled_up_metric_points AS (
            SELECT
                p.point_id,
                p.dimension_id,
                p.point_type,
                p.start_time_unix_nano,
                p.end_time_unix_nano,
                p.repeat_count,
                p.integer_value,
                p.double_value,
                p.histogram_sum,
                p.histogram_count,
                p.histogram_aggregation_start_unix_nano,
                p.histogram_aggregation_id,
                p.aggregation_temporality
            FROM effective_metric_points p
        )
        """;
 
    private OtlpInstrumentData? GetInstrumentFromDatabase(GetInstrumentRequest request, CancellationToken cancellationToken)
    {
        var instruments = GetCachedInstruments(request.ResourceKey, request.MeterName, request.InstrumentName);
        if (instruments.Count == 0)
        {
            return null;
        }
 
        using var connection = _database.OpenConnection();
        using var interrupt = connection.RegisterInterrupt(cancellationToken);
        // Keep dimensions, points, exemplars, and their attributes in one snapshot
        // so point trimming cannot change the result between queries.
        using var transaction = connection.BeginTransaction(deferred: true);
        var knownAttributeValues = new Dictionary<string, List<string?>>();
        var dimensions = MaterializeMetricDimensions(
            connection,
            transaction,
            instruments,
            request.StartTime,
            request.EndTime,
            request.DimensionFilters,
            request.DimensionCursors,
            request.DataPointInterval,
            request.IncludeExemplars,
            request.PopulateExemplarAttributes,
            knownAttributeValues,
            out var hasOverflow);
        return new OtlpInstrumentData
        {
            Summary = instruments[0].Summary,
            Dimensions = dimensions,
            KnownAttributeValues = knownAttributeValues,
            HasOverflow = hasOverflow
        };
    }
 
    private DateTime? GetInstrumentLatestEndTimeFromDatabase(ResourceKey resourceKey, string meterName, string instrumentName)
    {
        using var connection = _database.OpenConnection();
        // The highest bucket contains the maximum exact end time, but the newest point ID may not.
        // Keep both seeks inside a per-dimension scalar query: a top-level MAX can make SQLite
        // scan all points and execute the correlated bucket lookup once per point.
        var endTimeUnixNano = connection.QuerySingleOrDefault<long?>("""
            SELECT MAX((
                SELECT MAX(p.end_time_unix_nano)
                FROM telemetry_metric_points p
                WHERE p.dimension_id = d.dimension_id
                  AND p.end_time_bucket = (
                      SELECT end_time_bucket
                      FROM telemetry_metric_points
                      WHERE dimension_id = d.dimension_id
                      ORDER BY end_time_bucket DESC
                      LIMIT 1)
            ))
            FROM telemetry_metric_dimensions d
            JOIN telemetry_metric_instruments i ON i.instrument_id = d.instrument_id
            JOIN telemetry_resources r ON r.resource_id = i.resource_id
            JOIN telemetry_scopes s ON s.scope_id = i.scope_id
            WHERE r.resource_name = @ResourceName COLLATE NOCASE
                            AND (@InstanceId IS NULL OR r.instance_id = @InstanceId COLLATE NOCASE)
              AND s.scope_name = @MeterName
              AND i.instrument_name = @InstrumentName;
            """, new { ResourceName = resourceKey.Name, resourceKey.InstanceId, MeterName = meterName, InstrumentName = instrumentName });
        return endTimeUnixNano is not null ? OtlpHelpers.UnixNanoSecondsToDateTime(checked((ulong)endTimeUnixNano.Value)) : null;
    }
 
    private List<DimensionScope> MaterializeMetricDimensions(
        SqliteConnection connection,
        SqliteTransaction transaction,
        IReadOnlyList<CachedInstrument> instruments,
        DateTime? startTime,
        DateTime? endTime,
        IReadOnlyDictionary<string, IReadOnlyList<string?>> dimensionFilters,
        IReadOnlyList<MetricDimensionCursor> dimensionCursors,
        TimeSpan? dataPointInterval,
        bool includeExemplars,
        bool populateExemplarAttributes,
        Dictionary<string, List<string?>> knownAttributeValues,
        out bool hasOverflow)
    {
        if (dataPointInterval is { } interval && interval <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException(nameof(dataPointInterval), interval, "The metric data point interval must be greater than zero.");
        }
        if (dataPointInterval is { } largeInterval && largeInterval.Ticks > long.MaxValue / TimeSpan.NanosecondsPerTick)
        {
            throw new ArgumentOutOfRangeException(nameof(dataPointInterval), largeInterval, "The metric data point interval must fit in signed nanoseconds.");
        }
 
        var dimensions = GetMetricDimensions(
            connection,
            transaction,
            instruments,
            dimensionFilters,
            knownAttributeValues,
            out hasOverflow);
        if (dimensions.Count == 0)
        {
            return [];
        }
 
        var results = dimensions
            .Select(dimension => dimension.Scope)
            .ToList();
        if (startTime is null || endTime is null)
        {
            return results;
        }
 
        var queryParameters = new DynamicParameters();
        queryParameters.Add("EndUnixNano", GetMetricQueryTimeUnixNano(endTime.Value, upperBound: true));
        queryParameters.Add("PointIntervalUnixNano", dataPointInterval is { } pointInterval ? pointInterval.Ticks * TimeSpan.NanosecondsPerTick : 0);
        var dimensionQueryRangesCteSql = CreateMetricDimensionQueryRangesCte(
            dimensions,
            dimensionCursors,
            startTime,
            queryParameters);
        var metricPointsCteSql = dataPointInterval is null ? s_fullFidelityMetricPointsCteSql : s_rolledUpMetricPointsCteSql;
        var pointRecords = connection.Query<MetricPointDataRecord>($"""
            WITH {dimensionQueryRangesCteSql},
            {metricPointsCteSql}
            SELECT
                p.point_id AS PointId,
                p.dimension_id AS DimensionId,
                p.point_type AS PointType,
                p.start_time_unix_nano AS StartTimeUnixNano,
                p.end_time_unix_nano AS EndTimeUnixNano,
                p.repeat_count AS RepeatCount,
                p.integer_value AS IntegerValue,
                p.double_value AS DoubleValue,
                p.histogram_sum AS HistogramSum,
                p.histogram_count AS HistogramCount,
                p.histogram_aggregation_start_unix_nano AS HistogramAggregationStartUnixNano,
                p.histogram_aggregation_id AS HistogramAggregationId,
                p.aggregation_temporality AS AggregationTemporality,
                stored.start_time_unix_nano AS SourceStartTimeUnixNano,
                stored.bucket_counts AS BucketCounts,
                stored.explicit_bounds AS ExplicitBounds
            FROM rolled_up_metric_points p
            JOIN telemetry_metric_points stored ON stored.point_id = p.point_id
            ORDER BY p.dimension_id, p.start_time_unix_nano, p.point_id;
            """, queryParameters, transaction).AsList();
        var points = pointRecords.ToLookup(record => record.DimensionId);
        var exemplars = includeExemplars
            ? MaterializeMetricExemplars(
                connection,
                transaction,
                dimensionQueryRangesCteSql,
                queryParameters,
                dataPointInterval,
                pointRecords,
                populateExemplarAttributes)
            : Array.Empty<KeyValuePair<long, MetricsExemplar>>().ToLookup(pair => pair.Key, pair => pair.Value);
 
        for (var dimensionIndex = 0; dimensionIndex < dimensions.Count; dimensionIndex++)
        {
            var dimensionId = dimensions[dimensionIndex].DimensionId;
            var dimension = results[dimensionIndex];
            foreach (var point in points[dimensionId])
            {
                MetricValueBase value = point.PointType switch
                {
                    LongPointType => new MetricValue<long>(point.IntegerValue!.Value, checked((ulong)point.StartTimeUnixNano), checked((ulong)point.EndTimeUnixNano)),
                    DoublePointType => new MetricValue<double>(point.DoubleValue!.Value, checked((ulong)point.StartTimeUnixNano), checked((ulong)point.EndTimeUnixNano)),
                    HistogramPointType => CreateHistogramValue(point),
                    _ => throw new InvalidOperationException($"Unknown metric point type '{point.PointType}'.")
                };
                if (point.PointType != HistogramPointType)
                {
                    value.Count = checked((ulong)point.RepeatCount);
                }
                value.Exemplars.AddRange(exemplars[point.PointId]);
                dimension.Values.Add(value);
            }
        }
        return results;
    }
 
    private List<StoredMetricDimension> GetMetricDimensions(
        SqliteConnection connection,
        SqliteTransaction transaction,
        IReadOnlyList<CachedInstrument> instruments,
        IReadOnlyDictionary<string, IReadOnlyList<string?>> dimensionFilters,
        Dictionary<string, List<string?>> knownAttributeValues,
        out bool hasOverflow)
    {
        var dimensionRecords = connection.Query<MetricDimensionAttributeRecord>("""
            SELECT
                d.dimension_id AS DimensionId,
                d.instrument_id AS InstrumentId,
                a.attribute_key AS AttributeKey,
                a.attribute_value AS AttributeValue
            FROM telemetry_metric_dimensions d
            LEFT JOIN telemetry_metric_dimension_attributes a ON a.dimension_id = d.dimension_id
            WHERE d.instrument_id IN @InstrumentIds
            ORDER BY d.dimension_id, a.ordinal;
            """, new { InstrumentIds = instruments.Select(instrument => instrument.InstrumentId) }, transaction).AsList();
        var dimensionIds = dimensionRecords.Select(record => record.DimensionId).Distinct().ToArray();
        var pointAttributes = dimensionRecords
            .Where(record => record.AttributeKey is not null)
            .Select(record => new OwnedAttributeRecord
            {
                OwnerId = record.DimensionId,
                AttributeKey = record.AttributeKey!,
                AttributeValue = record.AttributeValue!
            })
            .ToLookup(record => record.OwnerId);
        hasOverflow = dimensionIds.Any(dimensionId => IsOverflowDimension(pointAttributes[dimensionId]));
        var instrumentsById = instruments.ToDictionary(instrument => instrument.InstrumentId);
        var instrumentIdsByDimensionId = dimensionRecords
            .DistinctBy(record => record.DimensionId)
            .ToDictionary(record => record.DimensionId, record => record.InstrumentId);
        // Dimension rows store only point attributes. Scope attributes are stored once with the scope and merged here for display and filtering.
        var attributes = dimensionIds
            .SelectMany(dimensionId => pointAttributes[dimensionId]
                .Select(attribute => KeyValuePair.Create(attribute.AttributeKey, attribute.AttributeValue))
                .Concat(instrumentsById[instrumentIdsByDimensionId[dimensionId]].Summary.Parent.Attributes)
                .Select(attribute => new OwnedAttributeRecord
                {
                    OwnerId = dimensionId,
                    AttributeKey = attribute.Key,
                    AttributeValue = attribute.Value
                }))
            .ToLookup(record => record.OwnerId);
        PopulateKnownAttributeValues(dimensionIds, attributes, knownAttributeValues);
 
        return dimensionIds
            .Where(dimensionId => MatchesDimensionFilters(attributes[dimensionId], dimensionFilters))
            .Select(dimensionId => new StoredMetricDimension(
                dimensionId,
                new DimensionScope(
                    _otlpContext.Options.MaxMetricsCount,
                    attributes[dimensionId].Select(attribute => KeyValuePair.Create(attribute.AttributeKey, attribute.AttributeValue)).ToArray())))
            .ToList();
    }
 
    private static bool IsOverflowDimension(IEnumerable<OwnedAttributeRecord> attributes)
    {
        using var enumerator = attributes.GetEnumerator();
        return enumerator.MoveNext() &&
            enumerator.Current is { AttributeKey: "otel.metric.overflow", AttributeValue: "true" } &&
            !enumerator.MoveNext();
    }
 
    private static string CreateMetricDimensionQueryRangesCte(
        IReadOnlyList<StoredMetricDimension> dimensions,
        IReadOnlyList<MetricDimensionCursor> dimensionCursors,
        DateTime? defaultStartTime,
        DynamicParameters queryParameters)
    {
        var sql = new StringBuilder("metric_dimension_query_ranges(dimension_id, start_time_unix_nano) AS (VALUES ");
        for (var i = 0; i < dimensions.Count; i++)
        {
            if (i > 0)
            {
                sql.Append(", ");
            }
 
            var dimension = dimensions[i];
            var cursor = dimensionCursors.FirstOrDefault(cursor =>
                cursor.Attributes.SequenceEqual(dimension.Scope.Attributes));
            queryParameters.Add($"DimensionId{i}", dimension.DimensionId);
            queryParameters.Add($"DimensionStartUnixNano{i}", GetMetricQueryTimeUnixNano(cursor?.StartTime ?? defaultStartTime ?? DateTime.UnixEpoch, upperBound: false));
            sql.Append(CultureInfo.InvariantCulture, $"(@DimensionId{i}, @DimensionStartUnixNano{i})");
        }
        sql.Append(')');
        return sql.ToString();
    }
 
    private static long? GetMetricQueryTimeUnixNano(DateTime time, bool upperBound)
    {
        var ticks = time.Ticks - DateTime.UnixEpoch.Ticks;
        if (ticks < 0)
        {
            return upperBound ? null : 0;
        }
        if (ticks > long.MaxValue / TimeSpan.NanosecondsPerTick)
        {
            return upperBound ? long.MaxValue : null;
        }
 
        var nanoseconds = ticks * TimeSpan.NanosecondsPerTick;
        // DateTime filters include their entire display tick. Otherwise a read-only query ending at
        // the latest displayed time could exclude a point 20 ns into that tick. Min/MaxValue remain
        // usable as unbounded filters; bounds wholly outside the supported range match no points.
        const long tickRemainder = TimeSpan.NanosecondsPerTick - 1;
        return upperBound ? Math.Min(nanoseconds, long.MaxValue - tickRemainder) + tickRemainder : nanoseconds;
    }
 
    private static bool MatchesDimensionFilters(IEnumerable<OwnedAttributeRecord> attributes, IReadOnlyDictionary<string, IReadOnlyList<string?>> dimensionFilters)
    {
        foreach (var (key, values) in dimensionFilters)
        {
            var value = attributes.FirstOrDefault(attribute => attribute.AttributeKey == key)?.AttributeValue;
            if (!values.Contains(value))
            {
                return false;
            }
        }
        return true;
    }
 
    private static void PopulateKnownAttributeValues(
        IReadOnlyList<long> dimensionIds,
        ILookup<long, OwnedAttributeRecord> attributes,
        Dictionary<string, List<string?>> knownAttributeValues)
    {
        // Point and scope attributes were already accepted during ingestion, so intentionally do not limit their
        // merged key or per-key value counts while building display metadata.
        for (var dimensionIndex = 0; dimensionIndex < dimensionIds.Count; dimensionIndex++)
        {
            var dimensionId = dimensionIds[dimensionIndex];
            foreach (var key in knownAttributeValues.Keys.Union(attributes[dimensionId].Select(attribute => attribute.AttributeKey)).Distinct().ToList())
            {
                if (!knownAttributeValues.TryGetValue(key, out var values))
                {
                    values = [];
                    knownAttributeValues.Add(key, values);
                    if (dimensionIndex > 0)
                    {
                        TryAddValue(values, null);
                    }
                }
                var value = attributes[dimensionId].FirstOrDefault(attribute => attribute.AttributeKey == key)?.AttributeValue;
                TryAddValue(values, value);
            }
        }
 
        static void TryAddValue(List<string?> values, string? value)
        {
            if (!values.Contains(value))
            {
                values.Add(value);
            }
        }
    }
 
    private static HistogramValue CreateHistogramValue(MetricPointDataRecord point) => new(
        UnpackUInt64Values(point.BucketCounts!),
        point.HistogramSum!.Value,
        checked((ulong)point.HistogramCount!.Value),
        checked((ulong)point.StartTimeUnixNano),
        checked((ulong)point.EndTimeUnixNano),
        UnpackDoubleValues(point.ExplicitBounds!),
        checked((ulong)point.HistogramAggregationStartUnixNano!.Value),
        point.HistogramAggregationId!.Value,
        (OtlpAggregationTemporality)point.AggregationTemporality);
 
    private static ILookup<long, MetricsExemplar> MaterializeMetricExemplars(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string dimensionQueryRangesCteSql,
        DynamicParameters queryParameters,
        TimeSpan? dataPointInterval,
        IReadOnlyList<MetricPointDataRecord> points,
        bool populateExemplarAttributes)
    {
        // Filter intervals through the time-window index before loading points and exemplars
        // to avoid scanning their history.
        var records = connection.Query<MetricExemplarRecord>($"""
            WITH {dimensionQueryRangesCteSql}
            SELECT
                e.exemplar_id AS ExemplarId,
                source.dimension_id AS DimensionId,
                source.point_type AS PointType,
                source.histogram_aggregation_id AS HistogramAggregationId,
                source.start_time_unix_nano AS SourceStartTimeUnixNano,
                e.time_unix_nano AS TimeUnixNano,
                e.exemplar_value AS ExemplarValue,
                e.span_id AS SpanId,
                e.trace_id AS TraceId
            FROM telemetry_metric_exemplars e
            JOIN telemetry_metric_points source INDEXED BY ix_telemetry_metric_points_time ON source.point_id = e.point_id
            JOIN metric_dimension_query_ranges r ON r.dimension_id = source.dimension_id
            WHERE {s_metricSourcePointRangeFilterSql}
              AND e.time_unix_nano >= r.start_time_unix_nano
              AND e.time_unix_nano <= @EndUnixNano;
            """, queryParameters, transaction).AsList();
        var pointIntervalUnixNano = dataPointInterval is { } interval ? interval.Ticks * TimeSpan.NanosecondsPerTick : (long?)null;
        // Match the SQL partition using source timestamps, independently of the displayed start.
        // A cumulative reset's representative can start at 3.5s while its SQL group starts at 3s.
        var pointIds = points.ToDictionary(
            point => GetPointKey(point.DimensionId, point.PointType, point.SourceStartTimeUnixNano, point.HistogramAggregationId),
            point => point.PointId);
        var mappedRecords = new List<(long PointId, MetricExemplarRecord Record)>();
        foreach (var record in records)
        {
            if (pointIds.TryGetValue(GetPointKey(record.DimensionId, record.PointType, record.SourceStartTimeUnixNano,
                record.HistogramAggregationId), out var pointId))
            {
                mappedRecords.Add((pointId, record));
            }
        }
        var attributes = populateExemplarAttributes
            ? MaterializeMetricExemplarAttributes(connection, transaction, mappedRecords.Select(item => item.Record.ExemplarId).Distinct().ToArray())
            : Array.Empty<OwnedAttributeRecord>().ToLookup(record => record.OwnerId);
        return mappedRecords
            .OrderBy(item => item.PointId)
            .ThenBy(item => item.Record.ExemplarId)
            .Select(item => new KeyValuePair<long, MetricsExemplar>(
                item.PointId,
                new MetricsExemplar
                {
                    TimeUnixNano = checked((ulong)item.Record.TimeUnixNano),
                    Value = item.Record.ExemplarValue,
                    SpanId = item.Record.SpanId,
                    TraceId = item.Record.TraceId,
                    Attributes = attributes[item.Record.ExemplarId].Select(attribute => KeyValuePair.Create(attribute.AttributeKey, attribute.AttributeValue)).ToArray()
                }))
            .ToLookup(pair => pair.Key, pair => pair.Value);
 
        MetricPointKey GetPointKey(long dimensionId, int pointType, long sourceStartTimeUnixNano, long? aggregationId)
        {
            var groupStartTimeUnixNano = pointIntervalUnixNano is { } intervalUnixNano
                ? (sourceStartTimeUnixNano / intervalUnixNano) * intervalUnixNano
                : sourceStartTimeUnixNano;
            return new MetricPointKey(dimensionId, pointType, groupStartTimeUnixNano, aggregationId);
        }
    }
 
    private static ILookup<long, OwnedAttributeRecord> MaterializeMetricExemplarAttributes(
        SqliteConnection connection,
        SqliteTransaction transaction,
        IReadOnlyList<long> exemplarIds)
    {
        var attributes = new List<OwnedAttributeRecord>();
        foreach (var exemplarIdBatch in exemplarIds.Chunk(MaxMetricReadBatchSize))
        {
            attributes.AddRange(connection.Query<OwnedAttributeRecord>("""
                SELECT exemplar_id AS OwnerId, attribute_key AS AttributeKey, attribute_value AS AttributeValue
                FROM telemetry_metric_exemplar_attributes
                WHERE exemplar_id IN @ExemplarIds
                ORDER BY exemplar_id, ordinal;
                """, new { ExemplarIds = exemplarIdBatch }, transaction));
        }
        return attributes.ToLookup(record => record.OwnerId);
    }
 
    internal sealed class MetricPointDataRecord : MetricPointRecord
    {
        public required long DimensionId { get; init; }
        public required long StartTimeUnixNano { get; init; }
        public required long SourceStartTimeUnixNano { get; init; }
        public required long RepeatCount { get; init; }
        public double? HistogramSum { get; init; }
        public long? HistogramAggregationStartUnixNano { get; init; }
        public long? HistogramAggregationId { get; init; }
        public int AggregationTemporality { get; init; }
        public byte[]? BucketCounts { get; init; }
        public byte[]? ExplicitBounds { get; init; }
    }
 
    internal sealed class MetricDimensionAttributeRecord
    {
        public required long DimensionId { get; init; }
        public required long InstrumentId { get; init; }
        public string? AttributeKey { get; init; }
        public string? AttributeValue { get; init; }
    }
 
    internal sealed class MetricExemplarRecord
    {
        public required long ExemplarId { get; init; }
        public required long DimensionId { get; init; }
        public required int PointType { get; init; }
        public long? HistogramAggregationId { get; init; }
        public required long SourceStartTimeUnixNano { get; init; }
        public required long TimeUnixNano { get; init; }
        public required double ExemplarValue { get; init; }
        public required string SpanId { get; init; }
        public required string TraceId { get; init; }
    }
 
    private sealed record StoredMetricDimension(long DimensionId, DimensionScope Scope);
 
    private readonly record struct MetricPointKey(long DimensionId, int PointType, long StartTimeUnixNano, long? HistogramAggregationId);
}