DB::DuckDB — Raku-DB模块接口契约逐字报告

Raku DB 模块接口契约逐字报告

目标:为新建 DB::DuckDB 驱动提供照抄级接口基线。 采集日期:2026-10-22 采集对象:本机 Rakudo(C:\rakudo\bin\raku.exe)已安装的 DB 发行版与其参考驱动 DB::SQLite。

0. 定位结果与可信度说明

0.1 定位过程

检查位置结果
C:\rakudo\share\perl6\site\lib\无 *.rakumod(Get-ChildItem -Recurse -Filter *.rakumod 计数 = 0),仅有 .precomp\ 缓存
C:\rakudo\share\perl6\vendor\lib\不存在
C:\rakudo\share\perl6\lib\仅 Perl6\、Raku\(core)
C:\Users\Admin\.raku\lib\不存在
zef / ~/.raku/dist、site/dist存在 DB 与 DB::SQLite 的 META6 记录(见 §0.2)
site/sources\<SHA1>DB 与 DB::SQLite 全部 .rakumod 源文件原文存在(内容寻址快照)

结论:DB 模块的 .rakumod 文件已不在 lib/ 下,但 Rakudo 的 precomp 源码快照(C:\rakudo\share\perl6\site\sources\,文件名为内容 SHA1)完整保留了逐字源码,且 use DB; 仍可从 precomp 缓存正常加载(已实测 raku -e 'use DB; say 42' → 42)。

0.2 发行版元数据(META6 原文,C:\rakudo\share\perl6\site\dist\A8844EDD7E4DD10FC8035648876F7B558A70BDBA)

{"api":"1","auth":"github:CurtTilmes","authors":["Curt Tilmes <[email protected]>"],"depends":{"runtime":{"requires":["Concurrent::Stack"]},"test":{"requires":["Test","Test::When"]}},"description":"Base roles for DB::* family of SQL Database modules","files":{},"license":"BSD-2-Clause","meta-version":"1","name":"DB","perl":"6.c","provides":{"DB":{"lib/DB.rakumod":{"file":"03DA109C5B8532E87FCA813A145C35AABBCB9BC8","time":null}},"DB::Connection":{"lib/DB/Connection.rakumod":{"file":"7D31F71E20FBC2186DACFFB1F4D67A911166094C","time":null}},"DB::Mock":{"lib/DB/Mock.rakumod":{"file":"48F58C267EEE065A1956AC0693126BB4D6CFF5A4","time":null}},"DB::Result":{"lib/DB/Result.rakumod":{"file":"95A7DEA39A879FBDF062CFC4BD669BE298F3078A","time":null}},"DB::Statement":{"lib/DB/Statement.rakumod":{"file":"E4E2D72E6404EF6F25B5680D2EC27229A5F0C795","time":null}}},"support":{"bugtracker":"https://github.com/CurtTilmes/raku-db/issues","email":"[email protected]","source":"https://github.com/CurtTilmes/raku-db.git"},"tags":["DATABASE"],"test-depends":["Test","Test::META","Test::When"],"ver":"0.5","version":"0.5"}

参考驱动 META6(site\dist\31271DE6533212DE6B4B2262DB0EB5066A695DCA):

{"api":"1","auth":"github:CurtTilmes","authors":["Curt Tilmes <[email protected]>"],"depends":{"runtime":{"requires":["BitEnum","DB","NativeLibs:ver<0.0.7+>:auth<github:salortiz>","sqlite3:from<native>"]},"test":{"requires":["Test","Test::When"]}},"description":"SQLite access for Raku","files":{},"license":"BSD-2-Clause","meta-version":"1","name":"DB::SQLite","perl":"6.c","provides":{"DB::SQLite":{"lib/DB/SQLite.rakumod":{"file":"79F654BF6F54F73FCB982ADA494C8AB7BB477D9E","time":null}},"DB::SQLite::Connection":{"lib/DB/SQLite/Connection.rakumod":{"file":"032DBF250FD89CDEFA0FCB5A64857F4F96336737","time":null}},"DB::SQLite::Native":{"lib/DB/SQLite/Native.rakumod":{"file":"87AE851FDD0C59A12264F4E093BEB86F2639B27C","time":null}},"DB::SQLite::Result":{"lib/DB/SQLite/Result.rakumod":{"file":"9B24C886DDC4C9876CE29796855AA99DF64B28AF","time":null}},"DB::SQLite::Statement":{"lib/DB/SQLite/Statement.rakumod":{"file":"FF81D543D88B162772D20C1D6C6440917214D568","time":null}}},"support":{"bugtracker":"https://github.com/CurtTilmes/raku-dbsqlite/issues","email":"[email protected]","source":"https://github.com/CurtTilmes/raku-dbsqlite.git"},"tags":["DATABASE","SQLITE"],"ver":"0.7","version":"0.7"}

