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 }