# DB-DuckDB **Repository Path**: skyter10086/DB-DuckDB ## Basic Information - **Project Name**: DB-DuckDB - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-23 - **Last Updated**: 2026-10-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- AIGC: Label: "1" ContentProducer: 001191440300708461136T1XGW3 ProduceID: aed9bd2c2f3c6f83a54981e0ddb92ca2_724505c8d20d11f1a301525400cd780f ReservedCode1: +vogNlngeEVl+qTqK/yJWEuRr0Yi3GbPewXpIrYdE7fphOtJMnw7Bsvdt0fRAAmI7Tk9PFbBoyB1GBgIvZfjx/8rz+wG3EERtW58s34+tFY/J6lRadSdGpNUTj3rfOxTRA+sj/4fssBIK4joEoBwBowmt4QJsLFB6ElGgCALccVYFkDCQjQ1lZ+f5fc= ContentPropagator: 001191440300708461136T1XGW3 PropagateID: aed9bd2c2f3c6f83a54981e0ddb92ca2_724505c8d20d11f1a301525400cd780f ReservedCode2: +vogNlngeEVl+qTqK/yJWEuRr0Yi3GbPewXpIrYdE7fphOtJMnw7Bsvdt0fRAAmI7Tk9PFbBoyB1GBgIvZfjx/8rz+wG3EERtW58s34+tFY/J6lRadSdGpNUTj3rfOxTRA+sj/4fssBIK4joEoBwBowmt4QJsLFB6ElGgCALccVYFkDCQjQ1lZ+f5fc= --- # DB::DuckDB A DuckDB driver for Raku's [DB](https://raku.land/zef:raku-community-modules/DB) interface, implemented as a direct [NativeCall](https://docs.raku.org/language/nativecall) binding over the DuckDB C API — no external process, no hand-maintained FFI layer. The C in the distribution is three small self-contained files under `resources/`, none of which includes `duckdb.h` or links libduckdb: a ~45-line pointer bridge (the two remaining by-value `duckdb_result` accessors cannot be called safely from NativeCall on SysV platforms; built on first use, skipped entirely on Windows), a `VARCHAR` bulk decoder, and an Arrow release bridge. The two shims are optional — without a compiler the driver uses slower pure Raku paths. ```raku use DB::DuckDB; my $db = DB::DuckDB.new; # in-memory # my $db = DB::DuckDB.new(path => 'data.duckdb'); # file backed say $db.query('select 42').value; # 42 $db.execute('create table foo (id integer, name varchar)'); $db.execute('insert into foo values (?, ?)', 1, 'a'); for $db.query('select * from foo').hashes -> %row { say %row, %row; } ``` ## Status Early but working. Implemented and covered by tests: * driver: `new`, `connect`, `query`, `execute`, `close`, `library-version` * connection: `prepare`, `prepare-nocache`, `query`, `execute`, `begin`, `commit`, `rollback`, `ping`, `finish` * statement: positional `?` and named `$name` placeholders (DuckDB syntax; `:name` is not supported), binding and rebinding, `execute` * result: `value`, `row`, `array`, `hash`, `arrays`, `hashes`, `column`, `column-view`, `column-views`, `column-arrays`, `:parallel`, `:degree`, `:stream`, `chunks`, `is-streaming`, `names`, `rows`, `rows-changed`, `free` * `DB::DuckDB::Arrow`: `arrow-chunk`, `with-arrow-chunks` — Arrow C data interface views, no Arrow extension and no NanoArrow Results are read chunk by chunk out of the vectors and decoded in Raku (`DB::DuckDB::Decoder`), because 2.0 stopped rendering several types through `duckdb_value_varchar`. `VARCHAR` columns are unpacked in bulk by a small C shim (see *Design notes*) and fall back to the per-value decoder when the shim cannot be built. Every chunk the driver fetches is destroyed again once its vector has been consumed or copied, including the chunks `column-views` holds for lazy decoding (released in `free`). Chunks that were never destroyed cost ~235 KB each, which is what made a 200k-row query leak ~23 MB per round; the whole suite now runs flat across hundreds of rounds. Not implemented (yet): callbacks/UDFs, extension loading, `Appender`, and anything that needs a by-value struct return (see *Design notes*). ## Requirements * Raku 6.d / Rakudo (tested on Rakudo 2025.06, Windows 11) * `DB`, plus `Alien::Libduckdb`, which puts a libduckdb where this module can find it: a prebuilt archive in seconds, a cmake source build as fallback (5-20 minutes). `zef` resolves both automatically. * *Optional:* a C compiler for the `VARCHAR` and Arrow shims. It is not required - without one the driver just logs a note and uses the slower pure Raku decoders (and the Raku Arrow release callback, which leaks on Windows). Set `DB_DUCKDB_CC` if the compiler is not called `cc`/`gcc`/`clang` and not in one of the usual Windows toolchain directories. ### The VARCHAR shim `resources/duckdb-varchar-shim.c` is a self-contained C file: it does not include `duckdb.h` and does not link libduckdb, it only knows the byte layout of `duckdb_string_t`. It is compiled on first use (one `gcc -O2 -shared`) into `~/.local/share/DB-DuckDB` and reloaded from there afterwards; it is rebuilt whenever the `.c` file it came from is newer. | variable | effect | |---|---| | `DB_DUCKDB_VARCHAR_SHIM` | path of a prebuilt shim library; skips the build | | `DB_DUCKDB_SHIM_CACHE` | where the built library is cached | | `DB_DUCKDB_CC` / `CC` | compiler to use | | `DB_DUCKDB_NO_VARCHAR_SHIM=1` | never use the shim (pure Raku decoder) | | `DB_DUCKDB_SHIM_TRACE` | path of a log file; the shim appends a line per step (survives a hard crash, unlike stderr) | The two paths are cross-checked in `t/04-varchar-shim.rakutest`: both digests of the same table have to match byte for byte. ### Why the Alien dependency is pinned The DuckDB C API is not ABI-stable across majors: `duckdb_result` is passed by value, the `DUCKDB_TYPE` enum was renumbered, and the by-value accessors moved. This binding is written and tested against **DuckDB 2.x**, so `META6.json` asks for exactly `Alien::Libduckdb:ver<1.1.0>` - the release that ships libduckdb 2.0.0 - instead of whatever is newest. Bumping that pin is a deliberate act: read `duckdb.h`, check the result struct layout, run `t/`, then bump. As a second line of defence, the driver calls `duckdb_library_version()` right before it opens its first database handle and refuses to continue when the loaded library is not 2.x: ``` DB::DuckDB: libduckdb v3.0.0 is not ABI-compatible with this binding, which targets DuckDB 2.x; library in use: C:\rakudo\bin\duckdb.dll. ... ``` That catches what the pin cannot: a `DB_DUCKDB_LIB` override, a stale `duckdb.dll` sitting next to `raku`, or a system libduckdb. ### How the library is located 1. `Alien::Libduckdb::shared-lib()` - the private prefix that `zef install` set up (this is the normal path) 2. `%DB_DUCKDB_LIB` environment variable, if it points at an existing file 3. `resources/libraries/duckdb` inside the distribution, if installed with one 4. next to the running `raku` executable - this covers `C:\rakudo\bin\duckdb.dll` 5. `/usr/local/lib`, `/usr/lib`, `/opt/homebrew/lib` 6. bare `duckdb`, letting the OS loader search ```powershell $env:DB_DUCKDB_LIB = 'C:\rakudo\bin\duckdb.dll' ``` ## Install ```sh zef install . ``` Or run straight from a checkout: ```sh raku -Ilib -e 'use DB::DuckDB; say DB::DuckDB.new.query("select version()").value' ``` ## Bulk writes Multi-row `INSERT` statements pay DuckDB's parser for every row. The appender API does not, and the driver exposes it at three levels: ```raku my $db = DB::DuckDB.new; # 1. the low-level handle my $ap = $db.appender('measurements'); $ap.append-row(1, 'x', 1.5); $ap.append-row([2, 'y', 2.5]); # a single Positional is unpacked $ap.free; # flush + close; raises on a failed cast # 2. whole batches (transactional by default) $db.insert-rows('measurements', ((3, 'z', 3.5), (4, Any, 4.5))); # 3. reuse an existing INSERT statement $db.execute-many('insert into measurements values (?, ?, ?)', ((5, 'w', 5.5), (6, 'v', 6.5))); ``` `insert-rows` and `execute-many` wrap the load in a transaction, so a batch that fails halfway leaves nothing behind. When the caller already has a transaction open (an explicit `begin`), the batch joins it instead of opening its own and nothing is committed early: ```raku $db.execute('begin'); $db.execute('insert into measurements values (7, NULL, 7.5)'); $db.insert-rows('measurements', ((8, 'u', 8.5),)); # joins the transaction $db.execute('rollback'); # both rows are gone ``` `execute-many` only routes to the appender when the statement is a plain `INSERT INTO t [(cols)] VALUES (?...)`; anything else ( `ON CONFLICT`, `RETURNING`, expressions, sub-selects) runs as one prepared statement inside the same transaction. ## Session options `threads`, `memory_limit` and `preserve_insertion_order` are pushed to every connection the driver opens, and can be changed on a live session: ```raku my $db = DB::DuckDB.new(:threads(8), :memory-limit('2GB'), :preserve-insertion-order(False)); $db.set-session-option('threads', 4); # applied immediately ``` `preserve_insertion_order => False` is only sent when it is turned off: `True` is DuckDB's own default, so the driver stays out of the engine's way. Set any other option with `set-session-option`. ## Zero-boxing column views `column()` always builds a plain `Array` and boxes every value into a Raku scalar. When you only want to scan or aggregate a column, `column-view` keeps DuckDB's own vector buffer and reads through it in place: ```raku my $r = $conn.query('SELECT id, amount, note FROM sales'); my $ids = $r.column-view(0); say $ids.type; # BIGINT say $ids.elems; # 5000 say $ids.int64(0); # no Mu allocation per value say $ids.sum; # walks the raw buffer, skips NULLs say $ids.min; # same say $ids.count; # non-NULL values say $ids.to-list; # List, built with a typed loop (fastest full read) say $ids.slice(0, 100); # a narrower view, no copy ``` `int64` / `num64` / `is-null` / `sum` / `min` / `max` / `count` / `mean` / `slice` work on the numeric types without boxing. `at($i)` returns a proper Raku value for every type, but for `VARCHAR`, dates, wide `DECIMAL` and nested types it goes through the same decoder the boxed path uses, so `to-list` is the better choice there. `$r.column-views` returns all columns at once. The view borrows the result's memory: it is only valid while `$r` is alive, and reading it after `$result.finish` raises. ## Parallel decoding `column-arrays` can decode the columns of a materialised result on several threads: ```raku my $r = $conn.query('SELECT * FROM wide_table'); my @cols = $r.column-arrays(:parallel); # all cores my @four = $r.column-arrays(:parallel, :degree(4)); # capped at 4 workers ``` The work is cut into `(column, chunk-span)` tasks and each worker writes into that column's preallocated array, so no memory is shared between workers and no value is copied twice. A result with more columns than workers spreads the columns over the workers; a result with fewer columns splits each column's chunks into contiguous spans and stitches the spans back in chunk order, so row order is preserved and the output is identical to the serial path (checked value by value in `t/08-parallel.rakutest`). `DB_DUCKDB_DEGREE` in the environment sets the default worker count. Measured on this machine, 1M rows, wall clock for the whole decode (median of four runs per shape; this is a 24-core box and the spread between runs is wide because GC dominates). The numbers predate the weight-based split below; the one- and two-column rows are exactly the lopsided cases that motivated it, so treat them as a lower bound: | shape | serial | `:parallel` | speedup | |---|---|---|---| | 1 x `VARCHAR` | 4.7 s | 1.9 s | 2.4x | | 5 numeric columns | 8.5 s | 3.4 s | 2.2x | | 4 numeric columns | 5.2 s | 2.6 s | 2.0x | | 7 mixed columns | 22.5 s | 10.5 s | 2.1x | | 3 mixed columns | 9.2 s | 6.5 s | 1.3x | | 2 mixed columns | 5.5 s | 4.8 s | 1.1x | Parallelism pays where the per-value cost is high (`VARCHAR` unpacking, `DECIMAL` formatting) or where there are many columns to spread. Results with only one or two columns are bound by the boxing step and by Raku's GC -- both shared between threads -- so the gain there is small. Below 20 000 rows the driver stays serial, because setting the workers up costs more than the decode. ### Splitting by column weight Cutting every column into the same number of spans spreads the *work* evenly only if the columns cost the same. They do not: a `VARCHAR` value costs about four BIGINTs to decode, a nested value about three, `DECIMAL`/`HUGEINT` about two. With two such columns and four workers, an even split of spans leaves the cheap column's worker idle while the expensive one still runs, and a lopsided result can end up *slower* than the serial path. So the span count per column is `ceil(nt * w / sum-of-weights)`, capped at both `nt` and the chunk count, with weights `VARCHAR` 4, `LIST`/`STRUCT`/`MAP`/ `ARRAY`/`UNION` 3, `DECIMAL`/`HUGEINT` 2, everything else 1. Plain numeric columns are left whole unless they are the only column, because stitching a numeric column back costs a copy that cancels the gain. Measured on 200k rows of `BIGINT + VARCHAR(40)`, 20 interleaved rounds in one process, paired median against the serial path: | workers | even split | by weight | |---|---|---| | `:degree(2)` | 1.22-1.24x | 1.20-1.38x | | `:degree(4)` | 0.92-0.93x | 1.29-1.32x | The point is the second row: adding workers no longer costs speed. Absolute ratios move by up to 20% with machine load. Two cases cannot be split and fall back to the serial path automatically: a streaming result (its chunks belong to the stream, not to a chunk list) and a result that was already partially read. ## Streaming results By default a result is materialised -- DuckDB runs the query to completion before the first row is available. `:stream` keeps the query pulling row batches on demand: ```raku my $r = $conn.query('SELECT * FROM huge', :stream); $r.is-streaming; # True for $r.chunks -> $chunk { # $chunk.size -- rows in this vector (<= 2048, the last one shorter) # $chunk.column-count my $view = $chunk.column-view(0); ... $view.sum ... my @row = $chunk.row(0); # boxed values, for the odd row my @rows = $chunk.to-rows; # boxed, for small chunks } ``` **A chunk is valid only until the next one is pulled.** DuckDB recycles the vector, so the driver expires the previous chunk (and every view it handed out) before fetching the next one; any read through a stale chunk or view raises instead of returning another row's data. That is the trade-off for not copying: consume inside the loop body and let the chunk go. `$chunk.is-valid` reports whether the driver has moved on; `.size` and `.column-count` stay readable either way, because they are plain numbers rather than pointers. Measured on this machine, 1M rows x 3 columns (`bigint`, `double`, `varchar`): first usable chunk after **12 ms**, versus **5.8 s** to materialise the whole result through `column-arrays`; summing the `bigint` column while streaming takes 3.9 s and agrees exactly with `SELECT sum(a)`. ### Prefetching `chunks` used to take turns with the engine: fetch, decode, fetch. The engine computes while you decode nothing, and sits idle while you decode. A worker thread now pulls the next chunk while the caller works on the current one. The queue is two slots deep, not more: a chunk is ~235 KB and decoding it costs several times what fetching it costs, so a deeper queue would only raise time to first chunk and memory. Two slots are enforced with a token channel -- the worker takes a token before fetching, the caller returns one when it drops the previous chunk -- so at most two chunks are alive. Nothing changes at the API: `free` stops the worker, joins it and destroys anything still queued before the result goes away, so a stream abandoned after one chunk is safe. Measured 300k rows, 7-9 interleaved rounds: **1.06-1.13x** on a decode-bound full scan, **1.03-1.18x** on an engine-bound streaming aggregation, time to first chunk unchanged, RSS plateau +17 MB. `DB_DUCKDB_NO_PREFETCH=1` restores the one-at-a-time path. ## Arrow views `DB::DuckDB::Arrow` exposes a chunk of a materialised result through the Arrow C data interface, so the buffers can be handed to another component instead of being boxed into Raku values here: ```raku use DB::DuckDB::Arrow; my $r = $conn.query('SELECT id, tags FROM items'); my $chunk = arrow-chunk($r, 0); # 0-based chunk index, 2048 rows each my $col = $chunk.column(1); # LIST say $chunk.length; # rows in this chunk say $chunk.columns; say $col.null-count; say $col.children[0].strs; # the child vector of the list $chunk.free; # idempotent with-arrow-chunks($r, -> $c, $i { ... }); # frees every chunk for you ``` `with-arrow-chunks` hands each chunk and its index to the block and frees the chunk on the way out, so nothing leaks if the block dies. Chunks are numbered the same way as `arrow-chunk`. `Column` gives typed access without boxing: `.ints`, `.int32s`, `.nums`, `.bools`, `.strs` (each returns a `List`), `.is-null($i)`, `.children` for recursing into `LIST`/`STRUCT`, plus the raw `.buffer($j)` pointer and `.length` for talking to something that wants the buffers themselves. `Chunk` also exposes `.addr` for the `ArrowArray*` struct. There is no `LOAD arrow` and no NanoArrow dependency: the Arrow C data interface is two ten-field structs, so `ArrowArray` / `ArrowSchema` are declared directly as `repr('CStruct')`. This is not a faster read path. `strs` has to follow the offset buffer and copy each value out, which on 200k rows is 6.3 s against 1.7 s for the bulk `join-varchar` decoder used by `column()` -- about 3.8x slower. Use it for nested types, for passing data to other libraries, or when you want the Arrow buffers as they are. Releasing an Arrow array means calling a callback pointer, and doing that from Raku leaked on Windows (~174 MB to ~711 MB over 30k releases); the small `resources/duckdb-arrow-shim.c` moves the indirect call to the C side and keeps it flat. It is built on demand next to the VARCHAR shim. `DB_DUCKDB_ARROW_SHIM` points at a prebuilt library, `DB_DUCKDB_ARROW_DEBUG=1` logs which release path is taken. A chunk fetched from the result is owned by the caller: `arrow-chunk` destroys it right after the conversion has copied the data into Arrow-owned buffers, so the Arrow view does not keep the DuckDB result alive. ## Environment switches | variable | effect | |---|---| | `DB_DUCKDB_DEGREE` | default worker count for `column-arrays(:parallel)` | | `DB_DUCKDB_NO_PREFETCH=1` | one chunk at a time in `chunks`, no prefetch worker | | `DB_DUCKDB_NO_COLUMN_VIEW=1` | `column-views` falls back to plain `column-arrays` (decodes eagerly, holds nothing) | | `DB_DUCKDB_ARROW_DEBUG=1` | log which Arrow release path is taken | | `DB_DUCKDB_ARROW_SHIM` | path of a prebuilt Arrow release shim | | `DB_DUCKDB_LIB` | path of the libduckdb to load | | `DB_DUCKDB_NO_VARCHAR_JOIN=1` | per-value `VARCHAR` decoding, no join buffer | | `DB_DUCKDB_NO_VARCHAR_SHIM=1` | no C shim at all, pure Raku decoder | The shim build variables (`DB_DUCKDB_VARCHAR_SHIM`, `DB_DUCKDB_SHIM_CACHE`, `DB_DUCKDB_CC`, `DB_DUCKDB_SHIM_TRACE`) are listed under *The VARCHAR shim*. ## Type mapping (DuckDB → Raku) | DuckDB type | Raku value | |---|---| | `BOOLEAN` | `Bool` | | `TINYINT` / `SMALLINT` / `INTEGER` / `BIGINT` | `Int` | | `UTINYINT` / `USMALLINT` / `UINTEGER` / `UBIGINT` | `Int` | | `FLOAT` / `DOUBLE` | `Num` | | `VARCHAR` | `Str` | | `NULL` (any type) | `Any` (undefined) | | `DATE`, `TIME`, `TIMESTAMP`, `TIMESTAMP_S/_MS/_NS`, `TIME_NS` | `Str` in DuckDB's ISO format, rendered from the integer payload (byte identical to `CAST(x AS VARCHAR)`) | | `TIME_TZ`, `TIMESTAMP_TZ`, `TIMESTAMP_TZ_NS` | `Str` in DuckDB's own ISO rendering (session time zone honoured) | | `DECIMAL`, `HUGEINT` / `UHUGEINT` | `Str`, exact and lossless | | `BLOB`, `UUID`, `ENUM`, `INTERVAL`, `LIST`, `STRUCT`, `MAP`, `UNION`, `BIT`, `BIGNUM`, `ARRAY` | `Str` in DuckDB's textual rendering | | `SQLNULL` type | `Any` | Raku → DuckDB (bind): `Bool` → `BOOLEAN`, `Int` → `BIGINT`, `Num` / `Rat` / `FatRat` → `DOUBLE`, `Str` → `VARCHAR`, `Blob` → `BLOB`, undefined → `NULL`, anything else → its `.Str`. Dates and decimals are deliberately *not* auto-coerced into Raku `Date`/`DateTime`/`Rat`: DuckDB's own textual rendering is exact for every type, while the `duckdb_value_*` family that would return real date/decimal structures is deprecated and partly by-value. Exactness beats convenience here — convert at the call site when you need it. ## Design notes * **One database instance per driver.** `connect` creates connections that all share a single `duckdb_database`. Opening `:memory:` twice would silently create two independent databases, and opening a file path twice trips DuckDB's instance/file locking, so the driver opens once and guards it with `$!opened`. * **`duckdb_result` is mapped as a fixed-layout struct** (48 bytes on LP64: three `idx_t` plus three pointers) and its size is asserted at load time, so an ABI change fails loudly instead of corrupting memory. * **Results are read through the chunk/vector API.** `duckdb_result_chunk_count` / `duckdb_fetch_chunk` hand back a `duckdb_data_chunk` pointer, and each vector's data buffer is read in place; the structs living inside those buffers are dissected field by field through `uint32`/`uint64` views, since NativeCall faults on `CStruct` array element access on Windows. No struct is ever returned or taken by value, with one deliberate exception: the two accessors that still declare a by-value `duckdb_result` are called through `DB::DuckDB::Bridge`, which passes the same 48 bytes as a pointer. NativeCall's by-value marshalling happens to work under MS x64 but is miscompiled under SysV, where it surfaced as `unique_ptr` NULL crashes inside DuckDB. * **Values are rendered exactly like DuckDB's own `CAST(x AS VARCHAR)`**, not coerced into Raku `Date`/`DateTime`/`Rat`. `TIMESTAMP_TZ` is the one piece of session state involved: the vector holds UTC microseconds, so the instant is shifted by the session zone's offset (synced from `strftime(..., '%z')` on construction and refreshed after `SET TimeZone`) and only then labelled with that offset. * **Prepared statements are cached per connection** by the `DB` role; statements are destroyed when their connection is dropped. `close` also tears down connections the caller forgot to `finish`, because DuckDB will not release the file lock until every connection and prepared statement is gone. * **`VARCHAR` is unpacked in bulk by a C shim.** Decoding a string column one `duckdb_string_t` at a time and `Buf.push`-ing each payload was the slowest thing in result reading (and pure-Raku batching was slower still, because every payload still had to be copied byte by byte). `duckdb-varchar-shim.c` snapshots the vector's `duckdb_string_t` array once, then copies every payload into one flat buffer while writing an offsets table, a lengths table and a NULL bitmap; Raku decodes that buffer once and slices values out with `substr`. Measured 2.7-3.4x on `column()` and 1.5-1.8x on `arrays()`/`hashes()` at 100k-1M rows. The shim never trusts the pointers it is handed: the `string_t` array is copied into memory it owns before being walked, out-of-line payloads are read through a probing copy, a payload longer than 16 MiB (the signature of a selection buffer mistaken for a string vector) and any unreadable pointer make it return `DKD_NOT_FLAT`, which is the signal to fall back to the per-value decoder for that chunk. * **Temporal columns are formatted by the driver, not by DuckDB.** A `DATE` is an int32 day count and the `TIMESTAMP*` / `TIME*` types are int64 counters, so the text can be computed directly from the payload instead of asking DuckDB to `CAST` each value. `dkd_temporal_join` in the same shim renders a whole chunk into one `0x01`-separated buffer (a `NULL` becomes `0x02`) that Raku splits once, and a pure Raku renderer produces exactly the same text when no compiler is around. Measured on 1M rows against DuckDB's own cast: `DATE` 8.38 → 1.38, `TIMESTAMP` 23.35 → 1.22, `TIME` 4.58 → 1.27 µs/value, and a three column `arrays()` read 35.88 → 4.78 µs/row (the cast itself costs 1.30 µs/value, so this is the floor). The text matches the `CAST` to the byte, including the `(BC)` suffix on pre-epoch dates, `24:00:00`, and fractional seconds with trailing zeros removed. `TIME_TZ` / `TIMESTAMP_TZ` / `TIMESTAMP_TZ_NS` stay on DuckDB's own rendering because their text carries the session zone. Set `DB_DUCKDB_NO_TEMPORAL_SHIM=1` to force the Raku renderer. * **Works on Windows without an import library**: the DLL is loaded at runtime, so the MSVC `.lib` that ships with the C API is not needed. ## Tests ```sh raku -Ilib t/01-basic.rakutest # types, placeholders, transactions, errors raku -Ilib t/02-file.rakutest # file database, multiple connections, reopen raku -Ilib t/03-types.rakutest # every 2.0 type vs. DuckDB's own text rendering, # NULLs, chunked results, session time zone, uuid raku -Ilib t/04-varchar-shim.rakutest # the VARCHAR shim, including its fallbacks raku -Ilib t/05-appender.rakutest # appender, insert-rows, type mapping raku -Ilib t/06-column-view.rakutest # column-views vs column-arrays, all types raku -Ilib t/07-streaming.rakutest # chunks, 2048-row vectors, chunk lifetime, # prefetch vs one-at-a-time raku -Ilib t/08-parallel.rakutest # parallel column decode vs the serial path, # including lopsided results at 2/3/4/8 workers raku -Ilib t/09-lazy-view.rakutest # lazily decoded views and their cleanup raku -Ilib t/10-varchar-join.rakutest # the join-buffer VARCHAR decoder and its fallbacks raku -Ilib t/11-arrow.rakutest # Arrow C data interface views, nested types, release raku -Ilib t/12-temporal.rakutest # date/time/timestamp rendering vs DuckDB's own cast, # pre-epoch and extreme values, NULLs, lazy views ``` No `prove` harness is required -- each file is a standalone `Test` script. ## License Artistic-2.0 *(内容由AI生成,仅供参考)* *(内容由AI生成,仅供参考)* *(内容由AI生成,仅供参考)*