0.3 文件清单

模块原始路径快照文件(C:\rakudo\share\perl6\site\sources\)字节
DBlib/DB.rakumod03DA109C5B8532E87FCA813A145C35AABBCB9BC8873
DB::Connectionlib/DB/Connection.rakumod7D31F71E20FBC2186DACFFB1F4D67A911166094C1664
DB::Statementlib/DB/Statement.rakumodE4E2D72E6404EF6F25B5680D2EC27229A5F0C795205
DB::Resultlib/DB/Result.rakumod95A7DEA39A879FBDF062CFC4BD669BE298F3078A1306
DB::Mocklib/DB/Mock.rakumod48F58C267EEE065A1956AC0693126BB4D6CFF5A41375
DB::SQLite(参考驱动)lib/DB/SQLite.rakumod79F654BF6F54F73FCB982ADA494C8AB7BB477D9E2896
DB::SQLite::Connectionlib/DB/SQLite/Connection.rakumod032DBF250FD89CDEFA0FCB5A64857F4F963367372798
DB::SQLite::Statementlib/DB/SQLite/Statement.rakumodFF81D543D88B162772D20C1D6C6440917214D5682093
DB::SQLite::Resultlib/DB/SQLite/Result.rakumod9B24C886DDC4C9876CE29796855AA99DF64B28AF2446
DB::SQLite::Nativelib/DB/SQLite/Native.rakumod87AE851FDD0C59A12264F4E093BEB86F2639B27C7556

1. DB(role,驱动入口)

文件:lib/DB.rakumod,全文(原文):

use Concurrent::Stack;
use DB::Connection;

role DB
{
    has $.max-connections = 5;
    has $.connections = Concurrent::Stack.new;

    method connect(--> DB::Connection) {...}

    method db(--> DB::Connection)
    {
        while my $db = $!connections.pop
        {
            return $db if $db.ping;
            $db.DESTROY
        }
        $.connect
    }

    method query(|args)
    {
        $.db.query(:finish, |args)
    }

    method execute(|args)
    {
        $.db.execute(:finish, |args)
    }

    method cache(DB::Connection:D $db)
    {
        if $!connections.elems < $!max-connections
        {
            $!connections.push($db)
        }
        else
        {
            $db.DESTROY
        }
    }

    method finish(--> Nil)
    {
        .DESTROY while $_ = $!connections.pop
    }

    submethod DESTROY()
    {
        self.finish
    }
}

要点:

  • role DB 是参数化 role group(运行时 DB.HOW = Perl6::Metamodel::ParametricRoleGroupHOW)。

  • 驱动侧唯一必须实现的方法:method connect(--> DB::Connection) {...}(stubbed,{...} 为必须实现)。

  • 属性 $.max-connections = 5、$.connections = Concurrent::Stack.new 为公有 accessor;db() 从池中取连接,ping 失败则 DESTROY 后新建。

  • query/execute 在 DB 层强制注入 :finish(用完即还池)。

2. DB::Connection(role)

文件:lib/DB/Connection.rakumod,全文(原文):

use DB::Statement;

role DB::Connection
{
    has $.owner is required;
    has Bool $!transaction = False;
    has %!prepare-cache;

    method ping(--> Bool) { True }

    method free(--> Nil) {}

    method prepare-nocache(Str:D $query --> DB::Statement) {...}

    # execute defaults to the same as query(), but some modules
    # do different things with it.
    method execute(Str:D $command, Bool :$finish, |args)
    {
        $.prepare($command).execute(|args, :$finish);
    }

    method clear-cache(--> Nil)
    {
        .free for %!prepare-cache.values;
        %!prepare-cache = ()
    }

    method finish(--> Nil)
    {
        if $.ping
        {
            $.rollback if $!transaction;
            $!owner.cache(self);
        }
        else
        {
            self.DESTROY
        }
    }

    method prepare(Str:D $query, Bool :$nocache --> DB::Statement)
    {
        return $.prepare-nocache($query) if $nocache;
        return $_ with %!prepare-cache{$query};
        %!prepare-cache{$query} = $.prepare-nocache($query)
    }

    method query(Str:D $query, Bool :$finish, Bool :$nocache, |args)
    {
        $.prepare($query, :$nocache).execute(|args, :$finish);
    }

    method begin(--> DB::Connection)
    {
        self.execute('begin');
        $!transaction = True;
        self
    }

    method commit(--> DB::Connection)
    {
        self.execute('commit');
        $!transaction = False;
        self
    }

    method rollback(--> DB::Connection)
    {
        self.execute('rollback');
        $!transaction = False;
        self
    }

    submethod DESTROY()
    {
        self.clear-cache;
        self.free
    }
}

