Rand Stats

Duckie

zef:bduggan

Actions Status Actions Status

NAME

Duckie - A wrapper and native bindings for DuckDB

SYNOPSIS

use Duckie;
Duckie.new.query('select name from META6.json').rows[0]
# {name => Duckie}

my $db = Duckie.new;
say $db.query("select 1 as the_loneliest_number").column-data(0);
# [1]

with $db.query("select 1 as the_loneliest_number") -> $result {
  say $result.column-data('the_loneliest_number'); # [1]
} else {
  # Errors are soft failures
  say "Failed to run query: $_";
}

# DuckDB can query or import data from CSV or JSON files, HTTP URLs,
# PostgreSQL, MySQL, SQLite databases and more.
my @cols = $db.query('select * from data.csv').columns;
my @rows = $db.query('select * from data.json').rows;

$db.query: q[ attach 'postgres://secret:pw@localhost/dbname' as pg (type postgres)]
$res = $db.query: "select * from pg.my_table"

$db.query("install httpfs");
$db.query("load httpfs");
$res = $db.query: "select * from 'http://example.com/data.csv'";

# Joins between different types are also possible.
$res = $db.query: q:to/SQL/
select *
from pg.my_table one
inner join 'http://example.com/data.csv' csv_data on one.id = csv_data.id
inner join 'data.json' json_data on one.id = json_data.id
SQL

DESCRIPTION

This module provides Raku bindings for DuckDB. DuckDB is a "fast in-process analytical database". It provides an SQL interface for a variety of data sources. Result sets are column-oriented, with a rich set of types that are either inferred, preserved, or explicitly defined. Duckie also provides a row-oriented API.

This module provides two sets of classes.

INSTALLATION

Since Duckie depends on the C API, follow the instructions here for installation of libduckdb before installing this module. For instance, the "C/C++" section of this page has a link to a .zip file, which will need to be extracted and placed in a location that is included in a standard locations, or one included in DYLD_LIBRARY_PATH for os/x, or a similar LD_LIBRARY_PATH location for linux.

EXPORTS

If an argument to use Duckie is provided, a new Duckie object is created and returned. Also "-debug" will enable debug output. e.g.

use Duckie;                 # no exports
use Duckie '$db';           # creates and exports "$db"
use Duckie '$db', '-debug'; # creates and exports "$db" with debug output

By default, duckdb-version is exported, so duckdb-version() is always available.

use Duckie 'db';
db.query("select 1 as the_loneliest_number").column-data(0);

METHODS

method new

method new(
  :$file = ':memory:'
) returns Duckie

Create a new Duckie object. The optional :file parameter specifies the path to a file to use as a database. If not specified, an in-memory database is used. The database is opened and connected to when the object is created.

method query

multi method query(Str $sql --> Duckie::Result)
multi method query(Str $sql, *@params --> Duckie::Result)
multi method query(Str $sql, *%named --> Duckie::Result)

Run a query and return a Duckie::Result. If the query fails, a soft failure is thrown.

Use ? as positional placeholders and $name for named placeholders.

$db.query("select 1 as n").rows;
$db.query('select ? as n', 42);
$db.query('select $x + $y as sum', x => 1, y => 2);

method prepare

method prepare(Str $sql --> Duckie::PreparedStatement)

Prepare a statement. Returns a Duckie::PreparedStatement. Use ? for positional parameters or $name for named parameters.

The returned Duckie::PreparedStatement has an execute method:

multi method execute(--> Duckie::Result)
multi method execute(*@params --> Duckie::Result)
multi method execute(*%named --> Duckie::Result)

Execute the prepared statement, optionally binding positional (?) or named ($name) parameters.

my $stmt = $db.prepare('select ? as n, ? as s');
$stmt.execute(42, 'hello').rows;
# [{n => 42, s => hello}]

my $stmt2 = $db.prepare('select $x + $y as sum');
$stmt2.execute(x => 1, y => 2).rows;

method register-table-function

method register-table-function(
  Str     $name,
  :@columns,      # required: list of Pairs  name => 'TYPE'
  :@params = [],  # optional: list of SQL parameter type names
  :&function!,    # required: callable (*@params --> Iterable of arrays)
) returns Duckie

Register a user-defined table function that calls a Raku subroutine.

@columns is a list of Pair objects mapping column name to DuckDB type string (e.g. 'n' =E<gt> 'INTEGER'). Supported types: VARCHAR, INTEGER, BIGINT, DOUBLE, FLOAT, BOOLEAN.

@params declares the SQL parameter types accepted at the call site.

&function is called once per query execution with the SQL arguments (as strings) and must return an iterable of arrays, one per output row.

Thread safety note: registering any table or scalar function causes the connection to switch to single-threaded mode (SET threads=1). This is necessary because MoarVM NativeCall callbacks are bound to the OS thread that created them; if DuckDB's internal worker threads invoke a callback, MoarVM panics with "native callback ran on thread unknown to MoarVM". Single-threaded mode ensures callbacks are always invoked on the same thread that issued the query.

$db.register-table-function('squares',
  columns  => ['n' => 'INTEGER', 'sq' => 'INTEGER'],
  params   => ['INTEGER'],
  function => sub ($n) { (1..$n.Int).map(-> $i { [$i, $i*$i] }) },
);
$db.query('SELECT * FROM squares(4)').rows;
# [{n => 1, sq => 1}, {n => 2, sq => 4}, ...]

method register-scalar-function

method register-scalar-function(
  Str     $name,
  :@params!,      # required: list of SQL parameter type names
  :$returns!,     # required: return type name
  :&function!,    # required: callable (*@row-values --> Any)
) returns Duckie

Register a user-defined scalar function that calls a Raku subroutine once per input row.

&function receives one argument per declared parameter and must return a single value of the declared return type.

Thread safety note: see register-table-function — registering any UDF switches the connection to single-threaded mode to prevent MoarVM panics from DuckDB worker threads invoking NativeCall callbacks.

$db.register-scalar-function('raku-upper',
  params   => ['VARCHAR'],
  returns  => 'VARCHAR',
  function => sub ($s) { $s.uc },
);
$db.query("SELECT raku_upper('hello world')").column-data(0);
# ['HELLO WORLD']

method register-raku-sub

method register-raku-sub(&function) returns Duckie

Register a named Raku subroutine as a DuckDB scalar UDF, inferring the SQL parameter and return types from the subroutine's type annotations.

The subroutine must:

Supported Raku-to-SQL type mappings:

Raku typeDuckDB type
IntINTEGER
StrVARCHAR
NumDOUBLE
BoolBOOLEAN

Unannotated or unrecognised types default to VARCHAR.

sub double-it(Int $n --> Int) { $n * 2 }
$db.register-raku-sub(&double-it);
# Raku hyphens are converted to underscores for SQL:
$db.query('SELECT double_it(21)').column-data(0);
# [42]

method DESTROY

Close the database connection and free resources.

SEE ALSO

ENVIRONMENT

Set DUCKIE_DEBUG to a true value to enable logging to STDERR.

AUTHOR

Brian Duggan