chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,35 @@
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
/// <summary>CRC-32C(Castagnoli),RawChunk 的完整性校验。</summary>
|
||||
public static class Crc32c
|
||||
{
|
||||
private static readonly uint[] s_table = BuildTable();
|
||||
|
||||
public static uint Compute(ReadOnlySpan<byte> data)
|
||||
{
|
||||
uint crc = uint.MaxValue;
|
||||
foreach (byte b in data)
|
||||
{
|
||||
crc = s_table[(crc ^ b) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
private static uint[] BuildTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
for (uint i = 0; i < 256; i++)
|
||||
{
|
||||
uint c = i;
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
c = (c & 1) != 0 ? (c >> 1) ^ 0x82F63B78u : c >> 1;
|
||||
}
|
||||
|
||||
table[i] = c;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
/// <summary>Agent 单调时钟与 UTC 的锚点,以及 Probe 时钟相对 Agent 时钟的偏移估计。</summary>
|
||||
public sealed class ChunkClock
|
||||
{
|
||||
[JsonPropertyName("agent_mono_us")]
|
||||
public long AgentMonoUs { get; set; }
|
||||
|
||||
[JsonPropertyName("utc_ms")]
|
||||
public long UtcMs { get; set; }
|
||||
|
||||
/// <summary>Agent 时钟 − Probe 时钟(微秒)。</summary>
|
||||
[JsonPropertyName("probe_offset_us")]
|
||||
public long ProbeOffsetUs { get; set; }
|
||||
|
||||
/// <summary>Probe 相对 Agent 的时钟漂移(ppb,十亿分之一)。</summary>
|
||||
[JsonPropertyName("probe_drift_ppb")]
|
||||
public long ProbeDriftPpb { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ColumnHeader
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
/// <summary>u8 | u16 | u32 | u64 | i32 | i64</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
/// <summary>plain | delta</summary>
|
||||
[JsonPropertyName("enc")]
|
||||
public string Enc { get; set; } = "plain";
|
||||
|
||||
[JsonPropertyName("nullable")]
|
||||
public bool Nullable { get; set; }
|
||||
|
||||
/// <summary>ts_probe | ts_agent | ts_local,其他为空。</summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string? Role { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StreamHeader
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("count")]
|
||||
public int Count { get; set; }
|
||||
|
||||
[JsonPropertyName("columns")]
|
||||
public List<ColumnHeader> Columns { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>块头(JSON,自描述):列定义写在块里,新增列或新增 Kind 不需要升级 container_version。</summary>
|
||||
public sealed class RawChunkHeader
|
||||
{
|
||||
[JsonPropertyName("chunk_id")]
|
||||
public ChunkId ChunkId { get; set; }
|
||||
|
||||
[JsonPropertyName("container_version")]
|
||||
public int ContainerVersion { get; set; } = ProtocolVersion.Container;
|
||||
|
||||
[JsonPropertyName("kind")]
|
||||
public string Kind { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("task_id")]
|
||||
public TaskId? TaskId { get; set; }
|
||||
|
||||
[JsonPropertyName("test_id")]
|
||||
public TestId? TestId { get; set; }
|
||||
|
||||
[JsonPropertyName("phase")]
|
||||
public int? Phase { get; set; }
|
||||
|
||||
[JsonPropertyName("agent_id")]
|
||||
public NodeId AgentId { get; set; }
|
||||
|
||||
[JsonPropertyName("probe_id")]
|
||||
public ProbeId? ProbeId { get; set; }
|
||||
|
||||
[JsonPropertyName("target")]
|
||||
public string? Target { get; set; }
|
||||
|
||||
/// <summary>参数的完整快照(包含默认值)。</summary>
|
||||
[JsonPropertyName("params")]
|
||||
public Dictionary<string, JsonElement> Params { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int Port { get; set; }
|
||||
|
||||
[JsonPropertyName("direction")]
|
||||
public string? Direction { get; set; }
|
||||
|
||||
[JsonPropertyName("start_utc_ms")]
|
||||
public long StartUtcMs { get; set; }
|
||||
|
||||
[JsonPropertyName("duration_ms")]
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
/// <summary>Probe 一侧的列没有在宽限期内送达:只有 Agent 一侧的数据。</summary>
|
||||
[JsonPropertyName("partial")]
|
||||
public bool Partial { get; set; }
|
||||
|
||||
[JsonPropertyName("clock")]
|
||||
public ChunkClock Clock { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("streams")]
|
||||
public List<StreamHeader> Streams { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// "line:<probe_id>" 或 "target:<task_id>":存储和分析用的序列标识。
|
||||
/// 一条线路是一对(Probe,Agent):同一个 Agent 上每个 Probe 的数据各是一个序列,互不混合。
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Series => TaskId is { } id && Target is not null ? "target:" + id.Value : LinePrefix + (ProbeId?.Value ?? "");
|
||||
|
||||
public const string LinePrefix = "line:";
|
||||
}
|
||||
|
||||
public sealed class RawColumn
|
||||
{
|
||||
public RawColumn(string name, ColumnType type, ColumnEnc enc, bool nullable, ColumnRole role, long[] values, bool[]? valid)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Enc = enc;
|
||||
Nullable = nullable;
|
||||
Role = role;
|
||||
Values = values;
|
||||
Valid = valid;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public ColumnType Type { get; }
|
||||
|
||||
public ColumnEnc Enc { get; }
|
||||
|
||||
public bool Nullable { get; }
|
||||
|
||||
public ColumnRole Role { get; }
|
||||
|
||||
/// <summary>所有列统一用 long 存放(u64 按位保存)。缺失值对应的位置内容无意义,请先看 <see cref="IsValid"/>。</summary>
|
||||
public long[] Values { get; }
|
||||
|
||||
public bool[]? Valid { get; }
|
||||
|
||||
public int Count => Values.Length;
|
||||
|
||||
public bool IsValid(int row) => Valid is null || Valid[row];
|
||||
}
|
||||
|
||||
public sealed class RawStream
|
||||
{
|
||||
public RawStream(string name, int count, IReadOnlyList<RawColumn> columns)
|
||||
{
|
||||
Name = name;
|
||||
Count = count;
|
||||
Columns = columns;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public IReadOnlyList<RawColumn> Columns { get; }
|
||||
|
||||
public RawColumn? Find(string name)
|
||||
{
|
||||
foreach (RawColumn column in Columns)
|
||||
{
|
||||
if (column.Name == name)
|
||||
{
|
||||
return column;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RawChunk
|
||||
{
|
||||
public RawChunk(RawChunkHeader header, IReadOnlyList<RawStream> streams)
|
||||
{
|
||||
Header = header;
|
||||
Streams = streams;
|
||||
}
|
||||
|
||||
public RawChunkHeader Header { get; }
|
||||
|
||||
public IReadOnlyList<RawStream> Streams { get; }
|
||||
|
||||
public RawStream? Find(string name)
|
||||
{
|
||||
foreach (RawStream stream in Streams)
|
||||
{
|
||||
if (stream.Name == name)
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// RawChunk 的编解码(Probe、Agent、Backend、离线工具共用同一份代码)。
|
||||
/// 布局:<c>"MNRC" | u8 container_version | u32 crc32c(压缩数据) | Brotli( u32 header_len | header JSON | 各列数据 )</c>。
|
||||
/// 列数据:可空列先写有效位图,再写有效值;plain 为无符号 varint(有符号类型用 zigzag),delta 为与上一个有效值之差的 zigzag varint。
|
||||
/// </summary>
|
||||
public static class RawChunkCodec
|
||||
{
|
||||
private const int PrefixBytes = 4 + 1 + 4;
|
||||
|
||||
private static ReadOnlySpan<byte> Magic => "MNRC"u8;
|
||||
|
||||
/// <summary>压缩之前的正文:<c>u32 header_len | header JSON | 各列数据</c>。布局变化时黄金字节测试会失败,提醒升级版本。</summary>
|
||||
public static byte[] EncodeBody(RawChunk chunk)
|
||||
{
|
||||
RawChunkHeader header = chunk.Header;
|
||||
header.ContainerVersion = ProtocolVersion.Container;
|
||||
header.Streams = [];
|
||||
foreach (RawStream stream in chunk.Streams)
|
||||
{
|
||||
var streamHeader = new StreamHeader { Name = stream.Name, Count = stream.Count };
|
||||
foreach (RawColumn column in stream.Columns)
|
||||
{
|
||||
streamHeader.Columns.Add(new ColumnHeader
|
||||
{
|
||||
Name = column.Name,
|
||||
Type = TypeName(column.Type),
|
||||
Enc = column.Enc == ColumnEnc.Delta ? "delta" : "plain",
|
||||
Nullable = column.Nullable,
|
||||
Role = RoleName(column.Role),
|
||||
});
|
||||
}
|
||||
|
||||
header.Streams.Add(streamHeader);
|
||||
}
|
||||
|
||||
byte[] headerJson = JsonSerializer.SerializeToUtf8Bytes(header, MonitorJsonContext.Default.RawChunkHeader);
|
||||
|
||||
using var body = new MemoryStream();
|
||||
Span<byte> length = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(length, (uint)headerJson.Length);
|
||||
body.Write(length);
|
||||
body.Write(headerJson);
|
||||
foreach (RawStream stream in chunk.Streams)
|
||||
{
|
||||
foreach (RawColumn column in stream.Columns)
|
||||
{
|
||||
WriteColumn(body, column);
|
||||
}
|
||||
}
|
||||
|
||||
return body.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] Encode(RawChunk chunk)
|
||||
{
|
||||
byte[] plain = EncodeBody(chunk);
|
||||
using var compressed = new MemoryStream();
|
||||
using (var brotli = new BrotliStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
|
||||
{
|
||||
brotli.Write(plain);
|
||||
}
|
||||
|
||||
byte[] payload = compressed.ToArray();
|
||||
byte[] result = new byte[PrefixBytes + payload.Length];
|
||||
Magic.CopyTo(result);
|
||||
result[4] = ProtocolVersion.Container;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(5), Crc32c.Compute(payload));
|
||||
payload.CopyTo(result, PrefixBytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result<RawChunk> Decode(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < PrefixBytes || !data[..4].SequenceEqual(Magic))
|
||||
{
|
||||
return Corrupt("bad magic");
|
||||
}
|
||||
|
||||
if (data[4] != ProtocolVersion.Container)
|
||||
{
|
||||
return Corrupt($"unsupported container version {data[4]}");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> payload = data[PrefixBytes..];
|
||||
if (BinaryPrimitives.ReadUInt32LittleEndian(data[5..]) != Crc32c.Compute(payload))
|
||||
{
|
||||
return Corrupt("crc mismatch");
|
||||
}
|
||||
|
||||
byte[]? plain = Inflate(payload);
|
||||
if (plain is null)
|
||||
{
|
||||
return Corrupt("invalid or oversized compressed data");
|
||||
}
|
||||
|
||||
if (plain.Length < 4)
|
||||
{
|
||||
return Corrupt("truncated header length");
|
||||
}
|
||||
|
||||
uint headerLength = BinaryPrimitives.ReadUInt32LittleEndian(plain);
|
||||
if (headerLength > plain.Length - 4)
|
||||
{
|
||||
return Corrupt("header length exceeds data");
|
||||
}
|
||||
|
||||
RawChunkHeader? header;
|
||||
try
|
||||
{
|
||||
header = JsonSerializer.Deserialize(plain.AsSpan(4, (int)headerLength), MonitorJsonContext.Default.RawChunkHeader);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Corrupt("header is not valid json");
|
||||
}
|
||||
|
||||
if (header is null || header.Streams.Count > ProtocolLimits.MaxStreamsPerChunk)
|
||||
{
|
||||
return Corrupt("header is missing or has too many streams");
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> columns = plain.AsSpan(4 + (int)headerLength);
|
||||
var streams = new List<RawStream>(header.Streams.Count);
|
||||
foreach (StreamHeader streamHeader in header.Streams)
|
||||
{
|
||||
if (streamHeader.Count is < 0 or > ProtocolLimits.MaxRecordsPerChunk || streamHeader.Columns.Count > ProtocolLimits.MaxColumnsPerStream)
|
||||
{
|
||||
return Corrupt($"stream {streamHeader.Name} has an invalid size");
|
||||
}
|
||||
|
||||
var cols = new List<RawColumn>(streamHeader.Columns.Count);
|
||||
foreach (ColumnHeader columnHeader in streamHeader.Columns)
|
||||
{
|
||||
if (!TryParseType(columnHeader.Type, out ColumnType type) || !TryParseEnc(columnHeader.Enc, out ColumnEnc enc))
|
||||
{
|
||||
return Corrupt($"column {columnHeader.Name} has an unknown type or encoding");
|
||||
}
|
||||
|
||||
RawColumn? column = ReadColumn(ref columns, columnHeader, type, enc, streamHeader.Count);
|
||||
if (column is null)
|
||||
{
|
||||
return Corrupt($"column {streamHeader.Name}.{columnHeader.Name} is truncated or out of range");
|
||||
}
|
||||
|
||||
cols.Add(column);
|
||||
}
|
||||
|
||||
streams.Add(new RawStream(streamHeader.Name, streamHeader.Count, cols));
|
||||
}
|
||||
|
||||
return columns.Length == 0 ? Result<RawChunk>.Ok(new RawChunk(header, streams)) : Corrupt("trailing bytes");
|
||||
}
|
||||
|
||||
private static Result<RawChunk> Corrupt(string message) =>
|
||||
Result<RawChunk>.Fail(MonitorErrors.RawCorrupt, message);
|
||||
|
||||
private static byte[]? Inflate(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
using var input = new MemoryStream(payload.ToArray());
|
||||
using var brotli = new BrotliStream(input, CompressionMode.Decompress);
|
||||
using var output = new MemoryStream();
|
||||
byte[] buffer = new byte[16 * 1024];
|
||||
try
|
||||
{
|
||||
int read;
|
||||
while ((read = brotli.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
output.Write(buffer, 0, read);
|
||||
if (output.Length > ProtocolLimits.MaxDecodedChunkBytes)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteColumn(MemoryStream body, RawColumn column)
|
||||
{
|
||||
int count = column.Count;
|
||||
if (column.Nullable)
|
||||
{
|
||||
byte[] bitmap = new byte[(count + 7) / 8];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (column.IsValid(i))
|
||||
{
|
||||
bitmap[i >> 3] |= (byte)(1 << (i & 7));
|
||||
}
|
||||
}
|
||||
|
||||
body.Write(bitmap);
|
||||
}
|
||||
|
||||
bool signed = column.Type is ColumnType.I32 or ColumnType.I64;
|
||||
long previous = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!column.IsValid(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long value = column.Values[i];
|
||||
if (column.Enc == ColumnEnc.Delta)
|
||||
{
|
||||
VarInt.Write(body, VarInt.ZigZag(value - previous));
|
||||
previous = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
VarInt.Write(body, signed ? VarInt.ZigZag(value) : (ulong)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static RawColumn? ReadColumn(ref ReadOnlySpan<byte> data, ColumnHeader header, ColumnType type, ColumnEnc enc, int count)
|
||||
{
|
||||
bool[]? valid = null;
|
||||
if (header.Nullable)
|
||||
{
|
||||
int bitmapBytes = (count + 7) / 8;
|
||||
if (data.Length < bitmapBytes)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
valid = new bool[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
valid[i] = (data[i >> 3] & (1 << (i & 7))) != 0;
|
||||
}
|
||||
|
||||
data = data[bitmapBytes..];
|
||||
}
|
||||
|
||||
bool signed = type is ColumnType.I32 or ColumnType.I64;
|
||||
long[] values = new long[count];
|
||||
long previous = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (valid is not null && !valid[i])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!VarInt.TryRead(data, out ulong raw, out int consumed))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
data = data[consumed..];
|
||||
long value;
|
||||
if (enc == ColumnEnc.Delta)
|
||||
{
|
||||
value = previous + VarInt.UnZigZag(raw);
|
||||
previous = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = signed ? VarInt.UnZigZag(raw) : unchecked((long)raw);
|
||||
}
|
||||
|
||||
if (!InRange(type, value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
values[i] = value;
|
||||
}
|
||||
|
||||
return new RawColumn(header.Name, type, enc, header.Nullable, ParseRole(header.Role), values, valid);
|
||||
}
|
||||
|
||||
private static bool InRange(ColumnType type, long value) => type switch
|
||||
{
|
||||
ColumnType.U8 => value is >= 0 and <= byte.MaxValue,
|
||||
ColumnType.U16 => value is >= 0 and <= ushort.MaxValue,
|
||||
ColumnType.U32 => value is >= 0 and <= uint.MaxValue,
|
||||
ColumnType.I32 => value is >= int.MinValue and <= int.MaxValue,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
public static string TypeName(ColumnType type) => type switch
|
||||
{
|
||||
ColumnType.U8 => "u8",
|
||||
ColumnType.U16 => "u16",
|
||||
ColumnType.U32 => "u32",
|
||||
ColumnType.U64 => "u64",
|
||||
ColumnType.I32 => "i32",
|
||||
_ => "i64",
|
||||
};
|
||||
|
||||
private static bool TryParseType(string text, out ColumnType type)
|
||||
{
|
||||
switch (text)
|
||||
{
|
||||
case "u8": type = ColumnType.U8; return true;
|
||||
case "u16": type = ColumnType.U16; return true;
|
||||
case "u32": type = ColumnType.U32; return true;
|
||||
case "u64": type = ColumnType.U64; return true;
|
||||
case "i32": type = ColumnType.I32; return true;
|
||||
case "i64": type = ColumnType.I64; return true;
|
||||
default: type = default; return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseEnc(string text, out ColumnEnc enc)
|
||||
{
|
||||
switch (text)
|
||||
{
|
||||
case "plain": enc = ColumnEnc.Plain; return true;
|
||||
case "delta": enc = ColumnEnc.Delta; return true;
|
||||
default: enc = default; return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? RoleName(ColumnRole role) => role switch
|
||||
{
|
||||
ColumnRole.TsProbe => "ts_probe",
|
||||
ColumnRole.TsAgent => "ts_agent",
|
||||
ColumnRole.TsLocal => "ts_local",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static ColumnRole ParseRole(string? text) => text switch
|
||||
{
|
||||
"ts_probe" => ColumnRole.TsProbe,
|
||||
"ts_agent" => ColumnRole.TsAgent,
|
||||
"ts_local" => ColumnRole.TsLocal,
|
||||
_ => ColumnRole.None,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Monitor.Protocol;
|
||||
|
||||
/// <summary>按 <see cref="StreamSpec"/> 逐行构造一个 <see cref="RawStream"/>(用于测试、模拟器和离线工具,不用于节点热路径)。</summary>
|
||||
public sealed class StreamBuilder
|
||||
{
|
||||
private readonly StreamSpec _spec;
|
||||
private readonly List<long>[] _values;
|
||||
private readonly List<bool>[] _valid;
|
||||
|
||||
public StreamBuilder(StreamSpec spec)
|
||||
{
|
||||
_spec = spec;
|
||||
_values = new List<long>[spec.Columns.Count];
|
||||
_valid = new List<bool>[spec.Columns.Count];
|
||||
for (int i = 0; i < _values.Length; i++)
|
||||
{
|
||||
_values[i] = [];
|
||||
_valid[i] = [];
|
||||
}
|
||||
}
|
||||
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>添加一行;<c>null</c> 表示缺失值(只允许用于可空列)。</summary>
|
||||
public StreamBuilder Add(params long?[] row)
|
||||
{
|
||||
if (row.Length != _values.Length)
|
||||
{
|
||||
throw new ArgumentException($"stream {_spec.Name} expects {_values.Length} values, got {row.Length}", nameof(row));
|
||||
}
|
||||
|
||||
for (int i = 0; i < row.Length; i++)
|
||||
{
|
||||
if (row[i] is null && !_spec.Columns[i].Nullable)
|
||||
{
|
||||
throw new ArgumentException($"column {_spec.Columns[i].Name} is not nullable", nameof(row));
|
||||
}
|
||||
|
||||
_values[i].Add(row[i] ?? 0);
|
||||
_valid[i].Add(row[i] is not null);
|
||||
}
|
||||
|
||||
Count++;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RawStream Build()
|
||||
{
|
||||
var columns = new List<RawColumn>(_values.Length);
|
||||
for (int i = 0; i < _values.Length; i++)
|
||||
{
|
||||
ColumnSpec spec = _spec.Columns[i];
|
||||
columns.Add(new RawColumn(spec.Name, spec.Type, spec.Enc, spec.Nullable, spec.Role, [.. _values[i]], spec.Nullable ? [.. _valid[i]] : null));
|
||||
}
|
||||
|
||||
return new RawStream(_spec.Name, Count, columns);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user