要点:

  • 必须实现的属性:has $.owner is required;(驱动类自身实例)。

  • 必须实现的方法:prepare-nocache(Str:D $query --> DB::Statement) {...};ping、free 有默认实现,驱动应按需 override。

  • 事务由父 role 用 SQL 文本 begin / commit / rollback 驱动(不是原生事务 API);私有布尔 $!transaction 控制 finish 时自动回滚。

  • 语句缓存 %!prepare-cache 由 role 负责,key 为 SQL 字符串;clear-cache/DESTROY 时逐个 free。

3. DB::Statement(role)

文件:lib/DB/Statement.rakumod,全文(原文,共 205 字节):

role DB::Statement
{
    has $.db is required;

    method free(--> Nil) {}

    method execute() {...}

    method finish(--> Nil)
    {
        $!db.finish
    }

    submethod DESTROY() { self.free }
}

要点:

  • 属性:has $.db is required;(所属 DB::Connection)。

  • 必须实现:method execute() {...}。

  • 注意签名陷阱:role 里 execute() 零参数,但所有实现(DB::Mock、DB::SQLite)都写成 method execute(Bool :$finish, *@args, *%args) —— Raku 允许实现加可选参数,实际调用点固定为 execute(|args, :$finish)。

4. DB::Result(role)+ 两个 Iterator 类

文件:lib/DB/Result.rakumod,全文(原文):

class DB::Result::ArrayIterator does Iterator
{
    has $.result is required;

    method pull-one()
    {
        $!result.row or do
        {
            $!result.finish;
            IterationEnd
        }
    }
}

class DB::Result::HashIterator does Iterator
{
    has $.result is required;

    method pull-one()
    {
        my $row = $!result.row or do
        {
            $!result.finish;
            return IterationEnd
        }
        %( $!result.keys.list Z=> @$row )
    }
}

role DB::Result
{
    has $.sth;
    has Bool $.finish;
    has $.keys-cache;

    method free() {}

    method finish(--> Nil)
    {
        self.free;
        $!sth.finish if $!finish && $!sth;
    }

    method row() { ... }

    method names() { ... }

    method keys()
    {
        $!keys-cache // ($!keys-cache = $.names)
    }

    method value()
    {
        LEAVE $.finish;
        $.row[0]
    }

    method array()
    {
        LEAVE $.finish;
        $.row
    }

    method hash()
    {
        LEAVE $.finish;
        %( @$.keys Z=> @$.row )
    }

    method arrays()
    {
        Seq.new: DB::Result::ArrayIterator.new(result => self)
    }

    method hashes()
    {
        Seq.new: DB::Result::HashIterator.new(result => self)
    }

    submethod DESTROY()
    {
        self.free
    }
}

要点:

  • 必须实现:row() 与 names()(均 stubbed)。

  • row() 约定:有行返回位置列表,无行返回空列表((),假值),迭代器据此终止。

  • finish/free 语义:free 驱动自实现;finish 在 :finish 为真时顺带 $!sth.finish(把连接还池)。

  • count() 不在 role 中,由驱动自定义(DB::SQLite::Result 提供 has $.count)。

5. DB::Mock(官方测试替身,完整可运行实现范例)

文件:lib/DB/Mock.rakumod,全文(原文):

use DB;
use DB::Connection;
use DB::Statement;
use DB::Result;

our $mock-connections = 0;
our $mock-statements = 0;
our $mock-results = 0;

class DB::Mock::Result does DB::Result
{
    has $.alive = True;
    has $.r = 0;
    has $.max;

    method free() { $mock-results--  if $!alive; $!alive = False }

    method names() { <a b c> }

    method row()
    {
        (++$!r > $!max) ?? () !! ($!r<>, "string {$!r}", Any)
    }
}

