//! SQLite prepared statement handling
//!
//! Provides the Statement struct for executing SQL queries with parameter binding.

const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const types = @import("types.zig");

const Error = errors.Error;
const resultToError = errors.resultToError;
const ColumnType = types.ColumnType;

// Forward declaration - Database will be imported where needed
pub const Database = @import("database.zig").Database;

/// Prepared SQL statement
pub const Statement = struct {
    handle: ?*c.sqlite3_stmt,
    db: *Database,

    const Self = @This();

    /// Finalizes (destroys) the statement.
    pub fn finalize(self: *Self) void {
        if (self.handle) |h| {
            _ = c.sqlite3_finalize(h);
            self.handle = null;
        }
    }

    /// Resets the statement for re-execution.
    pub fn reset(self: *Self) Error!void {
        const result = c.sqlite3_reset(self.handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Clears all parameter bindings.
    pub fn clearBindings(self: *Self) Error!void {
        const result = c.sqlite3_clear_bindings(self.handle);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    // ========================================================================
    // Statement metadata
    // ========================================================================

    /// Returns the SQL text of the statement.
    pub fn sql(self: *Self) ?[]const u8 {
        const s = c.sqlite3_sql(self.handle);
        if (s) |ptr| {
            return std.mem.span(ptr);
        }
        return null;
    }

    /// Returns whether the statement is read-only.
    pub fn isReadOnly(self: *Self) bool {
        return c.sqlite3_stmt_readonly(self.handle) != 0;
    }

    /// Returns the number of parameters in the statement.
    pub fn parameterCount(self: *Self) u32 {
        return @intCast(c.sqlite3_bind_parameter_count(self.handle));
    }

    /// Returns the index of a named parameter.
    /// Supports :name, @name, and $name styles.
    /// Returns null if the parameter is not found.
    pub fn parameterIndex(self: *Self, name: [:0]const u8) ?u32 {
        const idx = c.sqlite3_bind_parameter_index(self.handle, name.ptr);
        if (idx == 0) return null;
        return @intCast(idx);
    }

    /// Returns the name of a parameter by index.
    /// Returns null if the parameter has no name (positional ?).
    pub fn parameterName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_bind_parameter_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    // ========================================================================
    // Bind parameters (1-indexed as per SQLite convention)
    // ========================================================================

    /// Binds NULL to a parameter.
    pub fn bindNull(self: *Self, index: u32) Error!void {
        const result = c.sqlite3_bind_null(self.handle, @intCast(index));
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds an integer to a parameter.
    pub fn bindInt(self: *Self, index: u32, value: i64) Error!void {
        const result = c.sqlite3_bind_int64(self.handle, @intCast(index), value);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a float to a parameter.
    pub fn bindFloat(self: *Self, index: u32, value: f64) Error!void {
        const result = c.sqlite3_bind_double(self.handle, @intCast(index), value);
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds text to a parameter. The text is copied by SQLite (SQLITE_TRANSIENT).
    pub fn bindText(self: *Self, index: u32, value: []const u8) Error!void {
        const result = c.sqlite3_bind_text(
            self.handle,
            @intCast(index),
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a blob to a parameter. The blob is copied by SQLite (SQLITE_TRANSIENT).
    pub fn bindBlob(self: *Self, index: u32, value: []const u8) Error!void {
        const result = c.sqlite3_bind_blob(
            self.handle,
            @intCast(index),
            value.ptr,
            @intCast(value.len),
            c.SQLITE_TRANSIENT,
        );
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    /// Binds a boolean to a parameter (as integer 0 or 1).
    pub fn bindBool(self: *Self, index: u32, value: bool) Error!void {
        try self.bindInt(index, if (value) 1 else 0);
    }

    /// Binds a zeroblob (a blob of zeros) to a parameter.
    pub fn bindZeroblob(self: *Self, index: u32, size: u32) Error!void {
        const result = c.sqlite3_bind_zeroblob(self.handle, @intCast(index), @intCast(size));
        if (result != c.SQLITE_OK) {
            return resultToError(result);
        }
    }

    // ========================================================================
    // Named parameter binding
    // ========================================================================

    /// Binds NULL to a named parameter.
    pub fn bindNullNamed(self: *Self, name: [:0]const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindNull(idx);
    }

    /// Binds an integer to a named parameter.
    pub fn bindIntNamed(self: *Self, name: [:0]const u8, value: i64) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindInt(idx, value);
    }

    /// Binds a float to a named parameter.
    pub fn bindFloatNamed(self: *Self, name: [:0]const u8, value: f64) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindFloat(idx, value);
    }

    /// Binds text to a named parameter.
    pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindText(idx, value);
    }

    /// Binds a blob to a named parameter.
    pub fn bindBlobNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindBlob(idx, value);
    }

    /// Binds a boolean to a named parameter.
    pub fn bindBoolNamed(self: *Self, name: [:0]const u8, value: bool) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindBool(idx, value);
    }

    /// Binds a timestamp as ISO8601 text (YYYY-MM-DD HH:MM:SS).
    pub fn bindTimestamp(self: *Self, index: u32, ts: i64) Error!void {
        const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(ts) };
        const day_seconds = epoch_seconds.getDaySeconds();
        const year_day = epoch_seconds.getEpochDay().calculateYearDay();
        const month_day = year_day.calculateMonthDay();

        var buf: [20]u8 = undefined;
        const formatted = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
            year_day.year,
            @intFromEnum(month_day.month),
            @as(u8, month_day.day_index) + 1,
            day_seconds.getHoursIntoDay(),
            day_seconds.getMinutesIntoHour(),
            day_seconds.getSecondsIntoMinute(),
        }) catch return Error.SqliteError;

        try self.bindText(index, formatted);
    }

    /// Binds a timestamp to a named parameter as ISO8601 text.
    pub fn bindTimestampNamed(self: *Self, name: [:0]const u8, ts: i64) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindTimestamp(idx, ts);
    }

    /// Binds the current time as ISO8601 text.
    pub fn bindCurrentTime(self: *Self, index: u32) Error!void {
        const now = std.time.timestamp();
        try self.bindTimestamp(index, now);
    }

    /// Binds the current time to a named parameter.
    pub fn bindCurrentTimeNamed(self: *Self, name: [:0]const u8) Error!void {
        const idx = self.parameterIndex(name) orelse return Error.Range;
        try self.bindCurrentTime(idx);
    }

    // ========================================================================
    // Execution
    // ========================================================================

    /// Executes one step of the statement.
    /// Returns true if there's a row available (for SELECT statements).
    /// Returns false when done (SQLITE_DONE).
    pub fn step(self: *Self) Error!bool {
        const result = c.sqlite3_step(self.handle);
        return switch (result) {
            c.SQLITE_ROW => true,
            c.SQLITE_DONE => false,
            else => resultToError(result),
        };
    }

    // ========================================================================
    // Column access (0-indexed as per SQLite convention)
    // ========================================================================

    /// Returns the number of columns in the result set.
    pub fn columnCount(self: *Self) u32 {
        return @intCast(c.sqlite3_column_count(self.handle));
    }

    /// Returns the name of a column.
    pub fn columnName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_column_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    /// Returns the type of a column value.
    pub fn columnType(self: *Self, index: u32) ColumnType {
        const col_type = c.sqlite3_column_type(self.handle, @intCast(index));
        return switch (col_type) {
            c.SQLITE_INTEGER => .integer,
            c.SQLITE_FLOAT => .float,
            c.SQLITE_TEXT => .text,
            c.SQLITE_BLOB => .blob,
            c.SQLITE_NULL => .null_value,
            else => .null_value,
        };
    }

    /// Returns an integer column value.
    pub fn columnInt(self: *Self, index: u32) i64 {
        return c.sqlite3_column_int64(self.handle, @intCast(index));
    }

    /// Returns a float column value.
    pub fn columnFloat(self: *Self, index: u32) f64 {
        return c.sqlite3_column_double(self.handle, @intCast(index));
    }

    /// Returns a text column value.
    pub fn columnText(self: *Self, index: u32) ?[]const u8 {
        const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
        const text = c.sqlite3_column_text(self.handle, @intCast(index));
        if (text) |t| {
            return t[0..@intCast(len)];
        }
        return null;
    }

    /// Returns a blob column value.
    pub fn columnBlob(self: *Self, index: u32) ?[]const u8 {
        const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
        const blob = c.sqlite3_column_blob(self.handle, @intCast(index));
        if (blob) |b| {
            const ptr: [*]const u8 = @ptrCast(b);
            return ptr[0..@intCast(len)];
        }
        return null;
    }

    /// Returns true if the column is NULL.
    pub fn columnIsNull(self: *Self, index: u32) bool {
        return self.columnType(index) == .null_value;
    }

    /// Returns a boolean column value (interprets 0 as false, non-zero as true).
    pub fn columnBool(self: *Self, index: u32) bool {
        return self.columnInt(index) != 0;
    }

    /// Returns the size in bytes of a blob or text column.
    pub fn columnBytes(self: *Self, index: u32) u32 {
        return @intCast(c.sqlite3_column_bytes(self.handle, @intCast(index)));
    }

    /// Returns the declared type of a column.
    pub fn columnDeclType(self: *Self, index: u32) ?[]const u8 {
        const dtype = c.sqlite3_column_decltype(self.handle, @intCast(index));
        if (dtype) |d| {
            return std.mem.span(d);
        }
        return null;
    }

    /// Returns the database name for a column result.
    pub fn columnDatabaseName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_column_database_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    /// Returns the table name for a column result.
    pub fn columnTableName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_column_table_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    /// Returns the origin column name for a column result.
    pub fn columnOriginName(self: *Self, index: u32) ?[]const u8 {
        const name = c.sqlite3_column_origin_name(self.handle, @intCast(index));
        if (name) |n| {
            return std.mem.span(n);
        }
        return null;
    }

    /// Returns the SQL text with bound parameters expanded.
    pub fn expandedSql(self: *Self, allocator: std.mem.Allocator) ?[]u8 {
        const expanded = c.sqlite3_expanded_sql(self.handle);
        if (expanded == null) return null;

        const len = std.mem.len(expanded);
        const result = allocator.alloc(u8, len) catch return null;
        @memcpy(result, expanded[0..len]);

        c.sqlite3_free(expanded);
        return result;
    }

    // ========================================================================
    // Batch binding with tuples/structs
    // ========================================================================

    /// Binds all values from a tuple or struct to positional parameters.
    /// Parameters are bound starting at index 1.
    ///
    /// Example:
    /// ```zig
    /// var stmt = try db.prepare("INSERT INTO users (name, age, score) VALUES (?, ?, ?)");
    /// try stmt.bindAll(.{ "Alice", @as(i64, 30), @as(f64, 95.5) });
    /// ```
    ///
    /// Supported types:
    /// - `[]const u8`, `[:0]const u8` -> bindText
    /// - `i64`, `i32`, `i16`, `i8`, `u32`, `u16`, `u8` -> bindInt
    /// - `f64`, `f32` -> bindFloat
    /// - `bool` -> bindBool
    /// - `@TypeOf(null)` -> bindNull
    /// - `?T` (optionals) -> bindNull if null, otherwise bind inner value
    pub fn bindAll(self: *Self, values: anytype) Error!void {
        const T = @TypeOf(values);
        const info = @typeInfo(T);

        if (info == .@"struct") {
            const fields = info.@"struct".fields;
            inline for (fields, 0..) |field, i| {
                const value = @field(values, field.name);
                try self.bindValue(@intCast(i + 1), value);
            }
        } else {
            @compileError("bindAll expects a tuple or struct, got " ++ @typeName(T));
        }
    }

    /// Binds a single value of any supported type to a parameter index.
    pub fn bindValue(self: *Self, index: u32, value: anytype) Error!void {
        const T = @TypeOf(value);
        const info = @typeInfo(T);

        // Handle optionals
        if (info == .optional) {
            if (value) |v| {
                return self.bindValue(index, v);
            } else {
                return self.bindNull(index);
            }
        }

        // Handle null type
        if (T == @TypeOf(null)) {
            return self.bindNull(index);
        }

        // Handle pointers to arrays (string literals)
        if (info == .pointer) {
            const child = info.pointer.child;
            if (info.pointer.size == .slice) {
                // []const u8 or []u8
                if (child == u8) {
                    return self.bindText(index, value);
                }
            } else if (info.pointer.size == .one) {
                // *const [N]u8 (string literal)
                const child_info = @typeInfo(child);
                if (child_info == .array and child_info.array.child == u8) {
                    return self.bindText(index, value);
                }
            }
        }

        // Handle arrays
        if (info == .array and info.array.child == u8) {
            return self.bindText(index, &value);
        }

        // Handle integers
        if (info == .int or info == .comptime_int) {
            return self.bindInt(index, @intCast(value));
        }

        // Handle floats
        if (info == .float or info == .comptime_float) {
            return self.bindFloat(index, @floatCast(value));
        }

        // Handle booleans
        if (T == bool) {
            return self.bindBool(index, value);
        }

        @compileError("Unsupported type for bindValue: " ++ @typeName(T));
    }

    /// Resets the statement and binds new values in one call.
    /// Useful for executing the same statement multiple times with different values.
    ///
    /// Example:
    /// ```zig
    /// var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
    /// try stmt.rebind(.{ "Alice", @as(i64, 30) });
    /// _ = try stmt.step();
    /// try stmt.rebind(.{ "Bob", @as(i64, 25) });
    /// _ = try stmt.step();
    /// ```
    pub fn rebind(self: *Self, values: anytype) Error!void {
        try self.reset();
        try self.clearBindings();
        try self.bindAll(values);
    }

    // ========================================================================
    // Row Iterator
    // ========================================================================

    /// Returns an iterator over result rows.
    /// This provides a more idiomatic Zig interface for iterating over query results.
    ///
    /// Example:
    /// ```zig
    /// var stmt = try db.prepare("SELECT id, name FROM users");
    /// defer stmt.finalize();
    ///
    /// var iter = stmt.iterator();
    /// while (try iter.next()) |row| {
    ///     const id = row.int(0);
    ///     const name = row.text(1) orelse "(null)";
    ///     std.debug.print("User {}: {s}\n", .{ id, name });
    /// }
    /// ```
    pub fn iterator(self: *Self) RowIterator {
        return RowIterator{ .stmt = self };
    }

    /// Convenience method to iterate with a callback.
    /// The callback receives a Row for each result row.
    ///
    /// Example:
    /// ```zig
    /// try stmt.forEach(struct {
    ///     fn call(row: Row) void {
    ///         std.debug.print("{}: {s}\n", .{ row.int(0), row.text(1) orelse "" });
    ///     }
    /// }.call);
    /// ```
    pub fn forEach(self: *Self, callback: *const fn (Row) void) Error!void {
        while (try self.step()) {
            callback(Row{ .stmt = self });
        }
    }

    /// Executes the statement and collects all rows into a slice.
    /// Each row is represented as an array of column values.
    /// Caller owns the returned memory.
    pub fn collectAll(self: *Self, allocator: std.mem.Allocator) ![]Row.Values {
        var rows = std.ArrayList(Row.Values).init(allocator);
        errdefer {
            for (rows.items) |*r| r.deinit(allocator);
            rows.deinit();
        }

        while (try self.step()) {
            const row = Row{ .stmt = self };
            try rows.append(try row.toValues(allocator));
        }

        return rows.toOwnedSlice();
    }
};

