Another update thanks to the amazingly evolving Mojo type-level programming and reflection support.
A lot of the design was stolen from inspired by EmberJson (thanks @bgreni).
Reflection-based typed API
Type mapping between Mojo and DuckDB types is now derived automatically at compile time via struct reflection. The old Col[T] approach is removed in favor of get[T] methods taking the actual target type as parameters, including NULL handling.
get[T] expects non-null values and raises at runtime if a NULL is encountered. You can use get[Optional[T]] when nulls are expected:
var chunk = result.fetch_chunk()
var name = chunk.get[String](col=0, row=0) # raises on NULL
var maybe = chunk.get[Optional[String]](col=0, row=0) # None on NULL
var ages = chunk.get[Int64](col=1) # whole column as List[Int64]
var ages = chunk.get[Optional[Int64]](col=1) # whole column as List[Optional[Int64]]
Type mismatches (e.g. get[Int64] on a VARCHAR column) are caught and raise with a descriptive error.
Supported types so far: Bool, Int8–Int64, UInt8–UInt64, Float32, Float64, String, Date, Time, Timestamp, Interval, List[T], Optional[T], and nested combinations.
Struct decoding
You can deserialize full rows into Mojo structs — fields are matched to columns by position:
@fieldwise_init
struct User(Copyable, Movable):
var name: String
var age: Int64
var user = chunk.get[User](row=0) # single row
var users = chunk.get[User]() # all rows as List[User]
The NULL handling also applies to struct fields — non-Optional fields raise on NULL, Optional fields allow it:
@fieldwise_init
struct Record(Copyable, Movable):
var id: Int64 # must not be NULL
var nickname: Optional[String] # can be NULL
Recursive decoding of nested types
Nested DuckDB structs, lists , and mixed nesting all work with the typed API:
con = DuckDB.connect(":memory:")
_ = con.execute("CREATE TABLE t (nums INTEGER[])")
_ = con.execute("INSERT INTO t VALUES ([10, 20]), ([30])")
result = con.execute("SELECT nums FROM t")
var chunk = result.fetch_chunk()
var lists = chunk.get[List[Optional[Int32]]](col=0)
assert_equal(len(lists), 2)
var row0 = lists[0].copy()
assert_equal(len(row0), 2)
assert_equal(row0[0].value(), 10)
assert_equal(row0[1].value(), 20)
var row1 = lists[1].copy()
assert_equal(len(row1), 1)
assert_equal(row1[0].value(), 30)
Nested Mojo structs:
@fieldwise_init
struct Point(Copyable, Movable):
"""Simple 2D point with two Float64 fields."""
var x: Float64
var y: Float64
@fieldwise_init
struct LabeledPoint(Copyable, Movable):
var label: String
var pt: Point
con = DuckDB.connect(":memory:")
_ = con.execute(
"CREATE TABLE t (label VARCHAR, pt STRUCT(x DOUBLE, y DOUBLE))"
)
_ = con.execute("INSERT INTO t VALUES ('origin', {'x': 0.0, 'y': 0.0})")
_ = con.execute(
"INSERT INTO t VALUES ('offset', {'x': 1.5, 'y': 2.5})"
)
var result = con.execute("SELECT label, pt FROM t")
var chunk = result.fetch_chunk()
var row0 = chunk.get[LabeledPoint](row=0)
assert_equal(row0.label, "origin")
assert_equal(row0.pt.x, 0.0)
assert_equal(row0.pt.y, 0.0)
var row1 = chunk.get[LabeledPoint](row=1)
assert_equal(row1.label, "offset")
assert_equal(row1.pt.x, 1.5)
assert_equal(row1.pt.y, 2.5)