lib/sql/src/row.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const simd = @import("simd");
  3 
  4 const Bytes = simd.ScalableTag(u8);
  5 
  6 pub const Error = error{
  7     ColumnOutOfBounds,
  8     InvalidRow,
  9     OutputTooSmall,
 10     RowTooLarge,
 11 };
 12 
 13 pub const Storage = enum {
 14     nil,
 15     integer,
 16     text,
 17     blob,
 18 };
 19 
 20 pub const Collation = enum {
 21     binary,
 22     nocase,
 23     rtrim,
 24 };
 25 
 26 pub const Column = struct {
 27     collation: Collation = .binary,
 28 };
 29 
 30 pub fn Spec(comptime ColumnTag: type) type {
 31     @setEvalBranchQuota(100_000);
 32     const enum_info = switch (@typeInfo(ColumnTag)) {
 33         .@"enum" => |info| info,
 34         else => @compileError("row spec columns must be an enum"),
 35     };
 36     if (enum_info.field_names.len == 0) @compileError("row spec must contain a column");
 37     const columns = std.meta.tags(ColumnTag);
 38     return SpecColumns(ColumnTag, columns[0..]);
 39 }
 40 
 41 fn SpecColumns(comptime ColumnTag: type, comptime selected: []const ColumnTag) type {
 42     @setEvalBranchQuota(100_000);
 43     validateSpecColumns(ColumnTag, selected);
 44     return struct {
 45         pub const Column: type = ColumnTag;
 46         pub const Indices: type = SpecIndices(ColumnTag, selected);
 47         pub const column_count = selected.len;
 48         pub const columns = columnList(ColumnTag, selected, 0);
 49 
 50         pub fn Project(comptime projection: []const ColumnTag) type {
 51             return projectSpec(ColumnTag, selected, projection);
 52         }
 53 
 54         pub fn parameters(comptime prefix_count: usize) []const u8 {
 55             return specParameters(column_count, prefix_count);
 56         }
 57 
 58         pub fn assignments(comptime prefix_count: usize) []const u8 {
 59             return specAssignments(ColumnTag, selected, prefix_count);
 60         }
 61 
 62         pub fn index(comptime column: ColumnTag) usize {
 63             return specIndex(ColumnTag, selected, column);
 64         }
 65 
 66         pub fn at(base: usize, comptime column: ColumnTag) usize {
 67             return specAt(ColumnTag, selected, base, column);
 68         }
 69 
 70         pub fn atBase(base: usize) Indices {
 71             return specAtBase(Indices, column_count, base);
 72         }
 73 
 74         pub fn parameter(comptime prefix_count: usize, comptime column: ColumnTag) usize {
 75             return specParameter(ColumnTag, selected, prefix_count, column);
 76         }
 77 
 78         pub fn indices(
 79             comptime base: usize,
 80             comptime projection: []const ColumnTag,
 81         ) [projection.len]usize {
 82             return specIndices(ColumnTag, selected, base, projection);
 83         }
 84 
 85         pub fn assertStruct(comptime Record: type) void {
 86             specAssertStruct(ColumnTag, selected, Record);
 87         }
 88 
 89         pub fn assertStructWidth(comptime Record: type, comptime extra_fields: usize) void {
 90             specAssertStructWidth(column_count, Record, extra_fields);
 91         }
 92 
 93         pub fn matches(view: View, base: usize) bool {
 94             return specMatches(column_count, view, base);
 95         }
 96     };
 97 }
 98 
 99 fn SpecIndices(comptime ColumnTag: type, comptime selected: []const ColumnTag) type {
100     return struct {
101         base: usize,
102 
103         pub fn at(self: @This(), comptime column: ColumnTag) usize {
104             return specAt(ColumnTag, selected, self.base, column);
105         }
106     };
107 }
108 
109 fn projectSpec(
110     comptime ColumnTag: type,
111     comptime selected: []const ColumnTag,
112     comptime projection: []const ColumnTag,
113 ) type {
114     @setEvalBranchQuota(100_000);
115     for (projection) |column| {
116         if (!containsColumn(ColumnTag, selected, column)) {
117             @compileError(std.fmt.comptimePrint(
118                 "row projection contains unknown column {s}",
119                 .{@tagName(column)},
120             ));
121         }
122     }
123     return SpecColumns(ColumnTag, projection);
124 }
125 
126 fn specParameters(comptime column_count: usize, comptime prefix_count: usize) []const u8 {
127     @setEvalBranchQuota(100_000);
128     return comptime parameterList(prefixedCount(prefix_count, column_count), 1);
129 }
130 
131 fn specAssignments(
132     comptime ColumnTag: type,
133     comptime selected: []const ColumnTag,
134     comptime prefix_count: usize,
135 ) []const u8 {
136     @setEvalBranchQuota(100_000);
137     _ = prefixedCount(prefix_count, selected.len);
138     return comptime assignmentList(ColumnTag, selected, prefix_count, 0);
139 }
140 
141 fn specIndex(
142     comptime ColumnTag: type,
143     comptime selected: []const ColumnTag,
144     comptime column: ColumnTag,
145 ) usize {
146     inline for (selected, 0..) |candidate, ordinal| {
147         if (candidate == column) return ordinal;
148     }
149     @compileError(std.fmt.comptimePrint(
150         "row spec does not contain column {s}",
151         .{@tagName(column)},
152     ));
153 }
154 
155 fn specAt(
156     comptime ColumnTag: type,
157     comptime selected: []const ColumnTag,
158     base: usize,
159     comptime column: ColumnTag,
160 ) usize {
161     const relative = specIndex(ColumnTag, selected, column);
162     std.debug.assert(base <= std.math.maxInt(usize) - relative);
163     return base + relative;
164 }
165 
166 fn specAtBase(comptime Indices: type, column_count: usize, base: usize) Indices {
167     std.debug.assert(column_count > 0);
168     std.debug.assert(base <= std.math.maxInt(usize) - (column_count - 1));
169     return .{ .base = base };
170 }
171 
172 fn specParameter(
173     comptime ColumnTag: type,
174     comptime selected: []const ColumnTag,
175     comptime prefix_count: usize,
176     comptime column: ColumnTag,
177 ) usize {
178     _ = prefixedCount(prefix_count, selected.len);
179     return prefix_count + specIndex(ColumnTag, selected, column) + 1;
180 }
181 
182 fn specIndices(
183     comptime ColumnTag: type,
184     comptime selected: []const ColumnTag,
185     comptime base: usize,
186     comptime projection: []const ColumnTag,
187 ) [projection.len]usize {
188     @setEvalBranchQuota(100_000);
189     const Projected = projectSpec(ColumnTag, selected, projection);
190     _ = Projected;
191     var result: [projection.len]usize = undefined;
192     inline for (projection, 0..) |column, ordinal| {
193         result[ordinal] = specAt(ColumnTag, selected, base, column);
194     }
195     return result;
196 }
197 
198 fn specAssertStruct(
199     comptime ColumnTag: type,
200     comptime selected: []const ColumnTag,
201     comptime Record: type,
202 ) void {
203     const info = structInfo(Record);
204     if (info.field_names.len != selected.len) {
205         @compileError("row spec and record field counts differ");
206     }
207     for (selected, info.field_names) |column, field_name| {
208         if (!std.mem.eql(u8, @tagName(column), field_name)) {
209             @compileError(std.fmt.comptimePrint(
210                 "row column {s} does not match record field {s}",
211                 .{ @tagName(column), field_name },
212             ));
213         }
214     }
215 }
216 
217 fn specAssertStructWidth(
218     comptime column_count: usize,
219     comptime Record: type,
220     comptime extra_fields: usize,
221 ) void {
222     const info = structInfo(Record);
223     const expected = prefixedCount(extra_fields, column_count);
224     if (info.field_names.len != expected) {
225         @compileError("row spec and record field counts differ");
226     }
227 }
228 
229 fn specMatches(column_count: usize, view: View, base: usize) bool {
230     if (base > std.math.maxInt(usize) - column_count) return false;
231     return view.columnCount() == base + column_count;
232 }
233 
234 fn structInfo(comptime Record: type) std.builtin.Type.Struct {
235     return switch (@typeInfo(Record)) {
236         .@"struct" => |info| info,
237         else => @compileError("row spec record must be a struct"),
238     };
239 }
240 
241 fn validateSpecColumns(comptime ColumnTag: type, comptime selected: []const ColumnTag) void {
242     if (selected.len == 0) @compileError("row spec must contain a column");
243     for (selected, 0..) |column, index| {
244         validateColumnName(@tagName(column));
245         for (selected[0..index]) |prior| {
246             if (prior == column) {
247                 @compileError(std.fmt.comptimePrint(
248                     "row spec contains duplicate column {s}",
249                     .{@tagName(column)},
250                 ));
251             }
252         }
253     }
254 }
255 
256 fn validateColumnName(comptime name: []const u8) void {
257     if (name.len == 0) @compileError("row spec column name is empty");
258     for (name, 0..) |byte, index| {
259         const letter = byte >= 'a' and byte <= 'z';
260         const digit = byte >= '0' and byte <= '9';
261         if (!letter and byte != '_' and (index == 0 or !digit)) {
262             @compileError(std.fmt.comptimePrint(
263                 "row spec column name is not a SQL identifier: {s}",
264                 .{name},
265             ));
266         }
267     }
268 }
269 
270 fn containsColumn(
271     comptime ColumnTag: type,
272     comptime columns: []const ColumnTag,
273     comptime needle: ColumnTag,
274 ) bool {
275     for (columns) |column| {
276         if (column == needle) return true;
277     }
278     return false;
279 }
280 
281 fn prefixedCount(comptime prefix_count: usize, comptime column_count: usize) usize {
282     if (prefix_count > std.math.maxInt(usize) - column_count) {
283         @compileError("row spec parameter count overflows usize");
284     }
285     return prefix_count + column_count;
286 }
287 
288 fn columnList(
289     comptime ColumnTag: type,
290     comptime columns: []const ColumnTag,
291     comptime index: usize,
292 ) []const u8 {
293     if (index == columns.len) return "";
294     const separator = if (index == 0) "" else ", ";
295     return separator ++ @tagName(columns[index]) ++ columnList(ColumnTag, columns, index + 1);
296 }
297 
298 fn parameterList(comptime count: usize, comptime parameter: usize) []const u8 {
299     if (parameter > count) return "";
300     const separator = if (parameter == 1) "" else ", ";
301     return separator ++ std.fmt.comptimePrint("?{d}", .{parameter}) ++
302         parameterList(count, parameter + 1);
303 }
304 
305 fn assignmentList(
306     comptime ColumnTag: type,
307     comptime columns: []const ColumnTag,
308     comptime prefix_count: usize,
309     comptime index: usize,
310 ) []const u8 {
311     if (index == columns.len) return "";
312     const separator = if (index == 0) "" else ", ";
313     const parameter = prefix_count + index + 1;
314     return separator ++ @tagName(columns[index]) ++ " = " ++
315         std.fmt.comptimePrint("?{d}", .{parameter}) ++
316         assignmentList(ColumnTag, columns, prefix_count, index + 1);
317 }
318 
319 pub const Value = union(Storage) {
320     nil,
321     integer: i64,
322     text: []const u8,
323     blob: []const u8,
324 };
325 
326 const Varint = struct {
327     value: u64,
328     len: usize,
329 };
330 
331 pub const View = struct {
332     bytes: []const u8,
333     header_len: usize,
334     header_start: usize,
335     count: usize,
336 
337     pub fn init(bytes: []const u8) Error!View {
338         const header = try readVarint(bytes);
339         if (header.value > std.math.maxInt(usize)) return error.RowTooLarge;
340         const header_len: usize = @intCast(header.value);
341         if (header_len < header.len or header_len > bytes.len) return error.InvalidRow;
342         var header_cursor = header.len;
343         var body_len: usize = 0;
344         var count: usize = 0;
345         while (header_cursor < header_len) {
346             const serial = try readVarint(bytes[header_cursor..header_len]);
347             const size = try serialBodySize(serial.value);
348             body_len = try checkedAdd(body_len, size);
349             header_cursor += serial.len;
350             count += 1;
351         }
352         if (header_cursor != header_len) return error.InvalidRow;
353         if (body_len > bytes.len - header_len) return error.InvalidRow;
354         return .{
355             .bytes = bytes,
356             .header_len = header_len,
357             .header_start = header.len,
358             .count = count,
359         };
360     }
361 
362     pub fn columnCount(self: View) usize {
363         return self.count;
364     }
365 
366     /// Returns a cursor that opens a traversal of this row at its first column,
367     /// so a caller reading more than one column of the row walks the header
368     /// once. The cursor carries the two offsets the traversal needs and calls
369     /// no allocator. The cursor reads the row's borrowed bytes as it goes, so
370     /// those bytes stay unchanged while the cursor is in use.
371     pub fn cursor(self: View) Cursor {
372         return .{
373             .view = self,
374             .header_offset = self.header_start,
375             .body_offset = self.header_len,
376         };
377     }
378 
379     pub fn column(self: View, index: usize) Error!Value {
380         var reader = self.cursor();
381         return reader.column(index);
382     }
383 
384     pub fn project(self: View, indexes: []const usize, target: []Value) Error![]Value {
385         if (target.len < indexes.len) return error.OutputTooSmall;
386         var reader = self.cursor();
387         for (indexes, 0..) |index, ordinal| {
388             target[ordinal] = try reader.column(index);
389         }
390         return target[0..indexes.len];
391     }
392 
393     pub fn compare(self: View, other: View, columns: []const Column) Error!std.math.Order {
394         const count = @max(self.count, other.count);
395         var left_cursor = self.cursor();
396         var right_cursor = other.cursor();
397         var index: usize = 0;
398         while (index < count) : (index += 1) {
399             const left: Value = if (index < self.count) try left_cursor.column(index) else .nil;
400             const right: Value = if (index < other.count) try right_cursor.column(index) else .nil;
401             const collation = if (index < columns.len) columns[index].collation else Collation.binary;
402             const order = compareValues(left, right, collation);
403             if (order != .eq) return order;
404         }
405         return .eq;
406     }
407 };
408 
409 /// A cursor is a traversal over one row's columns, of a size fixed at compile
410 /// time. A caller holds one across more than one read of the same row. The
411 /// cursor holds the row view, the offset it has reached in the header, the
412 /// offset it has reached in the bodies, and the number of columns it has
413 /// passed. Reading columns in increasing order walks forward once over each
414 /// header field it has yet to see, while asking for a column at or before the
415 /// one it has most recently returned sends the traversal back to the first
416 /// column and walks forward again, which covers asking for the same column
417 /// twice. The traversal returns `ColumnOutOfBounds` for an index beyond the
418 /// row's column count.
419 pub const Cursor = struct {
420     view: View,
421     header_offset: usize,
422     body_offset: usize,
423     ordinal: usize = 0,
424 
425     pub fn column(self: *Cursor, index: usize) Error!Value {
426         std.debug.assert(self.ordinal <= self.view.count);
427         if (index >= self.view.count) return error.ColumnOutOfBounds;
428         if (index < self.ordinal) self.* = self.view.cursor();
429         while (self.ordinal <= index) {
430             const serial = try readVarint(self.view.bytes[self.header_offset..self.view.header_len]);
431             const size = try serialBodySize(serial.value);
432             const start = self.body_offset;
433             const end = try checkedAdd(start, size);
434             if (end > self.view.bytes.len) return error.InvalidRow;
435             self.header_offset += serial.len;
436             self.body_offset = end;
437             const ordinal = self.ordinal;
438             self.ordinal += 1;
439             if (ordinal == index) return try decode(serial.value, self.view.bytes[start..end]);
440         }
441         unreachable;
442     }
443 };
444 
445 pub fn encodedSize(values: []const Value) Error!usize {
446     var serial_len: usize = 0;
447     var body_len: usize = 0;
448     for (values) |value| {
449         const serial = try serialType(value);
450         serial_len = try checkedAdd(serial_len, varintSize(serial));
451         body_len = try checkedAdd(body_len, try valueBodySize(value));
452     }
453     const header_len = headerLength(serial_len);
454     return try checkedAdd(header_len, body_len);
455 }
456 
457 pub fn encode(target: []u8, values: []const Value) Error![]const u8 {
458     const size = try encodedSize(values);
459     if (target.len < size) return error.OutputTooSmall;
460     var serial_len: usize = 0;
461     for (values) |value| serial_len += varintSize(try serialType(value));
462     const header_len = headerLength(serial_len);
463     var cursor = try writeVarint(target, @intCast(header_len));
464     for (values) |value| cursor += try writeVarint(target[cursor..], try serialType(value));
465     if (cursor != header_len) return error.InvalidRow;
466     for (values) |value| {
467         const written = try writeBody(target[cursor..size], value);
468         cursor += written;
469     }
470     if (cursor != size) return error.InvalidRow;
471     return target[0..size];
472 }
473 
474 pub fn compare(left: []const u8, right: []const u8, columns: []const Column) Error!std.math.Order {
475     return try (try View.init(left)).compare(try View.init(right), columns);
476 }
477 
478 pub fn compareValues(left: Value, right: Value, collation: Collation) std.math.Order {
479     const left_rank = storageRank(std.meta.activeTag(left));
480     const right_rank = storageRank(std.meta.activeTag(right));
481     if (left_rank < right_rank) return .lt;
482     if (left_rank > right_rank) return .gt;
483     return switch (left) {
484         .nil => .eq,
485         .integer => |left_integer| switch (right) {
486             .integer => |right_integer| compareInteger(left_integer, right_integer),
487             else => unreachable,
488         },
489         .text => |left_text| switch (right) {
490             .text => |right_text| compareText(left_text, right_text, collation),
491             else => unreachable,
492         },
493         .blob => |left_blob| switch (right) {
494             .blob => |right_blob| simd.order(Bytes, left_blob, right_blob),
495             else => unreachable,
496         },
497     };
498 }
499 
500 fn headerLength(serial_len: usize) usize {
501     var header_len = serial_len + 1;
502     while (true) {
503         const next = serial_len + varintSize(@intCast(header_len));
504         if (next == header_len) return header_len;
505         header_len = next;
506     }
507 }
508 
509 fn serialType(value: Value) Error!u64 {
510     return switch (value) {
511         .nil => 0,
512         .integer => |integer| integerSerial(integer),
513         .text => |text| try sizedSerial(text.len, 13),
514         .blob => |blob| try sizedSerial(blob.len, 12),
515     };
516 }
517 
518 fn sizedSerial(len: usize, base: u64) Error!u64 {
519     if (len > (std.math.maxInt(u64) - base) / 2) return error.RowTooLarge;
520     return @as(u64, @intCast(len)) * 2 + base;
521 }
522 
523 fn integerSerial(value: i64) u64 {
524     if (value == 0) return 8;
525     if (value == 1) return 9;
526     if (value >= -128 and value <= 127) return 1;
527     if (value >= std.math.minInt(i16) and value <= std.math.maxInt(i16)) return 2;
528     if (value >= -8_388_608 and value <= 8_388_607) return 3;
529     if (value >= std.math.minInt(i32) and value <= std.math.maxInt(i32)) return 4;
530     if (value >= -140_737_488_355_328 and value <= 140_737_488_355_327) return 5;
531     return 6;
532 }
533 
534 fn valueBodySize(value: Value) Error!usize {
535     return try serialBodySize(try serialType(value));
536 }
537 
538 fn serialBodySize(serial: u64) Error!usize {
539     return switch (serial) {
540         0, 8, 9 => 0,
541         1 => 1,
542         2 => 2,
543         3 => 3,
544         4 => 4,
545         5 => 6,
546         6 => 8,
547         7, 10, 11 => error.InvalidRow,
548         else => {
549             if (serial < 12) return error.InvalidRow;
550             const size = if (serial % 2 == 0) (serial - 12) / 2 else (serial - 13) / 2;
551             if (size > std.math.maxInt(usize)) return error.RowTooLarge;
552             return @intCast(size);
553         },
554     };
555 }
556 
557 fn writeBody(target: []u8, value: Value) Error!usize {
558     const serial = try serialType(value);
559     const size = try serialBodySize(serial);
560     if (target.len < size) return error.OutputTooSmall;
561     switch (value) {
562         .nil => {},
563         .integer => |integer| writeInteger(target[0..size], integer),
564         .text => |text| @memcpy(target[0..text.len], text),
565         .blob => |blob| @memcpy(target[0..blob.len], blob),
566     }
567     return size;
568 }
569 
570 fn decode(serial: u64, body: []const u8) Error!Value {
571     return switch (serial) {
572         0 => .nil,
573         1, 2, 3, 4, 5, 6 => .{ .integer = readInteger(body) },
574         8 => .{ .integer = 0 },
575         9 => .{ .integer = 1 },
576         else => {
577             if (serial < 12) return error.InvalidRow;
578             if (serial % 2 == 0) return .{ .blob = body };
579             return .{ .text = body };
580         },
581     };
582 }
583 
584 fn writeInteger(target: []u8, value: i64) void {
585     var bytes: [8]u8 = undefined;
586     std.mem.writeInt(i64, bytes[0..8], value, .big);
587     @memcpy(target, bytes[8 - target.len ..]);
588 }
589 
590 fn readInteger(bytes: []const u8) i64 {
591     var target: [8]u8 = undefined;
592     @memset(target[0..], if (bytes[0] & 0x80 != 0) 0xff else 0);
593     @memcpy(target[8 - bytes.len ..], bytes);
594     return std.mem.readInt(i64, target[0..8], .big);
595 }
596 
597 fn varintSize(value: u64) usize {
598     var remaining = value;
599     var size: usize = 1;
600     while (remaining > 0x7f) {
601         remaining >>= 7;
602         size += 1;
603     }
604     return size;
605 }
606 
607 fn writeVarint(target: []u8, value: u64) Error!usize {
608     const size = varintSize(value);
609     if (target.len < size) return error.OutputTooSmall;
610     var shift = (size - 1) * 7;
611     var index: usize = 0;
612     while (index < size) : (index += 1) {
613         var byte: u8 = @intCast((value >> @intCast(shift)) & 0x7f);
614         if (index + 1 < size) byte |= 0x80;
615         target[index] = byte;
616         if (shift >= 7) shift -= 7;
617     }
618     return size;
619 }
620 
621 fn readVarint(bytes: []const u8) Error!Varint {
622     if (bytes.len == 0) return error.InvalidRow;
623     var value: u64 = 0;
624     var index: usize = 0;
625     while (index < bytes.len and index < 10) : (index += 1) {
626         const payload: u64 = bytes[index] & 0x7f;
627         if (value > (std.math.maxInt(u64) - payload) >> 7) return error.RowTooLarge;
628         value = (value << 7) | payload;
629         if (bytes[index] & 0x80 == 0) {
630             return .{ .value = value, .len = index + 1 };
631         }
632     }
633     return error.InvalidRow;
634 }
635 
636 fn checkedAdd(left: usize, right: usize) Error!usize {
637     return std.math.add(usize, left, right) catch error.RowTooLarge;
638 }
639 
640 fn storageRank(storage: Storage) u8 {
641     return switch (storage) {
642         .nil => 0,
643         .integer => 1,
644         .text => 2,
645         .blob => 3,
646     };
647 }
648 
649 fn compareInteger(left: i64, right: i64) std.math.Order {
650     if (left < right) return .lt;
651     if (left > right) return .gt;
652     return .eq;
653 }
654 
655 fn compareText(left: []const u8, right: []const u8, collation: Collation) std.math.Order {
656     return switch (collation) {
657         .binary => simd.order(Bytes, left, right),
658         .nocase => compareNoCase(left, right),
659         .rtrim => simd.order(Bytes, trimRightSpaces(left), trimRightSpaces(right)),
660     };
661 }
662 
663 fn compareNoCase(left: []const u8, right: []const u8) std.math.Order {
664     const min_len = @min(left.len, right.len);
665     var index: usize = 0;
666     while (index < min_len) : (index += 1) {
667         const left_byte = asciiLower(left[index]);
668         const right_byte = asciiLower(right[index]);
669         if (left_byte < right_byte) return .lt;
670         if (left_byte > right_byte) return .gt;
671     }
672     if (left.len < right.len) return .lt;
673     if (left.len > right.len) return .gt;
674     return .eq;
675 }
676 
677 fn asciiLower(byte: u8) u8 {
678     if (byte >= 'A' and byte <= 'Z') return byte + ('a' - 'A');
679     return byte;
680 }
681 
682 fn trimRightSpaces(bytes: []const u8) []const u8 {
683     var end = bytes.len;
684     while (end > 0 and bytes[end - 1] == ' ') end -= 1;
685     return bytes[0..end];
686 }
687 
688 test "row encodes typed values and projections" {
689     var bytes: [128]u8 = undefined;
690     const encoded = try encode(&bytes, &.{
691         Value.nil,
692         .{ .integer = -129 },
693         .{ .text = "Alpha" },
694         .{ .blob = &.{ 1, 2, 3 } },
695     });
696 
697     const view = try View.init(encoded);
698     try std.testing.expectEqual(@as(usize, 4), view.columnCount());
699     try std.testing.expectEqual(Storage.nil, std.meta.activeTag(try view.column(0)));
700     try std.testing.expectEqual(@as(i64, -129), (try view.column(1)).integer);
701     try std.testing.expectEqualStrings("Alpha", (try view.column(2)).text);
702     try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3 }, (try view.column(3)).blob);
703 
704     var projected: [2]Value = undefined;
705     const values = try view.project(&.{ 2, 1 }, &projected);
706     try std.testing.expectEqualStrings("Alpha", values[0].text);
707     try std.testing.expectEqual(@as(i64, -129), values[1].integer);
708 }
709 
710 test "row cursor preserves forward backward repeated and rejected reads" {
711     var bytes: [128]u8 = undefined;
712     const encoded = try encode(&bytes, &.{
713         .{ .integer = -129 },
714         .{ .text = "borrowed" },
715         .nil,
716         .{ .integer = 0x123456789 },
717     });
718     const view = try View.init(encoded);
719     var reader = view.cursor();
720     for ([_]usize{ 0, 1, 2, 3, 3, 1, 0, 3 }) |index| {
721         const expected = try view.column(index);
722         const actual = try reader.column(index);
723         try std.testing.expectEqual(std.math.Order.eq, compareValues(expected, actual, .binary));
724     }
725     try std.testing.expectError(error.ColumnOutOfBounds, reader.column(4));
726     const borrowed = (try reader.column(1)).text;
727     try std.testing.expectEqualStrings("borrowed", borrowed);
728     try std.testing.expectEqual((try view.column(1)).text.ptr, borrowed.ptr);
729     var output: [4]Value = undefined;
730     const projected = try view.project(&.{ 3, 0, 3, 1 }, &output);
731     try std.testing.expectEqual(@as(i64, 0x123456789), projected[0].integer);
732     try std.testing.expectEqual(@as(i64, -129), projected[1].integer);
733     try std.testing.expectEqual(projected[0].integer, projected[2].integer);
734     try std.testing.expectEqual(borrowed.ptr, projected[3].text.ptr);
735 }
736 
737 test "row cursor preserves noncanonical header prefixes and empty rows" {
738     const view = try View.init(&.{ 0x80, 0x04, 0x08, 0x09 });
739     var reader = view.cursor();
740     try std.testing.expectEqual(@as(i64, 0), (try reader.column(0)).integer);
741     try std.testing.expectEqual(@as(i64, 1), (try reader.column(1)).integer);
742     var empty = (try View.init(&.{1})).cursor();
743     try std.testing.expectError(error.ColumnOutOfBounds, empty.column(0));
744     var output: [0]Value = .{};
745     try std.testing.expectEqual(@as(usize, 0), (try view.project(&.{}, &output)).len);
746 }
747 
748 test "row comparison preserves trailing nil columns and the first unequal value" {
749     var left_bytes: [128]u8 = undefined;
750     var right_bytes: [128]u8 = undefined;
751     const left = try encode(&left_bytes, &.{ .{ .integer = 10 }, .nil });
752     const right = try encode(&right_bytes, &.{.{ .integer = 10 }});
753     try std.testing.expectEqual(std.math.Order.eq, try compare(left, right, &.{}));
754     const larger = try encode(&right_bytes, &.{ .{ .integer = 10 }, .nil, .{ .text = "a" } });
755     try std.testing.expectEqual(std.math.Order.lt, try compare(left, larger, &.{}));
756     try std.testing.expectEqual(std.math.Order.gt, try compare(larger, left, &.{}));
757 }
758 
759 test "row compares storage classes and collations" {
760     try std.testing.expectEqual(std.math.Order.lt, compareValues(Value.nil, .{ .integer = -1 }, .binary));
761     try std.testing.expectEqual(std.math.Order.lt, compareValues(.{ .integer = 9 }, .{ .text = "0" }, .binary));
762     try std.testing.expectEqual(std.math.Order.lt, compareValues(.{ .text = "z" }, .{ .blob = "a" }, .binary));
763     try std.testing.expectEqual(std.math.Order.gt, compareValues(.{ .text = "a" }, .{ .text = "A" }, .binary));
764     try std.testing.expectEqual(std.math.Order.eq, compareValues(.{ .text = "a" }, .{ .text = "A" }, .nocase));
765     try std.testing.expectEqual(std.math.Order.eq, compareValues(.{ .text = "a   " }, .{ .text = "a" }, .rtrim));
766 }
767 
768 test "row comparison walks columns left to right" {
769     var left_bytes: [128]u8 = undefined;
770     var right_bytes: [128]u8 = undefined;
771     const left = try encode(&left_bytes, &.{
772         .{ .integer = 10 },
773         .{ .text = "abc" },
774     });
775     const right = try encode(&right_bytes, &.{
776         .{ .integer = 10 },
777         .{ .text = "ABC" },
778     });
779 
780     try std.testing.expectEqual(std.math.Order.gt, try compare(left, right, &.{ .{}, .{ .collation = .binary } }));
781     try std.testing.expectEqual(std.math.Order.eq, try compare(left, right, &.{ .{}, .{ .collation = .nocase } }));
782 }
783 
784 test "row rejects invalid and truncated data" {
785     var bytes: [32]u8 = undefined;
786     const encoded = try encode(&bytes, &.{.{ .text = "abcd" }});
787     try std.testing.expectError(error.InvalidRow, View.init(encoded[0 .. encoded.len - 1]));
788     const view = try View.init(encoded);
789     try std.testing.expectError(error.ColumnOutOfBounds, view.column(1));
790     var projected: [0]Value = .{};
791     try std.testing.expectError(error.OutputTooSmall, view.project(&.{0}, &projected));
792 }
793 
794 test "row spec derives SQL text arity projections and indices" {
795     const Record = Spec(enum { id, title, created_at });
796     const Summary = Record.Project(&.{ .title, .id });
797 
798     try std.testing.expectEqualStrings("id, title, created_at", Record.columns);
799     try std.testing.expectEqualStrings("?1, ?2, ?3, ?4", Record.parameters(1));
800     try std.testing.expectEqualStrings(
801         "id = ?2, title = ?3, created_at = ?4",
802         Record.assignments(1),
803     );
804     try std.testing.expectEqual(@as(usize, 1), Record.index(.title));
805     try std.testing.expectEqual(@as(usize, 4), Record.at(3, .title));
806     try std.testing.expectEqual(@as(usize, 4), Record.atBase(3).at(.title));
807     try std.testing.expectEqual(@as(usize, 3), Record.parameter(1, .title));
808     try std.testing.expectEqual([2]usize{ 4, 2 }, Record.indices(2, &.{ .created_at, .id }));
809     try std.testing.expectEqualStrings("title, id", Summary.columns);
810     try std.testing.expectEqual(@as(usize, 1), Summary.index(.id));
811 
812     var bytes: [128]u8 = undefined;
813     const encoded = try encode(&bytes, &.{
814         .{ .integer = 7 },
815         .{ .text = "tiny-row" },
816         .{ .text = "Row specification" },
817         .{ .integer = 42 },
818     });
819     const view = try View.init(encoded);
820     try std.testing.expect(Record.matches(view, 1));
821     try std.testing.expect(!Record.matches(view, 0));
822 }