Skip to content

Commit

Permalink
Add tests for first extension
Browse files Browse the repository at this point in the history
  • Loading branch information
PThorpe92 committed Jan 13, 2025
1 parent ac11b62 commit ffcbf9c
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 70 deletions.
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,15 @@ limbo-wasm:
cargo build --package limbo-wasm --target wasm32-wasi
.PHONY: limbo-wasm

test: limbo test-compat test-sqlite3 test-shell
test: limbo test-compat test-sqlite3 test-shell test-extensions
.PHONY: test

test-shell: limbo
test-extensions: limbo
cargo build --package limbo_uuid
./testing/extensions.py
.PHONY: test-extensions

test-shell: limbo
SQLITE_EXEC=$(SQLITE_EXEC) ./testing/shelltests.py
.PHONY: test-shell

Expand Down
5 changes: 4 additions & 1 deletion cli/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ impl Limbo {
};
}

#[cfg(not(target_family = "wasm"))]
fn handle_load_extension(&mut self, path: &str) -> Result<(), String> {
self.conn.load_extension(path).map_err(|e| e.to_string())
}
Expand Down Expand Up @@ -504,7 +505,9 @@ impl Limbo {
let _ = self.writeln(e.to_string());
};
}
Command::LoadExtension => {
Command::LoadExtension =>
{
#[cfg(not(target_family = "wasm"))]
if let Err(e) = self.handle_load_extension(args[1]) {
let _ = self.writeln(&e);
}
Expand Down
5 changes: 5 additions & 0 deletions core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ mod vdbe;
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

use fallible_iterator::FallibleIterator;
#[cfg(not(target_family = "wasm"))]
use libloading::{Library, Symbol};
#[cfg(not(target_family = "wasm"))]
use limbo_extension::{ExtensionApi, ExtensionEntryPoint, RESULT_OK};
use log::trace;
use schema::Schema;
Expand Down Expand Up @@ -176,6 +178,7 @@ impl Database {
.insert(name.as_ref().to_string(), func.into());
}

#[cfg(not(target_family = "wasm"))]
pub fn load_extension(&self, path: &str) -> Result<()> {
let api = Box::new(self.build_limbo_extension());
let lib =
Expand Down Expand Up @@ -394,6 +397,7 @@ impl Connection {
Ok(())
}

#[cfg(not(target_family = "wasm"))]
pub fn load_extension(&self, path: &str) -> Result<()> {
Database::load_extension(self.db.as_ref(), path)
}
Expand Down Expand Up @@ -496,6 +500,7 @@ impl Rows {

pub(crate) struct SymbolTable {
pub functions: HashMap<String, Rc<crate::function::ExternalFunc>>,
#[cfg(not(target_family = "wasm"))]
extensions: Vec<(libloading::Library, *const ExtensionApi)>,
}

Expand Down
25 changes: 10 additions & 15 deletions core/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,35 +107,30 @@ impl OwnedValue {
}

pub fn from_ffi(v: &ExtValue) -> Self {
if v.value.is_null() {
return OwnedValue::Null;
}
match v.value_type {
match v.value_type() {
ExtValueType::Null => OwnedValue::Null,
ExtValueType::Integer => {
let int_ptr = v.value as *mut i64;
let integer = unsafe { *int_ptr };
OwnedValue::Integer(integer)
let Some(int) = v.to_integer() else {
return OwnedValue::Null;
};
OwnedValue::Integer(int)
}
ExtValueType::Float => {
let float_ptr = v.value as *mut f64;
let float = unsafe { *float_ptr };
let Some(float) = v.to_float() else {
return OwnedValue::Null;
};
OwnedValue::Float(float)
}
ExtValueType::Text => {
let Some(text) = v.to_text() else {
return OwnedValue::Null;
};
OwnedValue::build_text(std::rc::Rc::new(unsafe { text.as_str().to_string() }))
OwnedValue::build_text(std::rc::Rc::new(text))
}
ExtValueType::Blob => {
let Some(blob_ptr) = v.to_blob() else {
let Some(blob) = v.to_blob() else {
return OwnedValue::Null;
};
let blob = unsafe {
let slice = std::slice::from_raw_parts(blob_ptr.data, blob_ptr.size as usize);
slice.to_vec()
};
OwnedValue::Blob(std::rc::Rc::new(blob))
}
}
Expand Down
19 changes: 8 additions & 11 deletions extensions/uuid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ declare_scalar_functions! {
uuid::Timestamp::now(ctx)
} else {
let arg = &args[0];
match arg.value_type {
match arg.value_type() {
ValueType::Integer => {
let ctx = uuid::ContextV7::new();
let Some(int) = arg.to_integer() else {
Expand All @@ -47,8 +47,7 @@ declare_scalar_functions! {
let Some(text) = arg.to_text() else {
return Value::null();
};
let parsed = unsafe{text.as_str()}.parse::<i64>();
match parsed {
match text.parse::<i64>() {
Ok(unix) => {
if unix <= 0 {
return Value::null();
Expand All @@ -70,7 +69,7 @@ declare_scalar_functions! {
let timestamp = if args.is_empty() {
let ctx = uuid::ContextV7::new();
uuid::Timestamp::now(ctx)
} else if args[0].value_type == limbo_extension::ValueType::Integer {
} else if args[0].value_type() == limbo_extension::ValueType::Integer {
let ctx = uuid::ContextV7::new();
let Some(int) = args[0].to_integer() else {
return Value::null();
Expand All @@ -86,21 +85,20 @@ declare_scalar_functions! {

#[args(1)]
fn exec_ts_from_uuid7(args: &[Value]) -> Value {
match args[0].value_type {
match args[0].value_type() {
ValueType::Blob => {
let Some(blob) = &args[0].to_blob() else {
return Value::null();
};
let slice = unsafe{ std::slice::from_raw_parts(blob.data, blob.size as usize)};
let uuid = uuid::Uuid::from_slice(slice).unwrap();
let uuid = uuid::Uuid::from_slice(blob.as_slice()).unwrap();
let unix = uuid_to_unix(uuid.as_bytes());
Value::from_integer(unix as i64)
}
ValueType::Text => {
let Some(text) = args[0].to_text() else {
return Value::null();
};
let Ok(uuid) = uuid::Uuid::parse_str(unsafe {text.as_str()}) else {
let Ok(uuid) = uuid::Uuid::parse_str(&text) else {
return Value::null();
};
let unix = uuid_to_unix(uuid.as_bytes());
Expand All @@ -115,8 +113,7 @@ declare_scalar_functions! {
let Some(blob) = args[0].to_blob() else {
return Value::null();
};
let slice = unsafe{ std::slice::from_raw_parts(blob.data, blob.size as usize)};
let parsed = uuid::Uuid::from_slice(slice).ok().map(|u| u.to_string());
let parsed = uuid::Uuid::from_slice(blob.as_slice()).ok().map(|u| u.to_string());
match parsed {
Some(s) => Value::from_text(s),
None => Value::null()
Expand All @@ -128,7 +125,7 @@ declare_scalar_functions! {
let Some(text) = args[0].to_text() else {
return Value::null();
};
match uuid::Uuid::parse_str(unsafe {text.as_str()}) {
match uuid::Uuid::parse_str(&text) {
Ok(uuid) => {
Value::from_blob(uuid.as_bytes().to_vec())
}
Expand Down
62 changes: 21 additions & 41 deletions limbo_extension/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,6 @@ macro_rules! register_scalar_functions {
}
}

/// Provide a cleaner interface to define scalar functions to extension authors
/// . e.g.
/// ```
/// #[args(1)]
/// fn scalar_double(args: &[Value]) -> Value {
/// Value::from_integer(args[0].integer * 2)
/// }
///
/// #[args(0..=2)]
/// fn scalar_sum(args: &[Value]) -> Value {
/// Value::from_integer(args.iter().map(|v| v.integer).sum())
/// ```
///
#[macro_export]
macro_rules! declare_scalar_functions {
(
Expand Down Expand Up @@ -100,7 +87,7 @@ macro_rules! declare_scalar_functions {
}

#[repr(C)]
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum ValueType {
Null,
Integer,
Expand All @@ -111,8 +98,8 @@ pub enum ValueType {

#[repr(C)]
pub struct Value {
pub value_type: ValueType,
pub value: *mut c_void,
value_type: ValueType,
value: *mut c_void,
}

impl std::fmt::Debug for Value {
Expand Down Expand Up @@ -161,41 +148,27 @@ impl Default for TextValue {
}

impl TextValue {
pub fn new(text: *const u8, len: usize) -> Self {
pub(crate) fn new(text: *const u8, len: usize) -> Self {
Self {
text,
len: len as u32,
}
}

/// # Safety
/// Safe to call if the pointer is null, returns None
/// if the value is not a text type or if the value is null
pub unsafe fn from_value(value: &Value) -> Option<&Self> {
if value.value_type != ValueType::Text {
return None;
}
if value.value.is_null() {
return None;
}
Some(&*(value.value as *const TextValue))
}

/// # Safety
/// If self.text is null we safely return an empty string but
/// the caller must ensure that the underlying value is valid utf8
pub unsafe fn as_str(&self) -> &str {
fn as_str(&self) -> &str {
if self.text.is_null() {
return "";
}
std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.text, self.len as usize))
unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.text, self.len as usize))
}
}
}

#[repr(C)]
pub struct Blob {
pub data: *const u8,
pub size: u64,
data: *const u8,
size: u64,
}

impl std::fmt::Debug for Blob {
Expand All @@ -218,6 +191,10 @@ impl Value {
}
}

pub fn value_type(&self) -> ValueType {
self.value_type
}

pub fn to_float(&self) -> Option<f64> {
if self.value_type != ValueType::Float {
return None;
Expand All @@ -228,24 +205,27 @@ impl Value {
Some(unsafe { *(self.value as *const f64) })
}

pub fn to_text(&self) -> Option<&TextValue> {
pub fn to_text(&self) -> Option<String> {
if self.value_type != ValueType::Text {
return None;
}
if self.value.is_null() {
return None;
}
unsafe { Some(&*(self.value as *const TextValue)) }
let txt = unsafe { &*(self.value as *const TextValue) };
Some(String::from(txt.as_str()))
}

pub fn to_blob(&self) -> Option<&Blob> {
pub fn to_blob(&self) -> Option<Vec<u8>> {
if self.value_type != ValueType::Blob {
return None;
}
if self.value.is_null() {
return None;
}
unsafe { Some(&*(self.value as *const Blob)) }
let blob = unsafe { &*(self.value as *const Blob) };
let slice = unsafe { std::slice::from_raw_parts(blob.data, blob.size as usize) };
Some(slice.to_vec())
}

pub fn to_integer(&self) -> Option<i64> {
Expand Down
Loading

0 comments on commit ffcbf9c

Please sign in to comment.