No obvious way to return this zig-sqlite function

Hi all

Im having some Serious trouble trying to wrap a function with another function, because of the return type.
In particular a function from

(And i guess its possible that its a problem how the library is written, but i belive its more likely that its a problem with my lack of zig knowledge)

Following the githubs README example works fine, but wanting to create some logic separation, i try to create some functions that abstract the db calls, but for the sql query calls i hit a wall with the return type.

The minimun working example

const std = @import("std");
const sqlite = @import("sqlite");

const DB_PATH = "db_file.db";

const FETCH =
    \\SELECT id, name FROM Foo
;

const Row = struct {
    id: u32,
    name: sqlite.Text,
};

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;

    // Open db connection
    var db_conn: sqlite.Database = try connect(DB_PATH, gpa);
    defer db_conn.close();

    // Cant get to wrap the statement preparation into a function
    const db_sta = try listing(db_conn);
    // By commenting the prev line and uncommenting the next one it works, ofc
    //const db_stament = try db_conn.prepare(struct {}, Row, FETCH);
    defer db_stament.finalize();

    while (try db_stament.step()) |row| {
        std.debug.print("{d} {s}", .{ row.id, row.name });
    }
}

pub fn connect(db_path: []const u8, gpa: std.mem.Allocator) !sqlite.Database {
    // Convert db_path string to C compatible string
    const db_path_s = try gpa.dupeSentinel(u8, db_path, 0);
    defer gpa.free(db_path_s);

    std.debug.print("DEBUG: opening database \"{s}\"\n", .{db_path_s});
    return try sqlite.Database.open(.{ .path = db_path_s });
}

// None of this (and Many More attempts) work
// pub fn listing(conn: sqlite.Database) !sqlite.Statement(struct {}, Row) {
pub fn listing(conn: sqlite.Database) !@TypeOf(sqlite.Statement) {
    return try conn.prepare(struct {}, Row, FETCH);
}

Minimum sqlite db creation

CREATE TABLE IF NOT EXISTS Foo (
    id INTEGER PRIMARY KEY NOT NULL,
    name TEXT UNIQUE NOT NULL,
    data TEXT
);

INSERT INTO Row (name, data) VALUES
    ('bar', 'some text')
;

For the attemps in the minimal example up there i get:

src/main.zig:41:29: error: expected type 'type', found 'sqlite.Statement(main.main__struct_32813,Row)'

Ive also tried

  • type: it makes it the function comptime wich fails with the database connection parameter
  • anytype in all possible places
  • sqlite.Statement complains that its a function
  • many more, and nothing works

Ive looked at the source zig-sqlite/src/sqlite.zig at main · nDimensional/zig-sqlite · GitHub but i dont have the Params in the function definition so cant use it for the return type

And Statement zig-sqlite/src/sqlite.zig at main · nDimensional/zig-sqlite · GitHub just has return type as type and in the code it returns a struct defined in place, so i cant reference to the struct it uses as return…

Besides im also expecting even more problems as soon as i want to make a more general function that calls different queries based on some logic

Another slightly related question: using the other zig-sqlite library?

I see it says its in hiatus, and tried it and it failed to build for release. missing symbols.
But it had many more contributors, meanwhile this one just has the single maintainer (also i wanna extend my thanks to them for keeping the module)

This is seriously testing my sanity :sob:

One thing that might help here is that empty structs in zig are not equivalent:

const A = struct {};
const B = struct {};
// A != B

So when you have the params be a struct {}, you are creating 2 different types.
You may want to try creating a named struct and passing that in both places.

2 Likes

From what I see in the library source, you would want to return the exact type that conn.prepare returns. Maybe something like this works:

const FetchParams = struct {};

pub fn listing(conn: sqlite.Database) !sqlite.Statement(FetchParams, Row) {
    return try conn.prepare(FetchParams, Row, FETCH);
}
1 Like

Well, actually it does and i love you.

pub fn listing(conn: sqlite.Database, verbose: bool) !sqlite.Statement(EmptyStruct, Row)

But very quickly we hit the next snag, trying to make the function a bit more general

const FETCH_VERBOSE =
    \\SELECT id, name, data FROM Foo
;

const RowVerbose = struct {
    id: u32,
    name: sqlite.Text,
    data: sqlite.Text,
};

pub fn listing(
    conn: sqlite.Database,
    verbose: bool
) !sqlite.Statement(EmptyStruct, WhatsTheTypeHere) {
    if (verbose) return try conn.prepare(EmptyStruct, RowVerbose, FETCH_VERBOSE);
    return try conn.prepare(EmptyStruct, Row, FETCH);
}

Im thinking about unions (which ive never used in zig so im reading up on them) and cant see how to actually use them for this

Furthermore what about not always wanting an empty struct fields

const FILTERED =
    \\SELECT id, name, data FROM Foo WHERE id = :id
;
pub fn listing(
    conn: sqlite.Database,
    verbose: bool,
    filter: ?anytype,
) !sqlite.Statement(filter, ...) {
    // pseudocode
    if (filter not  null) {
        return try conn.prepare(filter, Row, FILTERED);
    }
    return try conn.prepare(EmptyStruct, Row, FETCH);
}

(In this case we could pass filter to the return type IF we make the call pass the EmptyStruct instead of null and this would be acceptable if there is no way to be able to pass null and have different return types on that)

You may be trying to optimize before you actually need it. Why not just have 2 separate functions for these? Do you really need to make it generic?

Unfortunately I don’t know the sqlite api that you are using well in order to give a good suggestion here. Just from looking at it, I think you can prepare the statements ahead of time and just pick the right statement.

1 Like

I guess you would want to have the filter inside the params struct?

I’m not sure how the library handles the Param type there :confused: . Maybe only structs are allowed.

a fair point in general, but maybe in this particular case its more of a problem of “coming with a different mentality” not the “zig” way, since i have the actual application implemented in python hahaha

but then again, from how the library is implemented, you set the Statment and then call next() to iterate over the results. Even if i were to create different functions for the fetching, since the db part of the code is gonna be separated from the display part of the code, i would still need to return the statements up the call stack for displaying later.

I guess an option would be to loop the statment and load the fetched values into an array and return that array for later looping (again) through it for displaying.

meanwhile in python im calling with the statement fetchall()/fetchone() directly which i suppose is precisely doing the thing i just mentioned, looping to get the data into a list of rows. And then im working on that.

basically yes, the libray uses Params for any replace in the statement
but i wrote filter thinking exclusevely for WHERE, thats why i called it filter, but yeah, basically filter=params

My suggestion would be to first write a substantial amount of code without abstracting it into tiny functions, then later on figure out what is actually worth putting into functions.

The verbose parameter could become a build option, if you don’t need to toggle it at runtime.

?anytype doesn’t work because anytype isn’t a type it is a placeholder for any type.
Maybe filter could be ?[]const u8 instead, or if it could be of multiple types, you could create a tagged union for it.

The whole putting tiny functions around the sqlite-api functions seems like too much abstraction, like creating a second layer of apis while you still have barely used the first layer.

Start with no abstraction to gain clarity about what is actually going on (instead of making code hard to understand because it is wrapped in layers of abstraction) and then slowly add it back in where it serves a real useful purpose.

5 Likes