WASM Procedures: Language SDKs
WASM Procedures: Language SDKs
Part of: WASM Procedures User Guide
Rust SDK
Setup
Cargo.toml:
[package]name = "my_procedure"version = "1.0.0"edition = "2021"
[lib]crate-type = ["cdylib"]
[dependencies]heliosdb-sdk = "1.0.0"serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
[profile.release]opt-level = "z" # Optimize for sizelto = true # Enable link-time optimizationcodegen-units = 1 # Better optimizationstrip = true # Strip symbolsBasic Procedure
use heliosdb_sdk::*;use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]struct User { id: i64, name: String, email: String,}
/// Get user by ID from database#[procedure]pub fn get_user(user_id: i64) -> Result<User, String> { let result = exec_sql(&format!("SELECT id, name, email FROM users WHERE id = {}", user_id))?;
if result.is_empty() { return Err(format!("User {} not found", user_id)); }
let user = User { id: result.get_i64(0, 0)?, name: result.get_string(0, 1)?, email: result.get_string(0, 2)?, };
Ok(user)}Type-Safe Bindings
use heliosdb_sdk::*;
// Strongly typed parameters and return values#[procedure]pub fn process_order(order_id: i64, apply_discount: bool) -> Result<OrderResult, String> { // Type-safe SQL execution let order: Order = exec_sql_typed( "SELECT * FROM orders WHERE id = ?", &[&order_id] )?;
// Type-safe JSON serialization let result = OrderResult { order_id: order.id, total: order.total, processed: true, };
Ok(result)}
#[derive(Debug, Serialize, Deserialize)]struct Order { id: i64, total: f64, status: String,}
#[derive(Debug, Serialize, Deserialize)]struct OrderResult { order_id: i64, total: f64, processed: bool,}Error Handling
use heliosdb_sdk::*;use thiserror::Error;
#[derive(Error, Debug)]pub enum ProcedureError { #[error("Database error: {0}")] Database(String),
#[error("Invalid input: {0}")] InvalidInput(String),
#[error("Business logic error: {0}")] BusinessLogic(String),}
#[procedure]pub fn transfer_funds( from_account: i64, to_account: i64, amount: f64) -> Result<(), ProcedureError> { // Validation if amount <= 0.0 { return Err(ProcedureError::InvalidInput("Amount must be positive".into())); }
// Start transaction let tx = begin_transaction() .map_err(|e| ProcedureError::Database(e))?;
// Check balance let balance: f64 = exec_sql_typed( "SELECT balance FROM accounts WHERE id = ?", &[&from_account] ).map_err(|e| ProcedureError::Database(e))?;
if balance < amount { rollback_transaction(tx)?; return Err(ProcedureError::BusinessLogic("Insufficient funds".into())); }
// Execute transfer exec_sql(&format!("UPDATE accounts SET balance = balance - {} WHERE id = {}", amount, from_account)) .map_err(|e| ProcedureError::Database(e))?;
exec_sql(&format!("UPDATE accounts SET balance = balance + {} WHERE id = {}", amount, to_account)) .map_err(|e| ProcedureError::Database(e))?;
commit_transaction(tx) .map_err(|e| ProcedureError::Database(e))?;
log_info(&format!("Transferred ${} from {} to {}", amount, from_account, to_account));
Ok(())}Testing
#[cfg(test)]mod tests { use super::*;
#[test] fn test_calculate_discount() { let discount = calculate_discount(1000.0, "gold").unwrap(); assert_eq!(discount, 150.0); }
#[test] fn test_invalid_tier() { let discount = calculate_discount(1000.0, "platinum").unwrap(); assert_eq!(discount, 0.0); }
#[test] fn test_transfer_invalid_amount() { let result = transfer_funds(1, 2, -100.0); assert!(result.is_err()); }}Build & Test:
# Run testscargo test
# Build releasecargo build --target wasm32-wasi --release
# Check binary sizels -lh target/wasm32-wasi/release/*.wasm
# Optimize with wasm-optwasm-opt -Oz target/wasm32-wasi/release/my_procedure.wasm \ -o procedure_optimized.wasmJavaScript/TypeScript SDK
Setup
package.json:
{ "name": "my-procedure", "version": "1.0.0", "scripts": { "asbuild:debug": "asc assembly/index.ts --target debug", "asbuild:release": "asc assembly/index.ts --target release --optimize", "test": "npm run asbuild:debug && node tests/test.js" }, "devDependencies": { "assemblyscript": "^0.27.0", "@heliosdb/wasm-sdk": "^1.0.0" }}asconfig.json:
{ "targets": { "debug": { "outFile": "build/debug.wasm", "textFile": "build/debug.wat", "sourceMap": true, "debug": true }, "release": { "outFile": "build/release.wasm", "textFile": "build/release.wat", "sourceMap": false, "optimizeLevel": 3, "shrinkLevel": 2, "converge": true, "noAssert": true } }, "options": { "bindings": "esm" }}Basic Procedure
assembly/index.ts:
import { execSql, beginTransaction, commitTransaction, rollbackTransaction, log, ResultSet} from "@heliosdb/wasm-sdk";
export function getUserCount(): i32 { const result = execSql("SELECT COUNT(*) FROM users"); return result.getInt(0, 0);}
export function createUser(name: string, email: string): i64 { const result = execSql( `INSERT INTO users (name, email) VALUES ('${name}', '${email}') RETURNING id` );
const userId = result.getInt64(0, 0); log("info", `Created user ${userId}: ${name}`);
return userId;}AssemblyScript Usage
import { execSql, log } from "@heliosdb/wasm-sdk";
// Type-safe functionexport function processOrder(orderId: i64, discount: f64): f64 { // Query with type safety const result = execSql( `SELECT total FROM orders WHERE id = ${orderId}` );
if (result.rowCount() === 0) { log("error", `Order ${orderId} not found`); return 0.0; }
const total = result.getFloat(0, 0); const finalTotal = total * (1.0 - discount);
// Update order execSql( `UPDATE orders SET final_total = ${finalTotal} WHERE id = ${orderId}` );
return finalTotal;}
// Working with arraysexport function calculateAveragePrice(productIds: Array<i64>): f64 { let sum: f64 = 0.0; let count: i32 = 0;
for (let i = 0; i < productIds.length; i++) { const result = execSql( `SELECT price FROM products WHERE id = ${productIds[i]}` );
if (result.rowCount() > 0) { sum += result.getFloat(0, 0); count++; } }
return count > 0 ? sum / f64(count) : 0.0;}Async/Await Pattern
import { execSqlAsync, logAsync } from "@heliosdb/wasm-sdk";
export async function processOrderAsync(orderId: i64): Promise<boolean> { await logAsync("info", `Processing order ${orderId}`);
try { // Async database operations const result = await execSqlAsync( `SELECT status FROM orders WHERE id = ${orderId}` );
if (result.rowCount() === 0) { throw new Error("Order not found"); }
const status = result.getString(0, 0);
if (status === "pending") { await execSqlAsync( `UPDATE orders SET status = 'processed' WHERE id = ${orderId}` ); return true; }
return false; } catch (error) { await logAsync("error", `Failed to process order: ${error.message}`); return false; }}Testing
tests/test.js:
const fs = require('fs');const { instantiate } = require('@heliosdb/wasm-sdk');
async function test() { const wasmModule = fs.readFileSync('./build/release.wasm'); const instance = await instantiate(wasmModule);
// Test getUserCount const count = instance.exports.getUserCount(); console.log('User count:', count);
// Test createUser const userId = instance.exports.createUser('John Doe', 'john@example.com'); console.log('Created user ID:', userId);
console.log('All tests passed!');}
test().catch(console.error);Python SDK
Setup
requirements.txt:
heliosdb-wasm-sdk>=1.0.0wasmtime>=15.0.0typing-extensions>=4.8.0Install:
pip install -r requirements.txtBasic Procedure
from typing import List, Dict, Optionalimport heliosdb
def get_user_count() -> int: """Get total number of users in the database.""" result = heliosdb.exec_sql("SELECT COUNT(*) FROM users") return result.rows[0][0]
def create_user(name: str, email: str) -> int: """Create a new user and return the ID.""" result = heliosdb.exec_sql( f"INSERT INTO users (name, email) VALUES ('{name}', '{email}') RETURNING id" ) user_id = result.rows[0][0] heliosdb.log_info(f"Created user {user_id}: {name}") return user_idType Hints
from typing import List, Dict, Optional, TypedDictfrom dataclasses import dataclassimport heliosdb
@dataclassclass User: id: int name: str email: str active: bool
class OrderSummary(TypedDict): order_id: int total: float discount: float final_total: float
def get_user(user_id: int) -> Optional[User]: """Get user by ID with type-safe return.""" result = heliosdb.exec_sql( f"SELECT id, name, email, active FROM users WHERE id = {user_id}" )
if not result.rows: return None
row = result.rows[0] return User( id=row[0], name=row[1], email=row[2], active=row[3] )
def calculate_order_summary(order_id: int) -> OrderSummary: """Calculate order summary with discounts.""" result = heliosdb.exec_sql( f"SELECT total, discount_rate FROM orders WHERE id = {order_id}" )
total = result.rows[0][0] discount_rate = result.rows[0][1] discount = total * discount_rate final_total = total - discount
return OrderSummary( order_id=order_id, total=total, discount=discount, final_total=final_total )Error Handling
import heliosdbfrom typing import Optional
class ProcedureError(Exception): """Base exception for procedure errors.""" pass
class InsufficientFundsError(ProcedureError): """Raised when account has insufficient funds.""" pass
def transfer_funds( from_account: int, to_account: int, amount: float) -> None: """Transfer funds between accounts with proper error handling.""" if amount <= 0: raise ValueError("Amount must be positive")
tx = None try: # Begin transaction tx = heliosdb.begin_transaction()
# Check balance result = heliosdb.exec_sql( f"SELECT balance FROM accounts WHERE id = {from_account}" )
if not result.rows: raise ProcedureError(f"Account {from_account} not found")
balance = result.rows[0][0] if balance < amount: raise InsufficientFundsError( f"Insufficient funds: has ${balance:.2f}, needs ${amount:.2f}" )
# Execute transfer heliosdb.exec_sql( f"UPDATE accounts SET balance = balance - {amount} WHERE id = {from_account}" ) heliosdb.exec_sql( f"UPDATE accounts SET balance = balance + {amount} WHERE id = {to_account}" )
# Commit transaction heliosdb.commit_transaction(tx) heliosdb.log_info(f"Transferred ${amount:.2f} from {from_account} to {to_account}")
except Exception as e: if tx: heliosdb.rollback_transaction(tx) heliosdb.log_error(f"Transfer failed: {str(e)}") raiseTesting
import unittestfrom procedure import get_user_count, create_user, transfer_funds, InsufficientFundsError
class TestProcedures(unittest.TestCase):
def test_get_user_count(self): """Test getting user count.""" count = get_user_count() self.assertIsInstance(count, int) self.assertGreaterEqual(count, 0)
def test_create_user(self): """Test creating a new user.""" user_id = create_user("Test User", "test@example.com") self.assertIsInstance(user_id, int) self.assertGreater(user_id, 0)
def test_transfer_insufficient_funds(self): """Test transfer with insufficient funds.""" with self.assertRaises(InsufficientFundsError): transfer_funds(1, 2, 1000000.0)
def test_transfer_negative_amount(self): """Test transfer with negative amount.""" with self.assertRaises(ValueError): transfer_funds(1, 2, -100.0)
if __name__ == '__main__': unittest.main()Go SDK
Setup
go.mod:
module my-procedure
go 1.21
require ( github.com/heliosdb/heliosdb-go-sdk v1.0.0)Basic Procedure
package main
import ( "fmt" "heliosdb")
//export getUserCountfunc getUserCount() int64 { result, err := heliosdb.ExecSQL("SELECT COUNT(*) FROM users") if err != nil { heliosdb.LogError(fmt.Sprintf("Failed to get user count: %v", err)) return 0 }
count, _ := result.GetInt64(0, 0) return count}
//export createUserfunc createUser(name, email string) int64 { query := fmt.Sprintf( "INSERT INTO users (name, email) VALUES ('%s', '%s') RETURNING id", name, email, )
result, err := heliosdb.ExecSQL(query) if err != nil { heliosdb.LogError(fmt.Sprintf("Failed to create user: %v", err)) return 0 }
userID, _ := result.GetInt64(0, 0) heliosdb.LogInfo(fmt.Sprintf("Created user %d: %s", userID, name))
return userID}
func main() { // Required for TinyGo}TinyGo Usage
package main
import ( "fmt" "heliosdb")
// User represents a database usertype User struct { ID int64 Name string Email string Active bool}
//export getUserfunc getUser(userID int64) *User { query := fmt.Sprintf( "SELECT id, name, email, active FROM users WHERE id = %d", userID, )
result, err := heliosdb.ExecSQL(query) if err != nil { heliosdb.LogError(fmt.Sprintf("Query failed: %v", err)) return nil }
if result.RowCount() == 0 { return nil }
user := &User{ ID: result.GetInt64(0, 0), Name: result.GetString(0, 1), Email: result.GetString(0, 2), Active: result.GetBool(0, 3), }
return user}
//export processOrderBatchfunc processOrderBatch(orderIDs []int64) int32 { successCount := int32(0)
for _, orderID := range orderIDs { if processOrder(orderID) { successCount++ } }
heliosdb.LogInfo(fmt.Sprintf("Processed %d/%d orders", successCount, len(orderIDs))) return successCount}
func processOrder(orderID int64) bool { query := fmt.Sprintf( "UPDATE orders SET status = 'processed', processed_at = NOW() WHERE id = %d", orderID, )
_, err := heliosdb.ExecSQL(query) return err == nil}
func main() {}Error Handling
package main
import ( "errors" "fmt" "heliosdb")
var ( ErrInsufficientFunds = errors.New("insufficient funds") ErrAccountNotFound = errors.New("account not found") ErrInvalidAmount = errors.New("invalid amount"))
//export transferFundsfunc transferFunds(fromAccount, toAccount int64, amount float64) error { // Validation if amount <= 0 { heliosdb.LogWarn("Transfer attempted with invalid amount") return ErrInvalidAmount }
// Begin transaction tx, err := heliosdb.BeginTransaction() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) }
// Check source account balance query := fmt.Sprintf("SELECT balance FROM accounts WHERE id = %d", fromAccount) result, err := heliosdb.ExecSQL(query) if err != nil { heliosdb.RollbackTransaction(tx) return err }
if result.RowCount() == 0 { heliosdb.RollbackTransaction(tx) return ErrAccountNotFound }
balance := result.GetFloat64(0, 0) if balance < amount { heliosdb.RollbackTransaction(tx) heliosdb.LogWarn(fmt.Sprintf("Insufficient funds: has $%.2f, needs $%.2f", balance, amount)) return ErrInsufficientFunds }
// Debit from source _, err = heliosdb.ExecSQL( fmt.Sprintf("UPDATE accounts SET balance = balance - %.2f WHERE id = %d", amount, fromAccount), ) if err != nil { heliosdb.RollbackTransaction(tx) return fmt.Errorf("failed to debit account: %w", err) }
// Credit to destination _, err = heliosdb.ExecSQL( fmt.Sprintf("UPDATE accounts SET balance = balance + %.2f WHERE id = %d", amount, toAccount), ) if err != nil { heliosdb.RollbackTransaction(tx) return fmt.Errorf("failed to credit account: %w", err) }
// Commit transaction if err := heliosdb.CommitTransaction(tx); err != nil { return fmt.Errorf("failed to commit: %w", err) }
heliosdb.LogInfo(fmt.Sprintf("Transferred $%.2f from %d to %d", amount, fromAccount, toAccount)) return nil}
func main() {}Testing
package main
import ( "testing")
func TestGetUserCount(t *testing.T) { count := getUserCount() if count < 0 { t.Errorf("Expected non-negative count, got %d", count) }}
func TestCreateUser(t *testing.T) { userID := createUser("Test User", "test@example.com") if userID <= 0 { t.Error("Expected positive user ID") }}
func TestTransferInvalidAmount(t *testing.T) { err := transferFunds(1, 2, -100.0) if err != ErrInvalidAmount { t.Errorf("Expected ErrInvalidAmount, got %v", err) }}
func TestTransferInsufficientFunds(t *testing.T) { err := transferFunds(1, 2, 1000000.0) if err != ErrInsufficientFunds { t.Errorf("Expected ErrInsufficientFunds, got %v", err) }}Build:
# Run testsgo test
# Build with TinyGotinygo build -o procedure.wasm -target wasi main.go
# Optimizetinygo build -o procedure.wasm -target wasi -opt=z -no-debug main.go
# Check sizels -lh procedure.wasmNavigation:
- Previous: Quick Start
- Next: Host Functions
- Index: WASM Procedures User Guide