SQLite - chung-leong/zigar GitHub Wiki
This example shows how you can use zig-sqlite to retrieve and store data in a SQLite database.
We'll start by creating an Electron-Vite boilerplate app:
npm create @quick-start/electron@latest
Need to install the following packages:
@quick-start/[email protected]
Ok to proceed? (y)
✔ Project name: … sqlite
✔ Select a framework: › react
✔ Add TypeScript? … [No] / Yes
✔ Add Electron updater plugin? … [No] / Yes
✔ Enable Electron download mirror proxy? … [No] / Yes
Then we add the node-zigar module:
cd sqlite
npm install
npm install node-zigar
After that we install zig-sqlite, a Zig package that provides a wrapper around the SQLite C API. As there is currently no central repository for Zig packages, you'll need to obtain zig-sqlite from the source. First, go to the project's Github page. Click on the SHA of the last commit:
Then click the "Browse files" button:
Then click the "Code" button, right-click on "Download ZIP", and select "Copy link address":
Go back to the terminal, create the sub-directory zig
, and cd to it:
mkdir zig
cd zig
Create an empty build.zig
:
touch build.zig
Enter "zig fetch --save " then paste the copied URL and press ENTER:
zig fetch --save https://github.com/vrischmann/zig-sqlite/archive/91e5fedd15c5ea3cb42ccceefb3d0f4bb9bad68f.zip
That'll fetch the package and create a build.zig.zon
listing it as a dependency. We'll then fill
in build.zig
:
const std = @import("std");
const cfg = @import("./build-cfg.zig");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addSharedLibrary(.{
.name = cfg.module_name,
.root_source_file = .{ .cwd_relative = cfg.stub_path },
.target = target,
.optimize = optimize,
});
const sqlite = b.dependency("sqlite", .{
.target = target,
.optimize = optimize,
});
const imports = .{
.{ .name = "sqlite", .module = sqlite.module("sqlite") },
};
const mod = b.createModule(.{
.root_source_file = .{ .cwd_relative = cfg.module_path },
.imports = &imports,
});
mod.addIncludePath(.{ .cwd_relative = cfg.module_dir });
lib.root_module.addImport("module", mod);
if (cfg.use_libc) {
lib.linkLibC();
}
const wf = switch (@hasDecl(std.Build, "addUpdateSourceFiles")) {
true => b.addUpdateSourceFiles(),
false => b.addWriteFiles(),
};
wf.addCopyFileToSource(lib.getEmittedBin(), cfg.output_path);
wf.step.dependOn(&lib.step);
b.getInstallStep().dependOn(&wf.step);
}
For this example we're going to the sample database provided by sqlitetutorial.net:
Download chinook.zip
and unzip the file into sqlite
.
In your text editor, create sqlite.zig
in the zig
sub-directory. Add the following code:
const std = @import("std");
const sqlite = @import("sqlite");
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const SqliteOpaquePtr = *align(@alignOf(sqlite.Db)) opaque {};
pub fn openDb(path: [:0]const u8) !SqliteOpaquePtr {
const allocator = gpa.allocator();
const db_ptr = try allocator.create(sqlite.Db);
errdefer allocator.destroy(db_ptr);
db_ptr.* = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{},
.threading_mode = .MultiThread,
});
return @ptrCast(db_ptr);
}
pub fn closeDb(db_op: SqliteOpaquePtr) void {
const db_ptr: *sqlite.Db = @ptrCast(db_op);
db_ptr.deinit();
const allocator = gpa.allocator();
allocator.destroy(db_ptr);
}
openDb()
allocates memory from the heap to hold the Db
struct from zig-sqlite.
It then calls Db.init()
to open the database file specified by path
. We return the pointer to Db
as an opaque pointer
so we aren't leaking zig-sqlite's implementation details to the JavaScript side.
closeDb()
takes a opaque pointer and casts it back into a pointer to Db
. It calls
Db.deinit()
, then frees the struct.
After creating the zig file, open index.js
in src/main
. Scroll to the bottom and add the
following lines:
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
require ('node-zigar/cjs')
const { openDb, closeDb } = require('../../zig/sqlite.zig')
const path = resolve(__dirname, '../../chinook.db')
const db = openDb(path)
closeDb(db)
Start Electron-Vite in dev mode:
npm run dev
You should see a message indicating the sqlite module is being built. After a while the Electron window will open. That means our functions work. We don't see anything because all our code does is open and close the database file. Let us create a function that query the database.
Our function is going to run the following query:
SELECT a.AlbumId, a.Title, b.ArtistId, b.Name AS Artist
FROM albums a
INNER JOIN artists b ON a.ArtistId = b.ArtistId
WHERE a.Title LIKE '%' || ? || '%'
It has one bound variable, the search string. It's sandwiched between two %
to indicate that it
can occur anywhere within Title
.
We add the following struct to sqlite.zig
, which is used to store the columns returned by the
query:
pub const Album = struct {
AlbumId: u32,
Title: []const u8,
ArtistId: u32,
Artist: []const u8,
};
Then the following function:
pub fn printAlbums(db_op: SqliteOpaquePtr, search_str: []const u8) !void {
const db_ptr: *sqlite.Db = @ptrCast(db_op);
const query =
\\SELECT a.AlbumId, a.Title, b.ArtistId, b.Name AS Artist
\\FROM albums a
\\INNER JOIN artists b ON a.ArtistId = b.ArtistId
\\WHERE a.Title LIKE '%' || ? || '%'
;
// prepare sql query
var stmt = try db_ptr.prepare(query);
defer stmt.deinit();
const allocator = gpa.allocator();
// create arena allocator
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
// create iterator
var iterator = try stmt.iteratorAlloc(Album, arena.allocator(), .{search_str});
while (try iterator.nextAlloc(arena.allocator(), .{})) |row| {
std.debug.print("{d} {s}\n", .{ row.AlbumId, row.Title });
}
}
The code above should be largely self-explanatory. We prepare a statement, obtain an iterator, then loop through it.
In index.js
, we call this function:
const db = openDb(path)
printAlbums(db, 'best')
closeDb(db)
When you restart Electron-Vite, you should see the following:
dev server running for the electron renderer process at:
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
start electron app...
13 The Best Of Billy Cobham
20 The Best Of Buddy Guy - The Millenium Collection
47 The Best of Ed Motta
61 Knocking at Your Back Door: The Best Of Deep Purple in the 80's
83 My Way: The Best Of Frank Sinatra [Disc 1]
146 Seek And Shall Find: More Of The Best (1963-1981)
147 The Best Of Men At Work
190 The Best Of R.E.M.: The IRS Years
213 Pure Cult: The Best Of The Cult (For Rockers, Ravers, Lovers & Sinners) [UK]
221 My Generation - The Very Best Of The Who
238 The Best Of 1980-1990
241 UB40 The Best Of - Volume Two [UK]
243 The Best Of Van Halen, Vol. I
257 20th Century Masters - The Millennium Collection: The Best of Scorpions
268 The Best of Beethoven
So our function is working. It's not particularly useful though. Instead of dumping the database rows, we want it to return them to our JavaScript code. Perhaps we can just return the iterator? The answer is no. While Zigar supports iterator starting from version 0.13.1, on return both the statement and the arena allocator would get deinit'ed, rendering the iterator invalid.
What we need to do is create a wrapper iterator that hangs onto stmt
and arena
until iterator
is done:
fn Iterator(comptime T: type, query: []const u8) type {
const IteratorData = struct {
stmt: sqlite.StatementType(.{}, query),
iterator: sqlite.Iterator(T),
arena: std.heap.ArenaAllocator,
};
const IteratorDataOpaquePtr = *align(@alignOf(IteratorData)) opaque {};
return struct {
data_ptr: IteratorDataOpaquePtr,
fn init(db_op: SqliteOpaquePtr, params: anytype) !@This() {
// allocate data for fields used by iterator
const allocator = gpa.allocator();
const data_ptr = try allocator.create(IteratorData);
errdefer allocator.destroy(data_ptr);
// prepare sql query
const db_ptr: *sqlite.Db = @ptrCast(db_op);
data_ptr.stmt = try db_ptr.prepare(query);
errdefer data_ptr.stmt.deinit();
// create arena allocator
data_ptr.arena = std.heap.ArenaAllocator.init(allocator);
errdefer data_ptr.arena.deinit();
// create iterator
data_ptr.iterator = try data_ptr.stmt.iteratorAlloc(T, data_ptr.arena.allocator(), params);
return .{ .data_ptr = @ptrCast(data_ptr) };
}
fn deinit(self: *@This()) void {
const data_ptr: *IteratorData = @ptrCast(self.data_ptr);
data_ptr.stmt.deinit();
data_ptr.arena.deinit();
const allocator = gpa.allocator();
errdefer allocator.destroy(data_ptr);
}
pub fn next(self: *@This(), allocator: std.mem.Allocator) !?T {
errdefer self.deinit();
const data_ptr: *IteratorData = @ptrCast(self.data_ptr);
// return row if there's one, otherwise deinit the iterator
if (try data_ptr.iterator.nextAlloc(allocator, .{})) |row| {
return row;
} else {
self.deinit();
return null;
}
}
};
}
Now it's just a matter of defining a specific iterator for our query and write a function that initiatizes and returns it:
const FindAlbumsIterator = Iterator(Album,
\\SELECT a.AlbumId, a.Title, b.ArtistId, b.Name AS Artist
\\FROM albums a
\\INNER JOIN artists b ON a.ArtistId = b.ArtistId
\\WHERE a.Title LIKE '%' || ? || '%'
);
pub fn findAlbums(db_op: SqliteOpaquePtr, search_str: []const u8) !FindAlbumsIterator {
return try FindAlbumsIterator.init(db_op, .{search_str});
}
In index.js
we replace the call to printAlbums()
with the following:
for(const album of findAlbums(db, 'best')) {
console.log(`${album.AlbumId} ${album.Title.string}`)
}
When you restart Electron, you should see the same output as before.
Now let us write a second function, one that retrieves the tracks in an album. Most of the hard work is done already. We just need to define a struct and an iterator for the query:
pub const Track = struct {
TrackId: u32,
Name: []const u8,
Milliseconds: u32,
GenreId: u32,
Genre: []const u8,
};
const GetTracksIterator = Iterator(Track,
\\SELECT a.TrackId, a.Name, a.Milliseconds, b.GenreId, b.Name as Genre
\\FROM tracks a
\\INNER JOIN genres b ON a.GenreId = b.GenreId
\\WHERE a.AlbumId = ?
\\ORDER BY a.TrackId
);
pub fn getTracks(db_op: SqliteOpaquePtr, album_id: u32) !GetTracksIterator {
return try GetTracksIterator.init(db_op, .{album_id});
}
We replace the previous code in index.js
with what follows:
for (const track of getTracks(db, 147)) {
console.log(`${track.Name.string}`)
}
And here's the new output:
dev server running for the electron renderer process at:
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
start electron app...
Down Under
Overkill
Be Good Johnny
Everything I Need
Down by the Sea
Who Can It Be Now?
It's a Mistake
Dr. Heckyll & Mr. Jive
Shakes and Ladders
No Sign of Yesterday
Now that we have a few backend functions, it's time to create the UI for our app. First we need to
set up the basic plumbing. We'll move these lines of code to the top of index.js
:
require ('node-zigar/cjs')
const { openDb, closeDb, findAlbums, getTracks } = require('../../zig/sqlite.zig')
const path = resolve(__dirname, '../../chinook.db')
const db = openDb(path)
The call to closeDb()
gets moved into an event handle:
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('quit', () => closeDb(db))
We replace the following:
// IPC test
ipcMain.on('ping', () => console.log('pong'))
with:
ipcMain.handle('findAlbums', (_, searchStr) => toArray(findAlbums(db, searchStr)))
ipcMain.handle('getTracks', (_, albumId) => toArray(getTracks(db, albumId)))
That allows the frontend to invoke our backend functions. toArray()
is a function that takes
an iterator and places its contents into an array:
function toArray(iterator) {
return [ ...iterator ].map(row => {
const object = {};
for (const [ name, value ] of row) {
object[name] = (typeof(value) === 'object') ? value.string : value;
}
return object
})
}
In the future, we will be able to simplify the above code to this:
function toArray(iterator) {
return [ ...iterator ].map(r => r.valueOf())
}
As of 0.13.0, Zig does not provide a mean to indicate that []const u8
is a string. A
call to valueOf()
would therefore produce something like this:
{
AlbumId: 215,
Title: [
84, 104, 101, 32, 80, 111, 108,
105, 99, 101, 32, 71, 114, 101,
97, 116, 101, 115, 116, 32, 72,
105, 116, 115
],
ArtistId: 141,
Artist: [
84, 104, 101, 32,
80, 111, 108, 105,
99, 101
]
}
In any event, the plumbing is done and it's time to work on the React part. Open
src/renderer/src/App.jsx
and replace the code with the following:
import { useCallback, useDeferredValue, useEffect, useState } from 'react';
function App() {
const [ albums, setAlbums ] = useState([])
const [ tracks, setTracks ] = useState([])
const [ searchString, setSearchString ] = useState('')
const [ selectedAlbumId, setSelectedAlbumId ] = useState()
const deferredSearchString = useDeferredValue(searchString)
const onSearchChange = useCallback((evt) => {
setSearchString(evt.target.value)
}, [])
const onAlbumClick = useCallback((evt) => {
if (evt.target.tagName === 'LI') {
setSelectedAlbumId(parseInt(evt.target.dataset.albumId))
}
}, [])
useEffect(() => {
window.electron.ipcRenderer.invoke('findAlbums', deferredSearchString).then(setAlbums)
}, [ deferredSearchString ])
useEffect(() => {
if (selectedAlbumId !== undefined) {
window.electron.ipcRenderer.invoke('getTracks', selectedAlbumId).then(setTracks)
} else {
setTracks([])
}
}, [ selectedAlbumId ])
useEffect(() => {
if (selectedAlbumId) {
if (!albums.find(a => a.AlbumId === selectedAlbumId)) {
setSelectedAlbumId(undefined)
}
}
}, [ albums ])
return (
<>
<div id="header">
<input id="search" value={searchString} onChange={onSearchChange} />
<div id="toolbar"></div>
</div>
<div id="content">
<ul id="album-list" onClick={onAlbumClick}>
{albums.map(album =>
<li className={album.AlbumId === selectedAlbumId ? 'selected' : ''} data-album-id={album.AlbumId}>{album.Title}</li>)
}
</ul>
<ul id="track-list">
{tracks.map(track =>
<li data-track-id={track.TrackId}>[{formatTime(track.Milliseconds)}] {track.Name}</li>)
}
</ul>
</div>
</>
)
}
function formatTime(ms) {
const min = Math.floor(ms / 60000).toString()
let sec = Math.floor((ms % 60000) / 1000).toString()
if (sec.length == 1) {
sec = '0' + sec;
}
return `${min}:${sec}`
}
export default App
Then src/renderer/src/assets/main.css
:
@import './base.css';
body {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background-image: url('./wavy-lines.svg');
background-size: cover;
user-select: none;
}
#root {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
}
#header {
flex: 0 0 auto;
display: flex;
flex-direction: row;
padding: .5em .5em .5em 1em;
}
#content {
flex: 1 1 auto;
display: flex;
flex-direction: row;
overflow: hidden;
}
#search {
flex: 1 1 50%;
display: block;
}
#toolbar {
flex: 1 1 50%;
display: flex;
flex-direction: row;
justify-content: end;
}
#album-list {
flex: 1 1 50%;
overflow: auto;
height: 100%;
margin-left: 0;
padding-left: 1em;
list-style:none;
}
#album-list LI {
cursor: pointer;
padding-left: 2px;
}
#album-list LI.selected {
color: #000000;
background-color: #FFFFAA;
}
#track-list {
flex: 1 1 50%;
overflow: auto;
padding-left: 1em;
list-style:none;
}
Start Electron-Vite in dev mode again:
npm run dev
You should see the new UI:
It's fairly simple but functional. You can search for a album by name and you can see the tracks the album contains.
Let us now implement an extra bit of functionality to our app: the ability to add an album. Since
we've our React code in front of us already, we'll start from the front-end this time. We alter
App.jsx
so that it would bring up a form when a button is pressed:
import { useCallback, useDeferredValue, useEffect, useState } from 'react';
function App() {
const [ screen, setScreen ] = useState('main')
const [ albums, setAlbums ] = useState([])
const [ tracks, setTracks ] = useState([])
const [ searchString, setSearchString ] = useState('')
const [ title, setTitle ] = useState('')
const [ artist, setArtist ] = useState('')
const [ error, setError ] = useState('')
const [ selectedAlbumId, setSelectedAlbumId ] = useState()
const deferredSearchString = useDeferredValue(searchString)
const onSearchChange = useCallback((evt) => {
setSearchString(evt.target.value)
}, [])
const onAlbumClick = useCallback((evt) => {
if (evt.target.tagName === 'LI') {
setSelectedAlbumId(parseInt(evt.target.dataset.albumId))
}
}, [])
const onAddClick = useCallback((evt) => {
setScreen('add')
})
const onTitleChange = useCallback((evt) => {
setTitle(evt.target.value)
})
const onArtistChange = useCallback((evt) => {
setArtist(evt.target.value)
})
const onSaveClick = useCallback(async (evt) => {
try {
await window.electron.ipcRenderer.invoke('addAlbum', { Title: title, Artist: artist })
setScreen('main')
setTitle('')
setArtist('')
} catch (err) {
setError(err.message)
}
})
const onCancelClick = useCallback((evt) => {
setScreen('main')
})
useEffect(() => {
window.electron.ipcRenderer.invoke('findAlbums', deferredSearchString).then(setAlbums)
}, [ deferredSearchString ])
useEffect(() => {
if (selectedAlbumId !== undefined) {
window.electron.ipcRenderer.invoke('getTracks', selectedAlbumId).then(setTracks)
} else {
setTracks([])
}
}, [ selectedAlbumId ])
useEffect(() => {
if (selectedAlbumId) {
if (!albums.find(a => a.AlbumId === selectedAlbumId)) {
setSelectedAlbumId(undefined)
}
}
}, [ albums ])
switch (screen) {
case 'add':
return (
<div id="form-add">
<section>
<label htmlFor="title">Title:</label>
<input id="title" value={title} onChange={onTitleChange} />
</section>
<section>
<label htmlFor="title">Artist:</label>
<input id="artist" value={artist} onChange={onArtistChange} />
</section>
<section>
<div id="error">{error}</div>
</section>
<section>
<button onClick={onSaveClick}>Save</button>
<button onClick={onCancelClick}>Cancel</button>
</section>
</div>
)
default:
return (
<>
<div id="header">
<input id="search" value={searchString} onChange={onSearchChange} />
<div id="toolbar">
<button onClick={onAddClick}>Add</button>
</div>
</div>
<div id="content">
<ul id="album-list" onClick={onAlbumClick}>
{albums.map(album =>
<li className={album.AlbumId === selectedAlbumId ? 'selected' : ''} data-album-id={album.AlbumId}>{album.Title}</li>)
}
</ul>
<ul id="track-list">
{tracks.map(track =>
<li data-track-id={track.TrackId}>[{formatTime(track.Milliseconds)}] {track.Name}</li>)
}
</ul>
</div>
</>
)
}
}
function formatTime(ms) {
const min = Math.floor(ms / 60000).toString()
let sec = Math.floor((ms % 60000) / 1000).toString()
if (sec.length == 1) {
sec = '0' + sec;
}
return `${min}:${sec}`
}
export default App
Styles for the form elements:
#form-add {
flex: 1 1 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#form-add SECTION {
padding: 5px 5px 5px 5px;
}
#form-add LABEL {
display: block;
padding-bottom: 0;
}
#form-add INPUT {
width: 50vw;
}
#form-add BUTTON {
margin-left: 5px;
}
#error {
color: #ff0000;
}
When the "Save" button is clicked, the following code is invoked:
try {
await window.electron.ipcRenderer.invoke('addAlbum', { Title: title, Artist: artist })
setScreen('main')
setTitle('')
setArtist('')
} catch (err) {
setError(err.message)
}
We add the handler to src/index.js
, which simply invokes the Zig function addAlbum()
:
ipcMain.handle('addAlbum', (_, album) => addAlbum(db, album))
We need to add this function to the import statement:
const { openDb, closeDb, findAlbums, getTracks, addAlbum } = require('../../zig/sqlite.zig')
We then define addAlbum()
itself in zig/sqlite.zig
. First, we need to change openDb()
so that the database is writable:
pub fn openDb(path: [:0]const u8) !SqliteOpaquePtr {
const allocator = gpa.allocator();
const db_ptr = try allocator.create(sqlite.Db);
errdefer allocator.destroy(db_ptr);
db_ptr.* = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{ .write = true }, // <--
.threading_mode = .MultiThread,
});
return @ptrCast(db_ptr);
}
We also need to change the Album
struct so that the ids it contains are optional:
pub const Album = struct {
AlbumId: ?u32 = null,
Title: []const u8,
ArtistId: ?u32 = null,
Artist: []const u8,
};
And here's the function:
pub fn addAlbum(db_op: SqliteOpaquePtr, album: *Album) !void {
const db_ptr: *sqlite.Db = @ptrCast(db_op);
if (album.ArtistId == null) {
const find_artist = "SELECT ArtistId FROM artists WHERE Name = ?";
if (try db_ptr.one(u32, find_artist, .{}, .{album.Artist})) |id| {
album.ArtistId = id;
} else {
const insert_artist = "INSERT INTO artists (Name) VALUES (?)";
try db_ptr.exec(insert_artist, .{}, .{album.Artist});
album.ArtistId = @intCast(db_ptr.getLastInsertRowID());
}
}
const insert_album = "INSERT INTO albums (Title, ArtistId) VALUES (?, ?)";
try db_ptr.exec(insert_album, .{}, .{ album.Title, album.ArtistId });
album.AlbumId = @intCast(db_ptr.getLastInsertRowID());
}
It inserts a new row into the table artists
if an existing row is not found. Then it inserts a
row into the table albums
. The struct passed into the function is updated with the new ids in
the event the caller needs that information.
Finally, we need to create a writable copy of the database. Go back to main/index.js
and replace
the DB opening code with following:
const path = join(app.getPath('documents'), 'chinook.db')
if (!existsSync(path)) {
const src = resolve(__dirname, '../../chinook.db')
copyFileSync(src, path)
}
const db = openDb(path)
And add the necessary imports:
import { copyFileSync, existsSync } from 'fs';
The first step is to change the require statement so that it loads a module instead of a source file:
const { openDb, closeDb, findAlbums, getTracks, addAlbum } = require('../../lib/sqlite.zigar')
Then we add the config file node-zigar.config.json
, which contains information regarding our
source files and platforms we intend to support:
{
"optimize": "ReleaseSmall",
"sourceFiles": {
"out/lib/sqlite.zigar": "zig/sqlite.zig"
},
"targets": [
{ "platform": "win32", "arch": "x64" },
{ "platform": "win32", "arch": "arm64" },
{ "platform": "win32", "arch": "ia32" },
{ "platform": "linux", "arch": "x64" },
{ "platform": "linux", "arch": "arm64" },
{ "platform": "darwin", "arch": "x64" },
{ "platform": "darwin", "arch": "arm64" }
]
}
Run the following command to generate library files for the said platforms:
npx node-zigar build
In electron-builder.yml
, add the following rules to the files
section:
files:
# ...
- '!zig/*'
- '!zig-cache/*'
- '!node-zigar.config.json'
Then add an additional rule to the asarUnpacked
section:
asarUnpack:
# ...
- out/lib/**
And finally, because we want to create packages supporting different CPU architectures, we want
the arch
variable in the package name:
nsis:
artifactName: ${name}-${version}-${arch}-setup.${ext}
dmg:
artifactName: ${name}-${version}-${arch}.${ext}
appImage:
artifactName: ${name}-${version}-${arch}.${ext}
To build for Linux, run the following commands:
npm run build:linux --x64
npm run build:linux --arm64
For Windows:
npm run build:win --ia32
npm run build:win --x64
npm run build:win --arm64
And Mac OS:
npm run build:mac --x64
npm run build:mac --arm64
You can find the complete source code for this example here.
I hope this example gave you a pretty good idea on what you can and cannot do with zig-sqlite. The library is very much designed with Zig programming in mind. A lot of things happen at comptime. As such, using it only makes sense if you're going to write most of your backend code in Zig. You can't really expose zig-sqlite's functionalities piecemeal to the JavaScript side.
In a future example, we'll explore the possibility of utilizing SQLite's C API directly. Stay tuned!