/// Row iterator for idiomatic iteration over query results.
pub const RowIterator = struct {
    stmt: *Statement,

    const Self = @This();

    /// Advances to the next row.
    /// Returns a Row if there's data available, null if iteration is complete.
    pub fn next(self: *Self) Error!?Row {
        if (try self.stmt.step()) {
            return Row{ .stmt = self.stmt };
        }
        return null;
    }

    /// Resets the iterator to the beginning.
    pub fn reset(self: *Self) Error!void {
        try self.stmt.reset();
    }
};

/// Represents a single row in a query result.
/// Provides convenient access to column values.
pub const Row = struct {
    stmt: *Statement,

    const Self = @This();

    /// Returns the number of columns.
    pub fn columnCount(self: Self) u32 {
        return self.stmt.columnCount();
    }

    /// Returns an integer column value.
    pub fn int(self: Self, index: u32) i64 {
        return self.stmt.columnInt(index);
    }

    /// Returns a float column value.
    pub fn float(self: Self, index: u32) f64 {
        return self.stmt.columnFloat(index);
    }

    /// Returns a text column value.
    pub fn text(self: Self, index: u32) ?[]const u8 {
        return self.stmt.columnText(index);
    }

    /// Returns a blob column value.
    pub fn blob(self: Self, index: u32) ?[]const u8 {
        return self.stmt.columnBlob(index);
    }

    /// Returns a boolean column value.
    pub fn boolean(self: Self, index: u32) bool {
        return self.stmt.columnBool(index);
    }

    /// Returns true if the column is NULL.
    pub fn isNull(self: Self, index: u32) bool {
        return self.stmt.columnIsNull(index);
    }

    /// Returns the column type.
    pub fn columnType(self: Self, index: u32) ColumnType {
        return self.stmt.columnType(index);
    }

    /// Returns the column name.
    pub fn columnName(self: Self, index: u32) ?[]const u8 {
        return self.stmt.columnName(index);
    }

    // ========================================================================
    // Row to Struct Mapping (inspired by zig-sqlite)
    // ========================================================================

    /// Maps the current row to a struct type.
    /// Fields are matched by column index (field order in struct = column order).
    /// Does not allocate - returns copies of numeric types and slices pointing
    /// to SQLite's internal buffers (valid until next step/reset).
    ///
    /// Example:
    /// ```zig
    /// const User = struct {
    ///     id: i64,
    ///     name: ?[]const u8,
    ///     score: f64,
    ///     active: bool,
    /// };
    ///
    /// var stmt = try db.prepare("SELECT id, name, score, active FROM users");
    /// var iter = stmt.iterator();
    /// while (try iter.next()) |row| {
    ///     const user = row.to(User);
    ///     std.debug.print("User {}: {s}\n", .{ user.id, user.name orelse "(anonymous)" });
    /// }
    /// ```
    ///
    /// Supported field types:
    /// - `i64`, `i32`, `i16`, `i8`, `u32`, `u16`, `u8` -> columnInt (with cast)
    /// - `f64`, `f32` -> columnFloat (with cast)
    /// - `bool` -> columnBool
    /// - `[]const u8` -> columnText (returns empty slice if NULL)
    /// - `?[]const u8` -> columnText (returns null if NULL)
    /// - `?i64`, `?f64`, etc. -> returns null if column is NULL
    pub fn to(self: Self, comptime T: type) T {
        const info = @typeInfo(T);
        if (info != .@"struct") {
            @compileError("Row.to() expects a struct type, got " ++ @typeName(T));
        }

        var result: T = undefined;
        const fields = info.@"struct".fields;

        inline for (fields, 0..) |field, i| {
            @field(result, field.name) = self.readColumn(field.type, @intCast(i));
        }

        return result;
    }

    /// Maps the current row to a struct type, allocating copies of text/blob data.
    /// The returned struct owns its string data and must be freed with `freeStruct`.
    ///
    /// Use this when you need the data to persist beyond the current row iteration.
    ///
    /// Example:
    /// ```zig
    /// const User = struct {
    ///     id: i64,
    ///     name: []const u8,  // Will be allocated copy
    /// };
    ///
    /// var users = std.ArrayList(User).init(allocator);
    /// defer {
    ///     for (users.items) |user| Row.freeStruct(User, user, allocator);
    ///     users.deinit();
    /// }
    ///
    /// var stmt = try db.prepare("SELECT id, name FROM users");
    /// var iter = stmt.iterator();
    /// while (try iter.next()) |row| {
    ///     const user = try row.toAlloc(User, allocator);
    ///     try users.append(user);
    /// }
    /// ```
    pub fn toAlloc(self: Self, comptime T: type, allocator: std.mem.Allocator) !T {
        const info = @typeInfo(T);
        if (info != .@"struct") {
            @compileError("Row.toAlloc() expects a struct type, got " ++ @typeName(T));
        }

        var result: T = undefined;
        const fields = info.@"struct".fields;

        // Track allocations for cleanup on error
        var allocated_count: usize = 0;
        errdefer {
            inline for (fields, 0..) |field, i| {
                if (i < allocated_count) {
                    if (isAllocatedStringType(field.type)) {
                        const val = @field(result, field.name);
                        freeFieldValue(field.type, val, allocator);
                    }
                }
            }
        }

        inline for (fields, 0..) |field, i| {
            @field(result, field.name) = try self.readColumnAlloc(field.type, @intCast(i), allocator);
            allocated_count = i + 1;
        }

        return result;
    }

    /// Frees a struct that was created with `toAlloc`.
    pub fn freeStruct(comptime T: type, value: T, allocator: std.mem.Allocator) void {
        const info = @typeInfo(T);
        if (info != .@"struct") return;

        const fields = info.@"struct".fields;
        inline for (fields) |field| {
            if (isAllocatedStringType(field.type)) {
                const val = @field(value, field.name);
                freeFieldValue(field.type, val, allocator);
            }
        }
    }

    fn isAllocatedStringType(comptime T: type) bool {
        if (T == []const u8 or T == []u8) return true;
        const info = @typeInfo(T);
        if (info == .optional) {
            const child = info.optional.child;
            return child == []const u8 or child == []u8;
        }
        return false;
    }

    fn freeFieldValue(comptime T: type, value: T, allocator: std.mem.Allocator) void {
        const info = @typeInfo(T);
        if (info == .optional) {
            if (value) |v| {
                if (v.len > 0) allocator.free(v);
            }
        } else if (T == []const u8 or T == []u8) {
            if (value.len > 0) allocator.free(value);
        }
    }

    /// Reads a column value into the specified Zig type (non-allocating).
    fn readColumn(self: Self, comptime T: type, index: u32) T {
        const info = @typeInfo(T);

        // Handle optionals
        if (info == .optional) {
            if (self.isNull(index)) return null;
            return self.readColumn(info.optional.child, index);
        }

        // Handle integers
        if (info == .int) {
            const val = self.stmt.columnInt(index);
            return @intCast(val);
        }

        // Handle floats
        if (info == .float) {
            const val = self.stmt.columnFloat(index);
            return @floatCast(val);
        }

        // Handle bool
        if (T == bool) {
            return self.stmt.columnBool(index);
        }

        // Handle strings
        if (T == []const u8) {
            return self.stmt.columnText(index) orelse "";
        }

        @compileError("Unsupported type for row mapping: " ++ @typeName(T));
    }

    /// Reads a column value into the specified Zig type (allocating for strings).
    fn readColumnAlloc(self: Self, comptime T: type, index: u32, allocator: std.mem.Allocator) !T {
        const info = @typeInfo(T);

        // Handle optionals
        if (info == .optional) {
            if (self.isNull(index)) return null;
            return try self.readColumnAlloc(info.optional.child, index, allocator);
        }

        // Handle integers
        if (info == .int) {
            const val = self.stmt.columnInt(index);
            return @intCast(val);
        }

        // Handle floats
        if (info == .float) {
            const val = self.stmt.columnFloat(index);
            return @floatCast(val);
        }

        // Handle bool
        if (T == bool) {
            return self.stmt.columnBool(index);
        }

        // Handle strings - allocate a copy
        if (T == []const u8 or T == []u8) {
            if (self.stmt.columnText(index)) |txt| {
                return try allocator.dupe(u8, txt);
            }
            return "";
        }

        @compileError("Unsupported type for row mapping: " ++ @typeName(T));
    }

    /// A value that can hold any SQLite column type.
    pub const Value = union(ColumnType) {
        integer: i64,
        float: f64,
        text: []const u8,
        blob: []const u8,
        null_value: void,
    };

    /// Owned values for a complete row.
    pub const Values = struct {
        items: []Value,
        text_copies: [][]u8,
        allocator: std.mem.Allocator,

        pub fn deinit(self: *Values, allocator: std.mem.Allocator) void {
            for (self.text_copies) |copy| {
                allocator.free(copy);
            }
            allocator.free(self.text_copies);
            allocator.free(self.items);
        }
    };

    /// Converts the current row to owned Values.
    /// Text and blob values are copied so they persist after iteration.
    pub fn toValues(self: Self, allocator: std.mem.Allocator) !Values {
        const count = self.columnCount();
        const items = try allocator.alloc(Value, count);
        errdefer allocator.free(items);

        var text_copies = std.ArrayList([]u8).init(allocator);
        errdefer {
            for (text_copies.items) |copy| allocator.free(copy);
            text_copies.deinit();
        }

        for (0..count) |i| {
            const idx: u32 = @intCast(i);
            const col_type = self.columnType(idx);
            items[i] = switch (col_type) {
                .integer => .{ .integer = self.int(idx) },
                .float => .{ .float = self.float(idx) },
                .text => blk: {
                    if (self.text(idx)) |t| {
                        const copy = try allocator.dupe(u8, t);
                        try text_copies.append(copy);
                        break :blk .{ .text = copy };
                    }
                    break :blk .{ .null_value = {} };
                },
                .blob => blk: {
                    if (self.blob(idx)) |b| {
                        const copy = try allocator.dupe(u8, b);
                        try text_copies.append(copy);
                        break :blk .{ .blob = copy };
                    }
                    break :blk .{ .null_value = {} };
                },
                .null_value => .{ .null_value = {} },
            };
        }

        return Values{
            .items = items,
            .text_copies = try text_copies.toOwnedSlice(),
            .allocator = allocator,
        };
    }
};
