From 8efa7cb0013aae1708e2d815b7fa65f055062159 Mon Sep 17 00:00:00 2001
From: Ekaitz Zarraga <ekaitz@elenq.tech>
Date: Mon, 5 Aug 2024 22:20:19 +0200
Subject: Results with proper conversion

---
 src/duckdb/Column.zig   | 204 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/duckdb/Database.zig |  34 +++++++-
 src/duckdb/Result.zig   |  18 ++---
 src/duckdb/Types.zig    |  66 ----------------
 4 files changed, 244 insertions(+), 78 deletions(-)
 create mode 100644 src/duckdb/Column.zig
 delete mode 100644 src/duckdb/Types.zig

(limited to 'src/duckdb')

diff --git a/src/duckdb/Column.zig b/src/duckdb/Column.zig
new file mode 100644
index 0000000..fb505bb
--- /dev/null
+++ b/src/duckdb/Column.zig
@@ -0,0 +1,204 @@
+const std   = @import("std");
+const c = @cImport({
+    @cInclude("duckdb.h");
+});
+
+
+const DuckdbType = enum {
+    BOOLEAN,
+    TINYINT,
+    SMALLINT,
+    INTEGER,
+    BIGINT,
+    UTINYINT,
+    USMALLINT,
+    UINTEGER,
+    UBIGINT,
+    FLOAT,
+    DOUBLE,
+    TIMESTAMP,
+    DATE,
+    TIME,
+    INTERVAL,
+    HUGEINT,
+    UHUGEINT,
+    VARCHAR,
+    BLOB,
+    TIMESTAMP_S,
+    TIMESTAMP_MS,
+    TIMESTAMP_NS,
+    UUID,
+    TIME_TZ,
+    TIMESTAMP_TZ,
+};
+
+pub const Column = union(DuckdbType) {
+    BOOLEAN:      [*]bool,
+    TINYINT:      [*]i8,
+    SMALLINT:     [*]i16,
+    INTEGER:      [*]i32,
+    BIGINT:       [*]i64,
+    UTINYINT:     [*]u8,
+    USMALLINT:    [*]u16,
+    UINTEGER:     [*]u32,
+    UBIGINT:      [*]u64,
+    FLOAT:        [*]f32,
+    DOUBLE:       [*]f64,
+    TIMESTAMP:    [*]c.duckdb_timestamp,
+    DATE:         [*]c.duckdb_date,
+    TIME:         [*]c.duckdb_time,
+    INTERVAL:     [*]c.duckdb_interval,
+    HUGEINT:      [*]c.duckdb_hugeint,
+    UHUGEINT:     [*]c.duckdb_uhugeint,
+    VARCHAR:      [*]c.duckdb_string_t,
+    BLOB:         [*]c.duckdb_string_t,
+    TIMESTAMP_S:  [*]c.duckdb_timestamp,
+    TIMESTAMP_MS: [*]c.duckdb_timestamp,
+    TIMESTAMP_NS: [*]c.duckdb_timestamp,
+    UUID:         [*]c.duckdb_hugeint,
+    TIME_TZ:      [*]c.duckdb_time_tz,
+    TIMESTAMP_TZ: [*]c.duckdb_timestamp,
+
+    const Self = @This();
+
+    pub fn fromType(column_type: c.DUCKDB_TYPE, column: *anyopaque) Column {
+        return switch (column_type) {
+            c.DUCKDB_TYPE_BOOLEAN      => .{.BOOLEAN      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TINYINT      => .{.TINYINT      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_SMALLINT     => .{.SMALLINT     = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_INTEGER      => .{.INTEGER      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_BIGINT       => .{.BIGINT       = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_UTINYINT     => .{.UTINYINT     = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_USMALLINT    => .{.USMALLINT    = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_UINTEGER     => .{.UINTEGER     = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_UBIGINT      => .{.UBIGINT      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_FLOAT        => .{.FLOAT        = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_DOUBLE       => .{.DOUBLE       = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_VARCHAR      => .{.VARCHAR      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_BLOB         => .{.BLOB         = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIMESTAMP    => .{.TIMESTAMP    = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_DATE         => .{.DATE         = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIME         => .{.TIME         = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_INTERVAL     => .{.INTERVAL     = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_HUGEINT      => .{.HUGEINT      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_UHUGEINT     => .{.UHUGEINT     = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIMESTAMP_S  => .{.TIMESTAMP_S  = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIMESTAMP_MS => .{.TIMESTAMP_MS = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIMESTAMP_NS => .{.TIMESTAMP_NS = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_UUID         => .{.UUID         = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIME_TZ      => .{.TIME_TZ      = @alignCast(@ptrCast(column))},
+            c.DUCKDB_TYPE_TIMESTAMP_TZ => .{.TIMESTAMP_TZ = @alignCast(@ptrCast(column))},
+            else => unreachable,
+        };
+    }
+
+    pub fn getAs(self: Self, i: usize, T: anytype) !T{
+        const ti : std.builtin.Type = @typeInfo(T);
+        switch (self){
+            .BOOLEAN
+                => |ptr| switch(ti) {
+                .Bool => return ptr[i],
+                .Int  => return @intFromBool(ptr[i]),
+                else  => return error.InvalidConversion,
+            },
+            .TINYINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .SMALLINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .INTEGER
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .BIGINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .UTINYINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .USMALLINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .UINTEGER
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .UBIGINT
+                => |ptr| switch(ti) {
+                .Int    => return @intCast(ptr[i]),
+                .Float  => return @floatFromInt(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .FLOAT
+                => |ptr| switch(ti) {
+                .Int    => return @intFromFloat(ptr[i]),
+                .Float  => return @floatCast(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .DOUBLE
+                => |ptr| switch(ti) {
+                .Int    => return @intFromFloat(ptr[i]),
+                .Float  => return @floatCast(ptr[i]),
+                else    => return error.InvalidConversion,
+            },
+            .VARCHAR,
+            .BLOB
+                => |ptr| switch(ti){
+                .Pointer => |p| switch (p.size) {
+                    .Slice => {
+                        if ( p.child == u8 ) {
+                            var result: T = undefined;
+                            if (c.duckdb_string_is_inlined(ptr[i])){
+                                result = &ptr[i].value.inlined.inlined;
+                                result.len = ptr[i].value.inlined.length;
+                                return result;
+                            } else {
+                                result.len = ptr[i].value.pointer.length;
+                                result.ptr = ptr[i].value.pointer.ptr;
+                                return result;
+                            }
+                        } else {
+                            @compileError("Invalid type for output data: "
+                                ++ @typeName(T));
+                        }
+                    },
+                    else    => return error.InvalidConversion,
+                },
+                else    => return error.InvalidConversion,
+            },
+            .TIMESTAMP,
+            .DATE,
+            .TIME,
+            .INTERVAL,
+            .HUGEINT,
+            .UHUGEINT,
+            .TIMESTAMP_S,
+            .TIMESTAMP_MS,
+            .TIMESTAMP_NS,
+            .UUID,
+            .TIME_TZ,
+            .TIMESTAMP_TZ =>
+                return error.ConversionNotImplemented,
+        }
+    }
+};
diff --git a/src/duckdb/Database.zig b/src/duckdb/Database.zig
index ca2a03a..ffdd052 100644
--- a/src/duckdb/Database.zig
+++ b/src/duckdb/Database.zig
@@ -72,7 +72,39 @@ test "Simple querying" {
     try std.testing.expect(z.segund == null);
 }
 
-test "Checks if all fields are captured" {
+test "Simple querying with conversion" {
+    var database = try Self.init(":memory:");
+    defer database.deinit();
+    var connection = try database.connect();
+    defer connection.deinit();
+
+    const s : type = comptime struct {
+        primer: f64, // Converting to f64
+        segund: ?i32
+    };
+
+    try connection.run("CREATE TABLE integers (i INTEGER NOT NULL, j INTEGER);");
+    try connection.run("INSERT INTO integers VALUES (3, 4), (5, 6), (7, NULL);");
+    var result = try connection.query("SELECT * FROM integers;", s);
+    defer result.deinit();
+
+
+    try std.testing.expect(2 == result.getColumnCount());
+
+    var z = try result.next();
+    try std.testing.expect(z.primer == 3.0);
+    try std.testing.expect(z.segund.? == 4);
+
+    z = try result.next();
+    try std.testing.expect(z.primer == 5.0);
+    try std.testing.expect(z.segund.? == 6);
+
+    z = try result.next();
+    try std.testing.expect(z.primer == 7.0);
+    try std.testing.expect(z.segund == null);
+}
+
+test "All fields are captured" {
     var database = try Self.init(":memory:");
     defer database.deinit();
     var connection = try database.connect();
diff --git a/src/duckdb/Result.zig b/src/duckdb/Result.zig
index 61a30ce..87d9f6f 100644
--- a/src/duckdb/Result.zig
+++ b/src/duckdb/Result.zig
@@ -1,5 +1,5 @@
 const std   = @import("std");
-const types = @import("Types.zig");
+const Column = @import("Column.zig").Column;
 const c = @cImport({
     @cInclude("duckdb.h");
 });
@@ -98,10 +98,7 @@ pub fn Result(comptime T: type) type{
             inline for (fields, 0..) |field, i| {
                 // TODO: check compatibility between the column type and
                 // the struct provided as result container
-                //
                 const column_type = c.duckdb_column_type(&self._res, i);
-                // std.debug.print("tipo => {any}\n", .{column_type});
-                // std.debug.print("{any}\n", .{self._data});
 
                 // Check validity
                 const entry_idx     :usize = self._current_row / 64;
@@ -115,21 +112,20 @@ pub fn Result(comptime T: type) type{
 
                 // Store the column in current row
                 if (is_valid){
-                    // Unwrap the Optional
+                    // Unwrap the output Optional
                     const t = switch (@typeInfo(field.type)){
                         .Optional => |t| t.child,
                         else => field.type
                     };
-                    // Unpack the C array of duckdb_type
-                    const col: [*]types.unpack_type(t) = @alignCast(@ptrCast(self._data[i]));
-                    std.debug.assert(types.valid_unpack(column_type, t));
+                    // Obtain and convert column to something we can process
+                    const column = Column.fromType(column_type, self._data[i].?);
 
-                    // Convert to Zig data type
-                    @field(result, field.name) = try types.cast(&col[self._current_row], t);
+                    // Convert to Zig data type and store in output struct
+                    @field(result, field.name) = try column.getAs(self._current_row, t);
                 } else {
                     // Got a NULL from the DB
                     if (@typeInfo(field.type) != .Optional){
-                        // Cannot return it because it's not optional
+                        // Cannot return it because output it's not optional
                         return error.NullInNotOptional;
                     }
                     @field(result, field.name) = null;
diff --git a/src/duckdb/Types.zig b/src/duckdb/Types.zig
deleted file mode 100644
index 2a12cd2..0000000
--- a/src/duckdb/Types.zig
+++ /dev/null
@@ -1,66 +0,0 @@
-const std = @import("std");
-const c = @cImport({
-    @cInclude("duckdb.h");
-});
-
-pub fn unpack_type(T: type) type {
-    return switch(@typeInfo(T)) {
-        .Bool, .Int, .Float => T,
-        .Pointer => |p| switch (p.size) {
-            .Slice => if (p.child == u8) c.duckdb_string_t  else
-                @compileError("Invalid type for output data"),
-            else => @compileError("Invalid type for output data")
-        },
-        // .Array =>
-        // .Struct =>
-        // .Enum =>
-        // .Union =>
-        else => @compileError("Invalid type for output data")
-    };
-}
-
-pub fn valid_unpack(column_type: c.DUCKDB_TYPE, T: type) bool {
-    switch (column_type) {
-        c.DUCKDB_TYPE_BOOLEAN       => if(T == bool) return true else return false,
-        c.DUCKDB_TYPE_TINYINT       => if(T == i8  ) return true else return false,
-        c.DUCKDB_TYPE_SMALLINT      => if(T == i16 ) return true else return false,
-        c.DUCKDB_TYPE_INTEGER       => if(T == i32 ) return true else return false,
-        c.DUCKDB_TYPE_BIGINT        => if(T == i64 ) return true else return false,
-        c.DUCKDB_TYPE_UTINYINT      => if(T == u8  ) return true else return false,
-        c.DUCKDB_TYPE_USMALLINT     => if(T == u16 ) return true else return false,
-        c.DUCKDB_TYPE_UINTEGER      => if(T == u32 ) return true else return false,
-        c.DUCKDB_TYPE_UBIGINT       => if(T == u64 ) return true else return false,
-        c.DUCKDB_TYPE_FLOAT         => if(T == f32 ) return true else return false,
-        c.DUCKDB_TYPE_DOUBLE        => if(T == f64 ) return true else return false,
-        c.DUCKDB_TYPE_VARCHAR       => if(T == []const u8) return true else return false,
-        c.DUCKDB_TYPE_BLOB          => if(T == []const u8) return true else return false,
-        else => return false,
-    }
-}
-
-/// Receives a pointer of the element (!)
-pub fn cast(el: anytype, T: type) !T {
-    return switch (@typeInfo(T)) {
-        .Bool, .Int, .Float =>  el.*,
-        .Pointer => |p| switch (p.size) {
-            .Slice =>  blk: {
-                if ( p.child == u8 ) {
-                    var result: T = undefined;
-                    if (c.duckdb_string_is_inlined(el.*)){
-                        result = &el.value.inlined.inlined;
-                        result.len = el.value.inlined.length;
-                        break :blk result;
-                    } else {
-                        result.len = el.value.pointer.length;
-                        result.ptr = el.value.pointer.ptr;
-                        break :blk result;
-                    }
-                } else {
-                    @compileError("Invalid type for output data");
-                }
-            },
-            else => @compileError("Invalid type for output data")
-        },
-        else => @compileError("Invalid type for output data")
-    };
-}
-- 
cgit v1.2.3