class DB::Mock::Statement does DB::Statement
{
    has $.alive = True;
    method free() { $mock-statements-- if $!alive; $!alive = False }

    method execute(Bool :$finish, *@args)
    {
        $mock-results++;
        DB::Mock::Result.new(:sth(self), :$finish, max => @args[0] // 5)
    }
}

class DB::Mock::Connection does DB::Connection
{
    has $.alive = True;
    has $.state is rw = True;

    method ping(--> Bool) { $!state }

    method free(--> Nil) { $mock-connections-- if $!alive; $!alive = False }

    method prepare-nocache(Str:D $query --> DB::Mock::Statement)
    {
        $mock-statements++;
        DB::Mock::Statement.new(:db(self))
    }

    method execute(Str:D $query, Bool :$finish)
    {
        LEAVE $.finish if $finish;
        return 1;
    }
}

class DB::Mock does DB
{
    method connect(--> DB::Mock::Connection)
    {
        $mock-connections++;
        DB::Mock::Connection.new(:owner(self))
    }
}

这是照抄级最小实现:一个驱动只需 4 个类(Driver does DB、Connection does DB::Connection、Statement does DB::Statement、Result does DB::Result)。

6. 参考驱动 DB::SQLite 全文

6.1 lib/DB/SQLite.rakumod(驱动类 + POD 用法)

use DB;
use DB::SQLite::Native;
use DB::SQLite::Connection;

class DB::SQLite does DB
{
    has $.filename;
    has $.busy-timeout;
    has %.flags;

    method BUILD(:$!filename = '', :$!busy-timeout = 10000, *%!flags) {}

    method connect(--> DB::SQLite::Connection)
    {
        my $conn = DB::SQLite::Native.open($!filename, |%!flags);
        $conn.busy-timeout($_) with $!busy-timeout;
        DB::SQLite::Connection.new(owner => self, :$conn)
    }
}

其 POD SYNOPSIS(官方用法示例,原文):

my $s = DB::SQLite.new; # You can pass in connection information if you want.

say $s.query('select 42').value;
# 42

$s.execute('insert into foo (x,y) values (1,2)');

for $s.query('select * from foo').arrays -> @row {
    say @row;
}

for $s.query('select * from foo').hashes -> %row {
    say %row;
}

6.2 lib/DB/SQLite/Connection.rakumod

use DB::Connection;
use DB::SQLite::Native;
use DB::SQLite::Statement;

class DB::SQLite::Connection does DB::Connection
{
    has DB::SQLite::Native $.conn is required;

    method free(--> Nil)
    {
        .close with $!conn;
        $!conn = Nil;
    }

    method prepare-nocache(Str:D $query --> DB::SQLite::Statement)
    {
        DB::SQLite::Statement.new(:db(self), stmt => $!conn.prepare($query))
    }

    method execute(Str:D $query, Bool :$finish --> Int)
    {
        LEAVE $.finish if $finish;
        $!conn.exec($query);
        $!conn.changes
    }
}

6.3 lib/DB/SQLite/Statement.rakumod

use DB::Statement;
use DB::SQLite::Native;
use DB::SQLite::Result;

class DB::SQLite::Statement does DB::Statement
{
    has DB::SQLite::Native::Statement $.stmt handles <bind sql clear>;
    has $.count = $!stmt.count;

    method free(--> Nil)
    {
        .finalize with $!stmt;
        $!stmt = Nil;
    }

    method execute(Bool :$finish, *@args, *%args)
    {
        .reset with $!stmt;

        if @args
        {
            my \num-params = @args.elems;
            loop (my $i = 0; $i < num-params; $i++)
            {
                $!stmt.bind($i+1, @args[$i]);
            }
        }

        if %args
        {
            for %args.kv -> $k, $v
            {
                $!stmt.bind($k, $v)
            }
        }

        if $!count                         # Has results?
        {
            return DB::SQLite::Result.new(:sth(self), :$finish)
        }
        else
        {
            LEAVE $.finish if $finish;
            $!stmt.step == SQLITE_DONE
                ?? $!stmt.db.changes
                !! $!stmt.db.check;
        }
    }
}

6.4 lib/DB/SQLite/Result.rakumod

use DB::SQLite::Native;
use DB::Result;

sub sqlite-int($stmt, $i)    { $stmt.int64($i)  }
sub sqlite-double($stmt, $i) { $stmt.double($i) }
sub sqlite-text($stmt, $i)   { $stmt.text($i)   }
sub sqlite-blob($stmt, $i)   { buf8.new($stmt.blob($i)[^$stmt.bytes($i)]) }
sub sqlite-null($stmt, $i)   { Any }

my %convert = Map.new: map( { +SQLITE_TYPE::{.key} => .value}, (
    SQLITE_INTEGER => &sqlite-int,
    SQLITE_FLOAT   => &sqlite-double,
    SQLITE_TEXT    => &sqlite-text,
    SQLITE_BLOB    => &sqlite-blob,
    SQLITE_NULL    => &sqlite-null
));

class DB::SQLite::Result does DB::Result
{
    has $.stmt = $!sth.stmt;
    has $.count = $!sth.count;

