Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions src/loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -509,20 +509,14 @@ fn normalizeDateTimeToIso(col_type: ColumnType, val: []const u8, buf: *[19]u8) [
return buf[0..19];
}

/// fmtThousands(buf, n) → []const u8
/// Pre: buf.len >= 26 (accommodates any usize value with thousands separators)
/// Post: n is formatted as a decimal string with ',' separating each group of
/// three digits from the right (e.g. 42317 → "42,317", 1000 → "1,000")
/// Format n with thousands separators (e.g. 42317 → "42,317").
/// buf must hold at least 26 bytes.
pub fn fmtThousands(buf: []u8, n: usize) []const u8 {
var tmp: [32]u8 = undefined; // 20 digits max (u64) + safety margin
var tmp: [32]u8 = undefined;
const digits = std.fmt.bufPrint(&tmp, "{d}", .{n}) catch unreachable;
const len = digits.len;
const first_group = len % 3; // digits in the leading group (0 means groups of 3 from start)
var out_len: usize = 0;
for (digits, 0..) |ch, i| {
if ((i > 0 and i == first_group) or
(i > first_group and (i - first_group) % 3 == 0))
{
if (i > 0 and (digits.len - i) % 3 == 0) {
buf[out_len] = ',';
out_len += 1;
}
Expand Down
105 changes: 13 additions & 92 deletions src/markdown.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
const std = @import("std");
const c = @import("c");
const sqlite_mod = @import("sqlite.zig");
const visual = @import("visual.zig");

/// Write a Markdown table from SQLite query results to the given writer.
///
Expand Down Expand Up @@ -40,7 +41,7 @@ pub fn writeMarkdown(
// 2. Pass 1: Compute column widths and detect numeric columns
const widths = try a.alloc(usize, ncols);
for (0..ncols) |i| {
widths[i] = visualWidth(col_names[i]);
widths[i] = visual.visualWidth(col_names[i]);
}
const numeric = try a.alloc(bool, ncols);
@memset(numeric, true);
Expand All @@ -64,7 +65,7 @@ pub fn writeMarkdown(
const ptr = c.sqlite3_column_text(stmt, idx);
if (ptr != null) {
const s = std.mem.span(@as([*:0]const u8, @ptrCast(ptr)));
const vw = visualWidth(s);
const vw = visual.visualWidth(s);
if (vw > widths[i]) widths[i] = vw;
}
}
Expand Down Expand Up @@ -108,14 +109,14 @@ fn writeRow(
for (values, 0..) |val, i| {
try writer.writeByte(' ');
const w = widths[i];
const vw = visualWidth(val);
const vw = visual.visualWidth(val);
const padding = w - vw;
if (right_align) {
try writeSpaces(writer, padding);
try visual.writeSpaces(writer, padding);
try writer.writeAll(val);
} else {
try writer.writeAll(val);
try writeSpaces(writer, padding);
try visual.writeSpaces(writer, padding);
}
try writer.writeByte(' ');
try writer.writeByte('|');
Expand All @@ -134,7 +135,7 @@ fn writeSeparator(
try writer.writeByte('|');
for (widths) |w| {
try writer.writeByte(' ');
try writeCharRepeated(writer, "-", w);
try visual.writeCharRepeated(writer, "-", w);
try writer.writeByte(' ');
try writer.writeByte('|');
}
Expand All @@ -157,23 +158,23 @@ fn writeDataRow(
if (c.sqlite3_column_type(stmt, idx) == c.SQLITE_NULL) {
// NULL renders as empty cell
if (numeric[i]) {
try writeSpaces(writer, w);
try visual.writeSpaces(writer, w);
} else {
try writeSpaces(writer, w);
try visual.writeSpaces(writer, w);
}
} else {
if (sqlite_mod.columnText(stmt, idx)) |val| {
const vw = visualWidth(val);
const vw = visual.visualWidth(val);
const padding = w - vw;
if (numeric[i] and val.len > 0) {
try writeSpaces(writer, padding);
try visual.writeSpaces(writer, padding);
try writer.writeAll(val);
} else {
try writer.writeAll(val);
try writeSpaces(writer, padding);
try visual.writeSpaces(writer, padding);
}
} else {
try writeSpaces(writer, w);
try visual.writeSpaces(writer, w);
}
}
try writer.writeByte(' ');
Expand All @@ -182,86 +183,6 @@ fn writeDataRow(
try writer.writeByte('\n');
}

// ── UTF-8 / visual-width helpers (copied from table.zig) ──────────────────

fn utf8CharLen(first: u8) usize {
if (first < 0x80) return 1;
if (first < 0xC0) return 1;
if (first < 0xE0) return 2;
if (first < 0xF0) return 3;
if (first < 0xF8) return 4;
return 1;
}

fn utf8DecodeRaw(bytes: []const u8) ?u21 {
return switch (bytes.len) {
1 => bytes[0],
2 => std.unicode.utf8Decode2(bytes[0..2].*) catch null,
3 => std.unicode.utf8Decode3(bytes[0..3].*) catch null,
4 => std.unicode.utf8Decode4(bytes[0..4].*) catch null,
else => null,
};
}

fn isWideCodepoint(cp: u21) bool {
return (cp >= 0x3400 and cp <= 0x4DBF) or
(cp >= 0x4E00 and cp <= 0x9FFF) or
(cp >= 0xAC00 and cp <= 0xD7AF) or
(cp >= 0xFF00 and cp <= 0xFFEF);
}

fn visualWidth(s: []const u8) usize {
var width: usize = 0;
var i: usize = 0;
while (i < s.len) {
const byte_len = utf8CharLen(s[i]);
if (i + byte_len > s.len) {
width += 1;
i += 1;
continue;
}
const slice = s[i..][0..byte_len];
const codepoint = utf8DecodeRaw(slice) orelse {
width += 1;
i += 1;
continue;
};
if (isWideCodepoint(codepoint)) {
width += 2;
} else {
width += 1;
}
i += byte_len;
}
return width;
}

fn writeCharRepeated(writer: *std.Io.Writer, char: []const u8, n: usize) error{WriteFailed}!void {
var buf: [256]u8 = undefined;
const char_len = char.len;
var filled: usize = 0;
while (filled + char_len <= buf.len) : (filled += char_len) {
@memcpy(buf[filled..][0..char_len], char);
}
var remaining = n;
while (remaining > 0) {
const chunk = @min(remaining, filled / char_len);
try writer.writeAll(buf[0..chunk * char_len]);
remaining -= chunk;
}
}

const spaces_buf = " " ** 256;

fn writeSpaces(writer: *std.Io.Writer, n: usize) error{WriteFailed}!void {
var remaining = n;
while (remaining > 0) {
const chunk = @min(remaining, spaces_buf.len);
try writer.writeAll(spaces_buf[0..chunk]);
remaining -= chunk;
}
}

test "writeMarkdown parameter order" {
try std.testing.expect(true);
}
41 changes: 13 additions & 28 deletions src/modes/columns.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const inference_buffer_size = loader.inference_buffer_size;
const ExitCode = args_mod.ExitCode;
const fatal = @import("../sqlite.zig").fatal;
const readAllInput = @import("../sqlite.zig").readAllInput;
const source = @import("source.zig");

pub fn runColumns(
allocator: std.mem.Allocator,
Expand All @@ -30,13 +31,9 @@ pub fn runColumns(
.csv, .tsv => {
const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter;
var read_buf: [4096]u8 = undefined;
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
defer if (input_source == .file) std.Io.File.close(source_file, io);
var source_reader = std.Io.File.reader(source_file, io, &read_buf);
const opened = source.openInput(input_source, io, stderr_writer);
defer opened.deinit(io);
var source_reader = std.Io.File.reader(opened.file, io, &read_buf);
var csv_reader = csv_mod.csvReaderWithDelimiter(allocator, &source_reader.interface, col_delim);

const header_record = csv_reader.nextRecord() catch |err| switch (err) {
Expand Down Expand Up @@ -98,13 +95,9 @@ pub fn runColumns(
},
.json => {
var read_buf: [4096]u8 = undefined;
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
defer if (input_source == .file) std.Io.File.close(source_file, io);
var source_reader = std.Io.File.reader(source_file, io, &read_buf);
const opened = source.openInput(input_source, io, stderr_writer);
defer opened.deinit(io);
var source_reader = std.Io.File.reader(opened.file, io, &read_buf);

const input = readAllInput(&source_reader.interface, allocator, stderr_writer, "JSON input");
defer allocator.free(input);
Expand Down Expand Up @@ -132,13 +125,9 @@ pub fn runColumns(
},
.ndjson => {
var read_buf: [4096]u8 = undefined;
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
defer if (input_source == .file) std.Io.File.close(source_file, io);
var source_reader = std.Io.File.reader(source_file, io, &read_buf);
const opened = source.openInput(input_source, io, stderr_writer);
defer opened.deinit(io);
var source_reader = std.Io.File.reader(opened.file, io, &read_buf);

// Read until we find a non-empty line
var line_num: usize = 0;
Expand Down Expand Up @@ -182,13 +171,9 @@ pub fn runColumns(
},
.xml => {
var read_buf: [4096]u8 = undefined;
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
defer if (input_source == .file) std.Io.File.close(source_file, io);
var source_reader = std.Io.File.reader(source_file, io, &read_buf);
const opened = source.openInput(input_source, io, stderr_writer);
defer opened.deinit(io);
var source_reader = std.Io.File.reader(opened.file, io, &read_buf);

const names = xml_mod.getXmlColumnNames(allocator, &source_reader.interface, args.xml_root_input, args.xml_row_input, stderr_writer);
defer {
Expand Down
11 changes: 4 additions & 7 deletions src/modes/sample.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const inference_buffer_size = loader.inference_buffer_size;

const ExitCode = args_mod.ExitCode;
const fatal = @import("../sqlite.zig").fatal;
const source = @import("source.zig");

pub fn runSample(
allocator: std.mem.Allocator,
Expand All @@ -36,13 +37,9 @@ pub fn runSample(
.csv, .tsv => {
const col_delim: []const u8 = if (args.input_format == .tsv) "\t" else args.delimiter;
var read_buf: [4096]u8 = undefined;
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
defer if (input_source == .file) std.Io.File.close(source_file, io);
var source_reader = std.Io.File.reader(source_file, io, &read_buf);
const opened = source.openInput(input_source, io, stderr_writer);
defer opened.deinit(io);
var source_reader = std.Io.File.reader(opened.file, io, &read_buf);
var csv_reader = csv_mod.csvReaderWithDelimiter(allocator, &source_reader.interface, col_delim);

const header_record = csv_reader.nextRecord() catch |err| switch (err) {
Expand Down
30 changes: 30 additions & 0 deletions src/modes/source.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Shared helpers for opening input sources in mode commands.
//!
//! Provides `SourceFile` — a file handle with a `needs_close` flag,
//! so callers can uniformly handle file-or-stdin sources.

const std = @import("std");
const fatal = @import("../sqlite.zig").fatal;

/// Result of opening a file-or-stdin input source.
pub const SourceFile = struct {
file: std.Io.File,
needs_close: bool,

/// Close the file if it was opened from a path (no-op for stdin).
pub fn deinit(self: SourceFile, io: std.Io) void {
if (self.needs_close) std.Io.File.close(self.file, io);
}
};

/// Open a file or stdin, handling errors uniformly.
///
/// `input_source` must be a tagged union with `.file: []const u8` and `.stdin` variants.
pub fn openInput(input_source: anytype, io: std.Io, stderr_writer: *std.Io.Writer) SourceFile {
const source_file = switch (input_source) {
.file => |path| std.Io.Dir.openFile(std.Io.Dir.cwd(), io, path, .{}) catch |err|
fatal("cannot open file '{s}': {s}", stderr_writer, .csv_error, .{ path, @errorName(err) }),
.stdin => std.Io.File.stdin(),
};
return .{ .file = source_file, .needs_close = input_source == .file };
}
Loading
Loading