Rand Stats

Badger

zef:jnthn

Badger

Badger, not an ORM (a snake).

What's Badger?

Badger is a SQL library that allows you to invoke SQL snippets as function as if it was Raku code. This way you can keep writing your SQL queries by hand for performance and tweakability, and your tools still recognize the .sql files to help you work with them.

What does a Badger SQL file look like?

A badger-compatible SQL file is just a normal SQL file, with signature headers. These signatures are intended to look like Raku signatures.

The most basic example:

-- sub my-query()
SELECT;

How do I feed Badger my SQL?

You have to pass the .sql file(s) to the use Badger statement:

use Badger <sql/my-query.sql>; # The file in the previous code block

This will generate this function Raku-side:

sub my-query(Database $db --> Int) { ... }

Which you can call just like any other Raku subs, by passing any object that has an interface similar to DB::Pg (for now at least) as the connection.

For parameters and return values, see below.

Parameters

A Badger SQL sub can have arguments that you can use in the SQL body. Interpolation works for sigilled variables:

-- sub query-with-params($a, $b)
SELECT $a + $b, @c

This will generate a prepared query with $a and $b replaced $1, $2 (or with ?s depending on the RDBMS).

Parameter Sigils

The Raku allowed sigils are $ and @.

Parameter typing

You can put type annotations on the parameters:

-- sub query-with-params(Int $x, Int @xs)
SELECT $x = ANY(@xs)

If a parameter is typed, Badger will try to help you by inserting coercions in the generated SQL. This is what the executed SQL looks like:

SELECT ($1::int) = ANY(($2::int[]))

Named Parameters

Parameters can be named, just like in Raku:

-- sub query-nameds(Int :$a, :$b)
SELECT $a + $b

Just like in Raku, you can't have a positional parameter after a named one.

Mandatory Named Parameters

Also just like in Raku, named parameters can be marked mandatory:

-- sub query-nameds(:$mandatory!)
SELECT $a * 2

Return Sigils

+ (default)

The default one -- in you don't specify a return sigil, you get this. Returns the number of affected rows (as an Int).

-- sub count-unnests(--> +)
-- ... or ...
-- sub count-unnests()
UPDATE products
   SET price = 999
   WHERE price IS NULL

$

Returns a single value. Nil is returned otherwise:

-- sub get-username(Str $token --> $)
SELECT username
FROM users
WHERE token = $token

Typed $

Calls .new on the given type with all the data returned from the SQL query:

-- sub get-user(Int $id --> MyApp::Models::User)
SELECT 1 AS id, 'steve' AS username

You'll usually need to import type module that provides the type, by placing a use at the top of the SQL file:

-- use MyApp::Models;
class MyApp::Models::User {
  has Int $.id;
  has Str $.username;
}
...
my MyApp::Models::User $user = get-user(db, 1);
# Result: `MyApp::Models::User.new(id => 1, :username<steve>);`

%

Returns a hash.

-- sub get-hash(--> %)
SELECT 'comment' as type, 'Hello world!' as txt
my %h = get-hash($db);
# Result: `%(type => "comment", txt => "Hello world!")`

If the database doesn't return anything, Badger gives you an empty hash back.

@

Returns an array of hashes.

-- sub get-hashes(--> @)
SELECT 'comment' as type, txt
FROM unnest(array['Hello', 'world!']) txt
my @hashes = get-hashes($db);
# Result: `%(type => "comment", txt => "Hello"), %(type => "comment", txt => "world!")`

Typed @

Calls .new on the given type on each row of the data returned from the SQL query:

-- sub get-data(--> Datum @)
SELECT row_number() OVER () as id
     , unnest(ARRAY['a','b']) as value
class Datum {
  has Int $.id;
  has Str $.value;                                                                                                                                                    
}
...
my Datum @data = get-data($db);
# Result: `Datum.new(id => 1, :value<a>), Datum.new(id => 2, :value<b>)`