    method names() { do for ^$!count { $!stmt.name($_) } }

    method row()
    {
        given $!stmt.step()
        {
            when SQLITE_ROW
            {
                do for ^$!count -> $i
                {
                    %convert{$!stmt.type($i)}($!stmt, $i);
                }
            }
            when SQLITE_DONE
            {
                ()
            }
            default
            {
                $!stmt.db.check($_)
            }
        }
    }
}

6.5 lib/DB/SQLite/Native.rakumod(NativeCall 层范式,节选关键部分)

use NativeLibs:ver<0.0.7+>:auth<github:salortiz>;
use BitEnum;

sub LIBSQLITE {
    NativeLibs::Searcher.at-runtime(
         'sqlite3', 'sqlite3_version', 0).()
}

enum SQLITE (
    SQLITE_OK   => 0,
    SQLITE_ROW  => 100,
    SQLITE_DONE => 101
);

my constant NULL = Pointer;

class DB::SQLite::Native is repr('CPointer') {...}

sub sqlite3_errstr(int32 --> Str) is native(LIBSQLITE) {}

class DB::SQLite::Error is Exception
{
    has Int $.code;
    has Str $.message = sqlite3_errstr($!code);
}

class DB::SQLite::Native::Statement is repr('CPointer')
{
    method db(--> DB::SQLite::Native)
        is native(LIBSQLITE) is symbol('sqlite3_db_handle') {}

    method check($code = $.db.errcode) is hidden-from-backtrace
    {
        $.db.check($code) unless $code == SQLITE_OK
    }

