Skip to content

Commit

Permalink
Merge 'bindings/go: Begin implementation of Go database/sql driver' f…
Browse files Browse the repository at this point in the history
…rom Preston Thorpe

This WIP driver uses the [purego](github.com/ebitengine/purego) library,
that supports cross platform `Dlopen`/`Dlsym` and not a whole lot else.
I really didn't want to use CGO, have very little experience with WASM
and I heard nothing but good things about this library. It's very easy
to use and stable especially when you consider the use case here of 3
functions.
![image](https://github.com/user-
attachments/assets/ae28c8f2-1d11-4d25-b999-22af8bd65a92)
NOTE: The WIP state that this PR is in right at this moment, is not able
to run these simple queries. This screengrab was taken from a couple
days ago when I wrote up a quick demo to load the library, call a simple
query and had it println! the result to make sure everything was working
properly.
I am opening this so kind of like the Java bindings, I can incrementally
work on this. I didn't want to submit a massive PR, try to keep them at
~1k lines max. The state of what's in this PR is highly subject and
likely to change.
I will update when they are at a working state where they can be tested
out and make sure they work across platforms.

Closes tursodatabase#776
  • Loading branch information
penberg committed Jan 26, 2025
2 parents 538b3ef + 32c985f commit 6c80b1d
Show file tree
Hide file tree
Showing 13 changed files with 1,304 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"bindings/python",
"bindings/rust",
"bindings/wasm",
"bindings/go",
"cli",
"core",
"extensions/core",
Expand Down
23 changes: 23 additions & 0 deletions bindings/go/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "turso-go"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true

[lib]
name = "_turso_go"
crate-type = ["cdylib"]
path = "rs_src/lib.rs"

[features]
default = ["io_uring"]
io_uring = ["limbo_core/io_uring"]


[dependencies]
limbo_core = { path = "../../core/" }

[target.'cfg(target_os = "linux")'.dependencies]
limbo_core = { path = "../../core/", features = ["io_uring"] }
8 changes: 8 additions & 0 deletions bindings/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module turso

go 1.23.4

require (
github.com/ebitengine/purego v0.8.2
golang.org/x/sys/windows v0.29.0
)
4 changes: 4 additions & 0 deletions bindings/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
203 changes: 203 additions & 0 deletions bindings/go/rs_src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
mod rows;
#[allow(dead_code)]
mod statement;
mod types;
use limbo_core::{Connection, Database, LimboError};
use std::{
ffi::{c_char, c_void},
rc::Rc,
str::FromStr,
sync::Arc,
};

/// # Safety
/// Safe to be called from Go with null terminated DSN string.
/// performs null check on the path.
#[no_mangle]
pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
if path.is_null() {
println!("Path is null");
return std::ptr::null_mut();
}
let path = unsafe { std::ffi::CStr::from_ptr(path) };
let path = path.to_str().unwrap();
let db_options = parse_query_str(path);
if let Ok(io) = get_io(&db_options.path) {
let db = Database::open_file(io.clone(), &db_options.path.to_string());
match db {
Ok(db) => {
println!("Opened database: {}", path);
let conn = db.connect();
return TursoConn::new(conn, io).to_ptr();
}
Err(e) => {
println!("Error opening database: {}", e);
return std::ptr::null_mut();
}
};
}
std::ptr::null_mut()
}

#[allow(dead_code)]
struct TursoConn {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
}

impl TursoConn {
fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
TursoConn { conn, io }
}
#[allow(clippy::wrong_self_convention)]
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}

fn from_ptr(ptr: *mut c_void) -> &'static mut TursoConn {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoConn) }
}
}

/// Close the database connection
/// # Safety
/// safely frees the connection's memory
#[no_mangle]
pub unsafe extern "C" fn db_close(db: *mut c_void) {
if !db.is_null() {
let _ = unsafe { Box::from_raw(db as *mut TursoConn) };
}
}

#[allow(clippy::arc_with_non_send_sync)]
fn get_io(db_location: &DbType) -> Result<Arc<dyn limbo_core::IO>, LimboError> {
Ok(match db_location {
DbType::Memory => Arc::new(limbo_core::MemoryIO::new()?),
_ => {
return Ok(Arc::new(limbo_core::PlatformIO::new()?));
}
})
}

#[allow(dead_code)]
struct DbOptions {
path: DbType,
params: Parameters,
}

#[derive(Default, Debug, Clone)]
enum DbType {
File(String),
#[default]
Memory,
}

impl std::fmt::Display for DbType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DbType::File(path) => write!(f, "{}", path),
DbType::Memory => write!(f, ":memory:"),
}
}
}

#[derive(Debug, Clone, Default)]
struct Parameters {
mode: Mode,
cache: Option<Cache>,
vfs: Option<String>,
nolock: bool,
immutable: bool,
modeof: Option<String>,
}

impl FromStr for Parameters {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.contains('?') {
return Ok(Parameters::default());
}
let mut params = Parameters::default();
for param in s.split('?').nth(1).unwrap().split('&') {
let mut kv = param.split('=');
match kv.next() {
Some("mode") => params.mode = kv.next().unwrap().parse().unwrap(),
Some("cache") => params.cache = Some(kv.next().unwrap().parse().unwrap()),
Some("vfs") => params.vfs = Some(kv.next().unwrap().to_string()),
Some("nolock") => params.nolock = true,
Some("immutable") => params.immutable = true,
Some("modeof") => params.modeof = Some(kv.next().unwrap().to_string()),
_ => {}
}
}
Ok(params)
}
}

#[derive(Default, Debug, Clone, Copy)]
enum Cache {
Shared,
#[default]
Private,
}

impl FromStr for Cache {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"shared" => Ok(Cache::Shared),
_ => Ok(Cache::Private),
}
}
}

#[allow(clippy::enum_variant_names)]
#[derive(Default, Debug, Clone, Copy)]
enum Mode {
ReadOnly,
ReadWrite,
#[default]
ReadWriteCreate,
}

impl FromStr for Mode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"readonly" | "ro" => Ok(Mode::ReadOnly),
"readwrite" | "rw" => Ok(Mode::ReadWrite),
"readwritecreate" | "rwc" => Ok(Mode::ReadWriteCreate),
_ => Ok(Mode::default()),
}
}
}

// At this point we don't have configurable parameters but many
// DSN's are going to have query parameters
fn parse_query_str(mut path: &str) -> DbOptions {
if path == ":memory:" {
return DbOptions {
path: DbType::Memory,
params: Parameters::default(),
};
}
if path.starts_with("sqlite://") {
path = &path[10..];
}
if path.contains('?') {
let parameters = Parameters::from_str(path).unwrap();
let path = &path[..path.find('?').unwrap()];
DbOptions {
path: DbType::File(path.to_string()),
params: parameters,
}
} else {
DbOptions {
path: DbType::File(path.to_string()),
params: Parameters::default(),
}
}
}
Loading

0 comments on commit 6c80b1d

Please sign in to comment.