-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathFixedWidthCodec.cs
More file actions
177 lines (155 loc) · 6.54 KB
/
Copy pathFixedWidthCodec.cs
File metadata and controls
177 lines (155 loc) · 6.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// <copyright file="FixedWidthCodec.cs" company="MPCoreDeveloper">
// Copyright (c) 2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace SharpCoreDB.DataStructures;
using System.Buffers.Binary;
using System.Collections.Generic;
/// <summary>
/// Shared fixed-width record codec (out-of-line overflow model). Every column occupies a constant
/// slot in the record's fixed part: fixed-size columns store <c>[null-flag(1)][payload]</c> inline,
/// variable-length columns (String / Blob) store a 5-byte slot <c>[null-flag(1)][arena-offset(4)]</c>
/// referencing a block in the overflow arena. Used by both the directory-mode <see cref="Table"/>
/// and the single-file (<c>.scdb</c>) table so the two storage modes share one record format.
/// </summary>
public static class FixedWidthCodec
{
/// <summary>Serializes a row dictionary into a fixed-width record (variable values → arena).</summary>
public static byte[] SerializeRow(
Dictionary<string, object> row,
IReadOnlyList<string> columns,
IReadOnlyList<DataType> types,
FixedWidthRecordLayout layout,
IOverflowArena arena)
{
var buffer = new byte[layout.FixedSize];
var span = buffer.AsSpan();
for (int i = 0; i < columns.Count; i++)
{
var value = row.TryGetValue(columns[i], out var v) ? v : DBNull.Value;
WriteSlot(span.Slice(layout.Offsets[i], layout.SlotSizes[i]), layout.IsVariable[i], types[i], value, arena);
}
return buffer;
}
/// <summary>Serializes a column-ordered object[] row (full table column order) with the fixed-width codec.</summary>
public static byte[] SerializeRow(
object[] row,
IReadOnlyList<DataType> types,
FixedWidthRecordLayout layout,
IOverflowArena arena)
{
var buffer = new byte[layout.FixedSize];
var span = buffer.AsSpan();
for (int i = 0; i < row.Length && i < types.Count; i++)
{
WriteSlot(span.Slice(layout.Offsets[i], layout.SlotSizes[i]), layout.IsVariable[i], types[i], row[i], arena);
}
return buffer;
}
/// <summary>
/// Writes one column value into its fixed-width slot: variable-length types store an out-of-line
/// arena block reference, fixed-size types store their payload inline.
/// </summary>
private static void WriteSlot(Span<byte> slot, bool isVariable, DataType type, object? value, IOverflowArena arena)
{
if (isVariable)
{
if (value == null || value == DBNull.Value)
{
slot[0] = 0;
BinaryPrimitives.WriteInt32LittleEndian(slot[1..], 0);
return;
}
var payload = Table.EncodeVariablePayload(type, value);
var offset = arena.Write(payload);
slot[0] = 1;
BinaryPrimitives.WriteInt32LittleEndian(slot[1..], (int)offset);
return;
}
_ = Table.WriteTypedValueToSpan(slot, value, type);
}
/// <summary>Deserializes a fixed-width record into a row dictionary (variable values ← arena).</summary>
public static Dictionary<string, object> DeserializeRow(
ReadOnlySpan<byte> data,
IReadOnlyList<string> columns,
IReadOnlyList<DataType> types,
FixedWidthRecordLayout layout,
IOverflowArena arena)
{
var row = new Dictionary<string, object>(columns.Count, System.StringComparer.Ordinal);
for (int i = 0; i < columns.Count; i++)
{
if (layout.Offsets[i] + layout.SlotSizes[i] > data.Length)
{
break; // truncated / corrupt record
}
var slot = data.Slice(layout.Offsets[i], layout.SlotSizes[i]);
if (layout.IsVariable[i])
{
if (slot[0] == 0)
{
row[columns[i]] = DBNull.Value;
}
else
{
var offset = BinaryPrimitives.ReadInt32LittleEndian(slot[1..]);
var payload = arena.Read(offset);
row[columns[i]] = payload is null ? DBNull.Value : Table.DecodeVariablePayload(types[i], payload);
}
}
else
{
row[columns[i]] = Table.ReadTypedValueFromSpan(slot, types[i], out _);
}
}
return row;
}
/// <summary>Collects the arena offsets referenced by a fixed-width record's variable slots.</summary>
public static void CollectVariableOffsets(byte[] record, FixedWidthRecordLayout layout, HashSet<long> live)
{
for (int i = 0; i < layout.ColumnCount; i++)
{
if (!layout.IsVariable[i])
{
continue;
}
var slot = layout.Offsets[i];
if (slot + 5 > record.Length || record[slot] == 0)
{
continue; // truncated or null slot
}
var blockOffset = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(slot + 1, 4));
// NOTE: offset 0 is a valid block offset (first arena block) — the flag byte above
// already excluded NULL slots, so collect every referenced offset unconditionally.
live.Add(blockOffset);
}
}
/// <summary>
/// Returns a copy of a fixed-width record with its variable slots re-pointed through the
/// compaction mapping, or null when no slot moved.
/// </summary>
public static byte[]? RepointVariableSlots(byte[] record, FixedWidthRecordLayout layout, Dictionary<long, long> mapping)
{
byte[]? result = null;
for (int i = 0; i < layout.ColumnCount; i++)
{
if (!layout.IsVariable[i])
{
continue;
}
var slot = layout.Offsets[i];
if (slot + 5 > record.Length || record[slot] == 0)
{
continue;
}
var blockOffset = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(slot + 1, 4));
// NOTE: offset 0 is a valid block offset (first arena block) — re-point it like any other.
if (mapping.TryGetValue(blockOffset, out var newOffset) && newOffset != blockOffset)
{
result ??= (byte[])record.Clone();
BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(slot + 1, 4), (int)newOffset);
}
}
return result;
}
}