    method step(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_step') {}

    method bind-text(int32, Str, int32, Pointer --> int32)
        is native(LIBSQLITE) is symbol('sqlite3_bind_text') {}

    multi method bind(Int $n, Str:D $v --> Nil)
    {
        $.bind-text($n, $v, -1, Pointer.new(-1)) == SQLITE_OK or $.check
    }

    multi method bind(Int $n, Any:U --> Nil)
    {
        $.bind-null($n) == SQLITE_OK or $.check
    }

    multi method bind(Str:D $name, |rest)
    {
        $.bind($.param-index($name ~~ /^<[:@$]>/ ?? $name !! ('$' ~ $name)),
               |rest)
    }

    method count(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_column_count') {}

    method name(int32 $iCol --> Str)
        is native(LIBSQLITE) is symbol('sqlite3_column_name') {}

    method type(int32 $iCol --> int32)
        is native(LIBSQLITE) is symbol('sqlite3_column_type') {}

    method text(int32 $iCol --> Str)
        is native(LIBSQLITE) is symbol('sqlite3_column_text') {}

    method finalize(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_finalize') {}

    method reset(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_reset') {}
}

class DB::SQLite::Native
{
    method errcode(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_errcode') {}

    method errmsg(--> Str)
        is native(LIBSQLITE) is symbol('sqlite3_errmsg') {}

    method check(int32 $code = $.errcode) is hidden-from-backtrace
    {
        die DB::SQLite::Error.new(:$code) unless $code == SQLITE_OK
    }

    method busy-timeout(int32 --> int32)
        is native(LIBSQLITE) is symbol('sqlite3_busy_timeout') {}

    sub sqlite3_open_v2(Str $filename,
                        DB::SQLite::Native $handle is rw,
                        int32 $flags,
                        Str $zVfs --> int32)
        is native(LIBSQLITE) {}

    method close( --> int32)
        is native(LIBSQLITE) is symbol('sqlite3_close_v2') {}

    method open(Str:D $filename, *%flags --> DB::SQLite::Native)
    {
        my DB::SQLite::Native $handle .= new;
        UNDO .close with $handle;

        my $flags = BitEnum[SQLITE_FILE_OPEN_FLAGS, :lc,
                            prefix => 'SQLITE_OPEN_'].new(%flags.keys);

        $flags.set(<readwrite create>) unless $flags.isset(<readonly>) ||
                                              $flags.isset(<readwrite>);

        $.check(sqlite3_open_v2($filename, $handle, +$flags, Str));
        return $handle;
    }

    method prepare(Str:D $sql) is hidden-from-backtrace
    {
        my DB::SQLite::Native::Statement $stmt .= new;
        UNDO .finalize with $stmt;
        $.check(sqlite3_prepare_v2(self, $sql, -1, $stmt, Pointer));
        return $stmt;
    }

    method changes(--> int32)
        is native(LIBSQLITE) is symbol('sqlite3_changes') {}

    method exec(Str:D $sql)
    {
        $.check($.sqlite3_exec($sql, NULL, NULL, NULL));
    }
}

NativeCall 层关键范式:

  1. class X::Native is repr('CPointer') {...} —— 句柄类型前置声明。

  2. 原生函数用 method ... is native(LIBSQLITE) is symbol('...') {} 直接绑到类方法上(Raku 把 self 作为首参传出)。

  3. 错误用 die X::Error.new(:$code) 抛自定义 is Exception 子类,无 out_error 字符串。

  4. 资源安全:UNDO .close with $handle; / UNDO .finalize with $stmt; —— 构造中途失败自动回滚释放。

7. 契约总表(实现 DB::DuckDB 的照抄清单)

7.1 命名与结构(沿用 DB::SRC 的既有模式)

层类/role必须 does
驱动入口class DB::DuckDB does DBDB
连接class DB::DuckDB::Connection does DB::ConnectionDB::Connection
语句class DB::DuckDB::Statement does DB::StatementDB::Statement
结果class DB::DuckDB::Result does DB::ResultDB::Result
原生句柄class DB::DuckDB::Native(+ is repr('CPointer') 的子类)—

7.2 必须实现 vs 可继承

类必须实现(stubbed / required)可继承默认实现
DBconnect(--> DB::Connection)db、query、execute、cache、finish、DESTROY
DB::Connectionprepare-nocache(Str:D $query --> DB::Statement);属性 $.owner is requiredping(True)、free(空)、prepare、query、execute、clear-cache、finish、begin/commit/rollback、DESTROY
DB::Statementexecute(...);属性 $.db is requiredfree(空)、finish($!db.finish)、DESTROY
DB::Resultrow()、names()free、finish、keys、value、array、hash、arrays、hashes、DESTROY

7.3 调用链(运行时实测确认)

$driver.query($sql, |args)         → DB.query  → $conn.query(:finish) → prepare($sql) → prepare-nocache
                                   → $sth.execute(|args, :finish)
$driver.execute($sql, |args)       → DB.execute → $conn.execute(:finish) → prepare-nocache + execute
$conn.query($sql, :finish, :nocache, |args) → prepare(...).execute(|args, :$finish)
DB::Statement.finish               → $!db.finish → 若 ping 则 rollback(if tx) + owner.cache(self)

7.4 构造与 DSN

  • 无统一 connect($dsn) 静态工厂;驱动类的 new/BUILD 即连接配置入口:

    • DB::SQLite: BUILD(:$!filename = '', :$!busy-timeout = 10000, *%!flags) {},即 DB::SQLite.new(filename => 'x.db', :readonly)。

    • 对应 DuckDB:DB::DuckDB.new(:$path = ':memory:', *%config),connect 内 duckdb_open + duckdb_connect。

  • connect 无参,返回 DB::Connection;连接不共享 dll 级全局状态,池由 DB role 管理(默认 5 条)。

7.5 错误处理约定

  • 无统一 X::DB::Error 基类。各驱动自定义 is Exception 子类并 die:

    class DB::SQLite::Error is Exception
    {
        has Int $.code;
        has Str $.message = sqlite3_errstr($!code);
    }
    ...
    method check(int32 $code = $.errcode) is hidden-from-backtrace
    {
        die DB::SQLite::Error.new(:$code) unless $code == SQLITE_OK
    }
  • 惯例:method check(...) is hidden-from-backtrace,失败即 die;DuckDB 侧对应 duckdb_state != DuckDBSuccess 时读 char** out_error 并 duckdb_free,构造 class DB::DuckDB::Error is Exception { has Int $.code; has Str $.message; }。

7.6 事务

  • 事务不调用原生 API,父 role 直接执行 SQL 文本:

    方法实现返回值
    begin(--> DB::Connection)self.execute('begin'); $!transaction = Trueself
    commit(--> DB::Connection)self.execute('commit'); $!transaction = Falseself
    rollback(--> DB::Connection)self.execute('rollback'); $!transaction = Falseself
  • 若驱动要支持嵌套/保存点,需自行 override 这三个方法。

  • finish 时若 $!transaction 为真,自动 rollback。

7.7 结果消费 API(DB::Result)

方法语义
value首行首列标量(LEAVE $.finish)
array首行(LEAVE $.finish)
hash首行 hash(LEAVE $.finish)
arraysSeq,全部行(迭代耗完自动 finish)
hashesSeq,全部行 hash
keys / names列名;keys 带 $!keys-cache
row驱动实现,无行返回 ()
finish / freefinish 在 :finish 时还连接;free 释放结果资源

8. 官方示例与测试用例

8.1 官方 POD SYNOPSIS(DB::SQLite,原文,见 §6.1)

my $s = DB::SQLite.new;

say $s.query('select 42').value;      # 42

$s.execute('insert into foo (x,y) values (1,2)');

for $s.query('select * from foo').arrays -> @row { say @row }

for $s.query('select * from foo').hashes -> %row { say %row }

8.2 t/ 目录可用性(重要)

  • 本机未安装 t/ 测试文件:DB 与 DB::SQLite 的 META6 中 "files":{},zef 安装只落地 lib/ 与 precomp 源码快照;全盘搜索 *raku-db* 目录为空,site/sources 中亦无 use DB::SQLite 的测试快照(仅 5 个 lib 模块命中)。

  • 因此以官方 DB::Mock(发行版自带测试替身)+ 运行时实跑代替 t/ 用例验证契约。下面 §8.3 是用例与实测输出。

8.3 实测用例(等价于官方测试写法)

use DB;
use DB::Mock;

my $db = DB::Mock.new;
my $result = $db.query('select 1', :finish);

say $result.^name;        # DB::Mock::Result
say $result.keys;         # (a b c)
say $result.value;        # 1
say $result.array.raku;   # $(2, "string 2", Any)
say $result.hash.raku;    # {:a(3), :b("string 3"), :c(Any)}
say $result.arrays.raku;  # ((4, "string 4", Any), (5, "string 5", Any)).Seq
say $db.execute('insert into foo values (1)');   # 1

my $conn = $db.db;
$conn.begin; $conn.commit; $conn.rollback;
say $conn.ping;           # True
$conn.finish;

实测输出(raku mock-usage.raku):

connect class: DB::Mock::Connection
db() class:    DB::Mock::Connection
result class:   DB::Mock::Result
keys:           (a b c)
value:          1
array:          $(2, "string 2", Any)
hash:           {:a(3), :b("string 3"), :c(Any)}
arrays:         ((4, "string 4", Any), (5, "string 5", Any)).Seq
hashes:         ().Seq
execute:        1
begin/commit/rollback: DB::Mock::Connection / DB::Mock::Connection / DB::Mock::Connection
ping: True
db.elems cached: 5

8.4 运行时内省结果(确认无隐藏契约)

raku introspect-db.raku 关键输出:

DB.^name = DB
DB HOW   = Perl6::Metamodel::ParametricRoleGroupHOW
DB attributes: $!max-connections : Mu [accessor $.max-connections] | $!connections : Mu [accessor $.connections]
DB methods: connect, db, query, execute, cache, finish, DESTROY

DB::Connection attributes: $!owner : Mu [accessor $.owner] | $!transaction : Bool | %!prepare-cache : Associative
DB::Connection methods: ping, free, prepare-nocache, execute, clear-cache, finish, prepare, query, begin, commit, rollback, DESTROY

DB::Statement attributes: $!db : Mu [accessor $.db]
DB::Statement methods: free, execute, finish, DESTROY

DB::Result attributes: $!sth : Mu [accessor $.sth] | $!finish : Bool [accessor $.finish] | $!keys-cache : Mu [accessor $.keys-cache]
DB::Result methods: free, finish, row, names, keys, value, array, hash, arrays, hashes, DESTROY

DB::Mock does: DB

注意:DB.^ver / ^api / ^auth 在无 META 关联加载时返回 n/a;发行版版本以 §0.2 的 META6 为准 —— DB ver 0.5 / api 1 / auth github:CurtTilmes。

9. 给 DB::DuckDB 的落地建议(严格对齐契约)

  1. DB::DuckDB does DB:has $.path = ':memory:'、has %.config;BUILD 只存配置,不在 BUILD 里建库(connect 才开)。

  2. connect 内:duckdb_open(path, $db) → duckdb_connect($db, $conn);把 $db(database handle)与 $conn(connection handle)都挂在 DB::DuckDB::Connection 上,free 中先 duckdb_disconnect 再 duckdb_close。

  3. prepare-nocache → duckdb_prepare,返回 DB::DuckDB::Statement,其 has $.count = duckdb_column_count(用于判断"是否有结果集",对齐 DB::SQLite 的 if $!count 分支)。

  4. execute 分两支:有结果集 → 返回 DB::DuckDB::Result;无结果集 → duckdb_execute_prepared 后返回 duckdb_rows_changed(Int)。

  5. DB::DuckDB::Result.row:duckdb_step → DuckDBRowCount 语义;duckdb_row_count/duckdb_column_count 取维度,duckdb_value_* 取列值,DuckDBType 做类型映射;names 用 duckdb_column_name。

  6. 事务:无需实现 begin/commit/rollback(父 role 已用 SQL 文本实现),但需保证 execute('begin') 走无结果集分支且返回真值。

  7. 错误:所有 duckdb_* 返回 duckdb_state 的调用统一走 self.check($state);out_error 用 char** + duckdb_free 释放,严禁内存泄漏(DuckDB 无 duckdb_free_string)。

附:原始快照路径(供人工复核)

C:\rakudo\share\perl6\site\sources\03DA109C5B8532E87FCA813A145C35AABBCB9BC8   DB.rakumod
C:\rakudo\share\perl6\site\sources\7D31F71E20FBC2186DACFFB1F4D67A911166094C   DB/Connection.rakumod
C:\rakudo\share\perl6\site\sources\E4E2D72E6404EF6F25B5680D2EC27229A5F0C795   DB/Statement.rakumod
C:\rakudo\share\perl6\site\sources\95A7DEA39A879FBDF062CFC4BD669BE298F3078A   DB/Result.rakumod
C:\rakudo\share\perl6\site\sources\48F58C267EEE065A1956AC0693126BB4D6CFF5A4   DB/Mock.rakumod
C:\rakudo\share\perl6\site\sources\79F654BF6F54F73FCB982ADA494C8AB7BB477D9E   DB/SQLite.rakumod
C:\rakudo\share\perl6\site\sources\032DBF250FD89CDEFA0FCB5A64857F4F96336737   DB/SQLite/Connection.rakumod
C:\rakudo\share\perl6\site\sources\FF81D543D88B162772D20C1D6C6440917214D568   DB/SQLite/Statement.rakumod
C:\rakudo\share\perl6\site\sources\9B24C886DDC4C9876CE29796855AA99DF64B28AF   DB/SQLite/Result.rakumod
C:\rakudo\share\perl6\site\sources\87AE851FDD0C59A12264F4E093BEB86F2639B27C   DB/SQLite/Native.rakumod

(内容由AI生成,仅供参考) (内容由AI生成,仅供参考)

DB::DuckDB v0.9.2

DuckDB driver for the Raku DB interface - NativeCall binding over libduckdb

Authors

  • skyter10086

License

Artistic-2.0

Dependencies

DBAlien::Libduckdb:ver<1.1.0>

Test Dependencies

Provides

  • DB::DuckDB
  • DB::DuckDB::Appender
  • DB::DuckDB::Arrow
  • DB::DuckDB::ArrowShim
  • DB::DuckDB::ArrowShim::Native
  • DB::DuckDB::Bridge
  • DB::DuckDB::Chunk
  • DB::DuckDB::ColumnView
  • DB::DuckDB::Connection
  • DB::DuckDB::Decoder
  • DB::DuckDB::Error
  • DB::DuckDB::Native
  • DB::DuckDB::Parquet
  • DB::DuckDB::Result
  • DB::DuckDB::ShimCache
  • DB::DuckDB::Statement
  • DB::DuckDB::TextColumn
  • DB::DuckDB::VarcharShim
  • DB::DuckDB::VarcharShim::Native

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.