diff --git a/.github/workflows/dotnet-publish.yml b/.github/workflows/dotnet-publish.yml
new file mode 100644
index 0000000000..929a984b80
--- /dev/null
+++ b/.github/workflows/dotnet-publish.yml
@@ -0,0 +1,121 @@
+name: Dotnet Publish
+
+on:
+ # Manually trigger the workflow
+ workflow_dispatch:
+
+env:
+ working-directory: bindings/dotnet
+
+jobs:
+ # Build native libraries for each platform
+ build-natives:
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-latest
+ make-target: build-rust-linux64
+ artifact-name: linux64
+ target: x86_64-unknown-linux-gnu
+
+ - os: macos-latest
+ make-target: build-rust-macos64
+ artifact-name: macos64
+ target: x86_64-apple-darwin
+
+ - os: macos-latest
+ make-target: build-rust-macosarm64
+ target: aarch64-apple-darwin
+ artifact-name: macosarm64
+
+ - os: windows-latest
+ make-target: build-rust-windows64
+ target: x86_64-pc-windows-msvc
+ artifact-name: windows64
+
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ defaults:
+ run:
+ working-directory: ${{ env.working-directory }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Add target
+ run: rustup target add ${{ matrix.target }}
+
+ - name: Build rust native library
+ run: make ${{ matrix.make-target }} RUST_RELEASE_OPT='--release'
+
+ - name: Upload rust native library
+ uses: actions/upload-artifact@v4
+ with:
+ name: native-${{ matrix.artifact-name }}
+ path: |
+ ${{ env.working-directory }}/rs_compiled/**/turso_dotnet.dll
+ ${{ env.working-directory }}/rs_compiled/**/libturso_dotnet.so
+ ${{ env.working-directory }}/rs_compiled/**/libturso_dotnet.dylib
+
+ retention-days: 1
+
+ # Create Turso.Raw and Turso nuget packages
+ publish:
+ needs: build-natives
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ defaults:
+ run:
+ working-directory: ${{ env.working-directory }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install dotnet sdk 9.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Download all native libraries
+ uses: actions/download-artifact@v4
+ with:
+ pattern: native-*
+ path: ${{ env.working-directory }}/rs_compiled
+ merge-multiple: true
+
+ - name: Build and pack
+ run:
+ make pack
+
+ # https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package
+ - name: Publish to nuget
+ if: false # TODO: Get nuget api key and publish packages
+ run: |
+ dotnet nuget push ${{ env.working-directory }}/src/Turso.Raw/bin/Release/Turso.Raw.*.nupkg --api-key ... --source https://api.nuget.org/v3/index.json
+
+ dotnet nuget push ${{ env.working-directory }}/src/Turso/bin/Release/Turso.*.nupkg --api-key ... --source https://api.nuget.org/v3/index.json
+
+ - name: Upload Turso.Raw to artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: Turso.Raw
+ path: ${{ env.working-directory }}/src/Turso.Raw/bin/Release/Turso.Raw.*.nupkg
+ retention-days: 7
+
+ - name: Upload Turso to artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: Turso
+ path: ${{ env.working-directory }}/src/Turso/bin/Release/Turso.*.nupkg
+ retention-days: 7
+
+
+
diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml
new file mode 100644
index 0000000000..9f8537d276
--- /dev/null
+++ b/.github/workflows/dotnet-test.yml
@@ -0,0 +1,38 @@
+name: Dotnet Tests
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - v*
+ pull_request:
+ branches:
+ - main
+
+env:
+ working-directory: bindings/dotnet
+
+jobs:
+ test:
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ timeout-minutes: 30
+
+ defaults:
+ run:
+ working-directory: ${{ env.working-directory }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Rust(stable)
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Install dotnet sdk 9.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Run tests
+ run: make test
diff --git a/Cargo.lock b/Cargo.lock
index 4583e45535..b4c58a7051 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4848,6 +4848,13 @@ dependencies = [
"turso_core",
]
+[[package]]
+name = "turso-dotnet"
+version = "0.3.0-pre.4"
+dependencies = [
+ "turso_core",
+]
+
[[package]]
name = "turso-java"
version = "0.3.0-pre.4"
diff --git a/Cargo.toml b/Cargo.toml
index 4460ca6021..52783d984c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ members = [
"bindings/javascript",
"bindings/javascript/sync",
"bindings/python",
+ "bindings/dotnet",
"bindings/rust",
"cli",
"core",
diff --git a/bindings/dotnet/.gitignore b/bindings/dotnet/.gitignore
new file mode 100644
index 0000000000..a3868f0786
--- /dev/null
+++ b/bindings/dotnet/.gitignore
@@ -0,0 +1,487 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
+
+src/Turso.Native/*
+rs_compiled/*
\ No newline at end of file
diff --git a/bindings/dotnet/Cargo.toml b/bindings/dotnet/Cargo.toml
new file mode 100644
index 0000000000..3fb2ee4a11
--- /dev/null
+++ b/bindings/dotnet/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "turso-dotnet"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+publish = false
+
+[lib]
+name = "turso_dotnet"
+crate-type = ["cdylib"]
+path = "rs_src/lib.rs"
+
+[dependencies]
+turso_core = { workspace = true }
diff --git a/bindings/dotnet/Makefile b/bindings/dotnet/Makefile
new file mode 100644
index 0000000000..5ba8ce1261
--- /dev/null
+++ b/bindings/dotnet/Makefile
@@ -0,0 +1,59 @@
+ifeq ($(OS),Windows_NT)
+ OS_TARGET = windows64
+else
+ UNAME_S := $(shell uname -s)
+ ifeq ($(UNAME_S),Linux)
+ OS_TARGET = linux64
+ endif
+ ifeq ($(UNAME_S),Darwin)
+ UNAME_P := $(shell uname -p)
+ ifeq ($(UNAME_P),x86_64)
+ OS_TARGET = macos64
+ else
+ OS_TARGET = macosarm64
+ endif
+ endif
+endif
+
+RUST_RELEASE_OPT ?=
+DOTNET_RELEASE_OPT ?=
+
+all:
+ echo "make [build|test|benchmark|build-production]"
+
+build-rust-windows64:
+ cargo build --target-dir ./rs_compiled --target x86_64-pc-windows-msvc $(RUST_RELEASE_OPT)
+
+build-rust-linux64:
+ cargo build --target-dir ./rs_compiled --target x86_64-unknown-linux-gnu $(RUST_RELEASE_OPT)
+
+build-rust-macos64:
+ cargo build --target-dir ./rs_compiled --target x86_64-apple-darwin $(RUST_RELEASE_OPT)
+
+build-rust-macosarm64:
+ cargo build --target-dir ./rs_compiled --target aarch64-apple-darwin $(RUST_RELEASE_OPT)
+
+build-rust:
+ echo "Building for $(OS_TARGET)"
+ $(MAKE) build-rust-$(OS_TARGET)
+
+build-dotnet:
+ dotnet build $(DOTNET_RELEASE_OPT) ./src/Turso/Turso.csproj
+
+build: build-rust build-dotnet
+
+build-production:
+ $(MAKE) build RUST_RELEASE_OPT='--release' DOTNET_RELEASE_OPT='-c Release'
+
+test: build
+ dotnet test ./src/Turso.Tests/Turso.Tests.csproj
+
+benchmark: build-production
+ dotnet run -c Release --project ./src/Benchmarks/Benchmarks.csproj
+
+pack:
+ dotnet build -c Release ./src/Turso.Raw/Turso.Raw.csproj
+ dotnet build -c Release ./src/Turso.Raw/Turso.Raw.csproj
+
+ dotnet pack -c Release ./src/Turso.Raw/Turso.Raw.csproj
+ dotnet pack -c Release ./src/Turso/Turso.csproj
diff --git a/bindings/dotnet/Readme.md b/bindings/dotnet/Readme.md
new file mode 100644
index 0000000000..4a6ecc7a24
--- /dev/null
+++ b/bindings/dotnet/Readme.md
@@ -0,0 +1,26 @@
+# Turso_dotnet
+
+Dotnet binding for turso database.
+
+## Getting Started
+
+```C#
+using Turso;
+
+using var connection = new TursoConnection("Data Source=:memory:");
+connection.Open();
+
+connection.ExecuteNonQuery("CREATE TABLE t(a, b)");
+var rowsAffected = connection.ExecuteNonQuery("INSERT INTO t(a, b) VALUES (1, 2), (3, 4)");
+Console.WriteLine($"RowsAffected: {rowsAffected}");
+
+using var command = connection.CreateCommand();
+command.CommandText = "SELECT * FROM t";
+using var reader = command.ExecuteReader();
+while (reader.Read())
+{
+ var a = reader.GetInt32(0);
+ var b = reader.GetInt32(1);
+ Console.WriteLine($"Value1: {a}, Value2: {b}");
+}
+```
\ No newline at end of file
diff --git a/bindings/dotnet/Turso.slnx b/bindings/dotnet/Turso.slnx
new file mode 100644
index 0000000000..c880622161
--- /dev/null
+++ b/bindings/dotnet/Turso.slnx
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bindings/dotnet/rs_src/lib.rs b/bindings/dotnet/rs_src/lib.rs
new file mode 100644
index 0000000000..570582ec5b
--- /dev/null
+++ b/bindings/dotnet/rs_src/lib.rs
@@ -0,0 +1,391 @@
+use std::borrow::Cow;
+use std::ffi::CStr;
+use std::num::NonZero;
+use std::os::raw::c_char;
+use std::ptr::null;
+use std::slice;
+use std::sync::Arc;
+use turso_core::types::Text;
+use turso_core::{self, Connection, Statement, StepResult, Value, IO};
+
+type Error = *const i8;
+
+#[repr(C)]
+pub struct Database {
+ io: Arc,
+ connection: Arc,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub enum ValueType {
+ Empty = 0,
+ Null = 1,
+ Integer = 2,
+ Float = 3,
+ Text = 4,
+ Blob = 5,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct Array {
+ ptr: *const u8,
+ len: usize,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub union TursoValueUnion {
+ int_val: i64,
+ real_val: f64,
+ text: Array,
+ blob: Array,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct TursoValue {
+ value_type: ValueType,
+ value: TursoValueUnion,
+}
+
+pub fn allocate(value: T) -> *const T {
+ Box::into_raw(Box::new(value))
+}
+
+pub fn allocate_string(str: &str) -> *const c_char {
+ std::ffi::CString::new(str).unwrap().into_raw()
+}
+
+pub fn to_vec(array: Array) -> Vec {
+ unsafe {
+ let slice = slice::from_raw_parts(array.ptr, array.len);
+ slice.to_vec()
+ }
+}
+
+pub fn to_value(value: TursoValue) -> Value {
+ match value.value_type {
+ ValueType::Empty => Value::Null,
+ ValueType::Null => Value::Null,
+ ValueType::Integer => Value::Integer(unsafe { value.value.int_val }),
+ ValueType::Float => Value::Float(unsafe { value.value.real_val }),
+ ValueType::Blob => Value::Blob(to_vec(unsafe { value.value.blob })),
+ ValueType::Text => Value::Text(Text {
+ value: to_vec(unsafe { value.value.text }),
+ subtype: turso_core::types::TextSubtype::Text,
+ }),
+ }
+}
+
+/// Opens a database at the specified path and returns a pointer to the database.
+/// If an error occurred, returns null and writes a pointer to a null-terminated string into `error_ptr`.
+///
+/// # Safety
+///
+/// - The returned database pointer must be freed with `db_close`.
+/// - Any error string written to `error_ptr` must be freed with `free_string`.
+/// - `path_ptr` must not be null and must point to a valid null-terminated UTF-8 string.
+/// - `error_ptr` must not be null and must point to a valid writable location.
+#[no_mangle]
+pub unsafe extern "C" fn db_open(
+ path_ptr: *const c_char,
+ error_ptr: *mut Error,
+) -> *const Database {
+ let path_cstr: &CStr = unsafe { CStr::from_ptr(path_ptr) };
+ let path_str = path_cstr.to_str();
+
+ let connection_result =
+ Connection::from_uri(path_str.unwrap(), true, false, false, false, false, false);
+ match connection_result {
+ Ok((io, val)) => allocate(Database {
+ io,
+ connection: val,
+ }),
+ Err(err) => {
+ unsafe {
+ *error_ptr =
+ allocate_string(format!("Error while opening database: {err}").as_str())
+ }
+ null()
+ }
+ }
+}
+
+/// Disposes the database pointer.
+///
+/// # Safety
+///
+/// - `db_ptr` must be a pointer allocated by `db_open`.
+/// - Call `db_close` only once per `db_ptr`.
+#[no_mangle]
+pub unsafe extern "C" fn db_close(db_ptr: *mut Database) {
+ let _ = unsafe { Box::from_raw(db_ptr) };
+}
+
+/// Frees a null-terminated string previously allocated by this library.
+///
+/// # Safety
+///
+/// - `string_ptr` must be a pointer returned by this library (e.g., error messages, column names).
+/// - Call `free_string` only once per `string_ptr`.
+#[no_mangle]
+pub unsafe extern "C" fn free_string(string_ptr: *mut c_char) {
+ unsafe { drop(std::ffi::CString::from_raw(string_ptr)) };
+}
+
+/// Prepares an SQL statement and returns a pointer to the prepared statement.
+/// If an error occurred, returns null and writes a pointer to a null-terminated string into `error_ptr`.
+///
+/// # Safety
+///
+/// - `db_ptr` must not be null.
+/// - `sql_ptr` must not be null and must point to a valid null-terminated UTF-8 string.
+/// - `error_ptr` must not be null and must point to a valid writable location.
+/// - When not null, the statement pointer must be freed with `free_statement` and any error string with `free_string`.
+#[no_mangle]
+pub unsafe extern "C" fn db_prepare_statement(
+ db_ptr: *mut Database,
+ sql_ptr: *const c_char,
+ error_ptr: *mut Error,
+) -> *const Statement {
+ let sql = unsafe { CStr::from_ptr(sql_ptr) }.to_str();
+ let db = unsafe { &mut (*db_ptr) };
+
+ let prepare_result = db.connection.prepare(sql.unwrap());
+ match prepare_result {
+ Ok(statement) => allocate(statement),
+ Err(e) => {
+ unsafe {
+ *error_ptr = allocate_string(format!("Unable to prepare statement: {e}").as_str())
+ }
+ null()
+ }
+ }
+}
+
+/// Binds a parameter to the statement by index.
+///
+/// # Safety
+///
+/// - `statement_ptr` must be a pointer returned by `db_prepare_statement`.
+/// - `index` must be >= 1.
+/// - `parameter_value` must be a valid pointer to a `TursoValue`.
+#[no_mangle]
+pub unsafe extern "C" fn bind_parameter(
+ statement_ptr: *mut Statement,
+ index: i32,
+ parameter_value: *const TursoValue,
+) {
+ let statement = unsafe { &mut (*statement_ptr) };
+ statement.bind_at(
+ NonZero::new(index.try_into().unwrap()).unwrap(),
+ to_value(*parameter_value),
+ );
+}
+
+/// Binds a parameter to the statement by name.
+///
+/// # Safety
+///
+/// - `statement_ptr` must be a pointer returned by `db_prepare_statement`.
+/// - `parameter_name` must not be null and must point to a valid null-terminated UTF-8 string.
+/// - `parameter_value` must be a valid pointer to a `TursoValue`.
+#[no_mangle]
+pub unsafe extern "C" fn bind_named_parameter(
+ statement_ptr: *mut Statement,
+ parameter_name: *const c_char,
+ parameter_value: *const TursoValue,
+) {
+ let statement = unsafe { &mut (*statement_ptr) };
+ let parameter_name = unsafe { CStr::from_ptr(parameter_name) }.to_str().unwrap();
+
+ for idx in 1..statement.parameters_count() + 1 {
+ let non_zero_idx = NonZero::new(idx).unwrap();
+ let param = statement.parameters().name(non_zero_idx);
+ let Some(name) = param else {
+ continue;
+ };
+ if parameter_name == name {
+ statement.bind_at(non_zero_idx, to_value(*parameter_value));
+ return;
+ }
+ }
+}
+
+/// Returns the number of rows changed by the statement.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_nchange(statement_ptr: *mut Statement) -> i64 {
+ let statement = unsafe { &mut (*statement_ptr) };
+ statement.n_change()
+}
+
+/// Executes the statement, advancing it by one step.
+/// If an error occurred, sets `error_ptr` to a pointer to a null-terminated string.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+/// - `error_ptr` must not be null and must point to a location that is valid for writing.
+/// - If set, the error string must be freed with `free_string`.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_execute_step(
+ statement_ptr: *mut Statement,
+ error_ptr: *mut Error,
+) -> bool {
+ let statement = unsafe { &mut (*statement_ptr) };
+
+ loop {
+ match statement.step() {
+ Ok(step_result) => match step_result {
+ StepResult::Row => {
+ return true;
+ }
+ StepResult::Done => {
+ return false;
+ }
+ StepResult::IO => {
+ if let Err(err) = statement.run_once() {
+ unsafe { *error_ptr = allocate_string(err.to_string().as_str()) };
+ return false;
+ }
+ continue;
+ }
+ StepResult::Interrupt => {
+ unsafe { *error_ptr = allocate_string("Interrupted") };
+ return false;
+ }
+ StepResult::Busy => {
+ unsafe { *error_ptr = allocate_string("Database is busy") };
+ return false;
+ }
+ },
+ Err(err) => {
+ unsafe { *error_ptr = allocate_string(err.to_string().as_str()) };
+ return false;
+ }
+ }
+ }
+}
+
+/// Frees the statement pointer.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+/// - Call `free_statement` only once per `statement_ptr`.
+#[no_mangle]
+pub unsafe extern "C" fn free_statement(statement_ptr: *mut Statement) {
+ let mut statement = unsafe { Box::from_raw(statement_ptr) };
+ statement.reset();
+}
+
+/// Gets the current value from the row at the specified column index.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+/// - `col_idx` must be >= 0.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_get_value(
+ statement_ptr: *mut Statement,
+ col_idx: i32,
+) -> TursoValue {
+ let statement = unsafe { &mut (*statement_ptr) };
+ if let Some(row) = statement.row() {
+ let value = match row.get_value(col_idx.try_into().unwrap()) {
+ Value::Null => TursoValue {
+ value_type: ValueType::Null,
+ value: TursoValueUnion { int_val: 0 },
+ },
+ Value::Integer(int_val) => TursoValue {
+ value_type: ValueType::Integer,
+ value: TursoValueUnion { int_val: *int_val },
+ },
+ Value::Float(float_value) => TursoValue {
+ value_type: ValueType::Float,
+ value: TursoValueUnion {
+ real_val: *float_value,
+ },
+ },
+ Value::Text(text) => {
+ let array = Array {
+ ptr: text.value.as_ptr(),
+ len: text.value.len(),
+ };
+ TursoValue {
+ value_type: ValueType::Text,
+ value: TursoValueUnion { text: array },
+ }
+ }
+ Value::Blob(blob) => {
+ let bytes = blob.as_ptr();
+ let array = Array {
+ ptr: bytes,
+ len: blob.len(),
+ };
+ TursoValue {
+ value_type: ValueType::Blob,
+ value: TursoValueUnion { blob: array },
+ }
+ }
+ };
+
+ return value;
+ }
+
+ TursoValue {
+ value_type: ValueType::Empty,
+ value: TursoValueUnion { int_val: 0 },
+ }
+}
+
+/// Gets the number of columns in the current statement.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_num_columns(statement_ptr: *mut Statement) -> i32 {
+ let statement = unsafe { &mut (*statement_ptr) };
+ statement.num_columns().try_into().unwrap()
+}
+
+/// Gets the column name for the specified index.
+/// The returned string is heap-allocated; free it with `free_string` when no longer needed.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+/// - `index` must be >= 0.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_column_name(
+ statement_ptr: *mut Statement,
+ index: i32,
+) -> *const i8 {
+ let statement = unsafe { &mut (*statement_ptr) };
+ let col_name = statement.get_column_name(index.try_into().unwrap());
+ match col_name {
+ Cow::Borrowed(value) => allocate_string(value),
+ Cow::Owned(value) => allocate_string(value.as_str()),
+ }
+}
+
+/// Checks whether the statement currently points to a row.
+///
+/// # Safety
+///
+/// - `statement_ptr` must not be null.
+#[no_mangle]
+pub unsafe extern "C" fn db_statement_has_rows(statement_ptr: *mut Statement) -> bool {
+ let statement = unsafe { &mut (*statement_ptr) };
+ match statement.row() {
+ Some(_val) => true,
+ None => false,
+ }
+}
diff --git a/bindings/dotnet/src/Benchmarks/Benchmarks.cs b/bindings/dotnet/src/Benchmarks/Benchmarks.cs
new file mode 100644
index 0000000000..0a8a01138d
--- /dev/null
+++ b/bindings/dotnet/src/Benchmarks/Benchmarks.cs
@@ -0,0 +1,66 @@
+using System.Data;
+using System.Data.SQLite;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+using Microsoft.Data.Sqlite;
+using SQLitePCL;
+using Turso;
+
+namespace Benchmarks;
+
+[MemoryDiagnoser]
+public class Benchmarks
+{
+ private SQLiteConnection _systemDataSqliteConnection;
+ private SqliteConnection _microsoftDataSqliteConnection;
+ private TursoConnection _tursoConnection;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _systemDataSqliteConnection = new SQLiteConnection("Data Source=:memory:");
+ _systemDataSqliteConnection.Open();
+
+ _microsoftDataSqliteConnection = new SqliteConnection("Data Source=:memory:");
+ _microsoftDataSqliteConnection.Open();
+
+ _tursoConnection = new TursoConnection("Data Source=:memory:");
+ _tursoConnection.Open();
+ CreateTable(_systemDataSqliteConnection);
+ CreateTable(_microsoftDataSqliteConnection);
+ CreateTable(_tursoConnection);
+ }
+
+ [Benchmark]
+ public void TursoSelect() => Select(_tursoConnection);
+
+ [Benchmark]
+ public void SystemSqliteSelect() => Select(_systemDataSqliteConnection);
+
+ [Benchmark]
+ public void MicrososftSqliteSelect() => Select(_microsoftDataSqliteConnection);
+
+ private void CreateTable(IDbConnection connection)
+ {
+ using var createTableCommand = connection.CreateCommand();
+ createTableCommand.CommandText = "CREATE TABLE t(a, b)";
+ createTableCommand.ExecuteNonQuery();
+
+ using var insertCommand = connection.CreateCommand();
+ insertCommand.CommandText = @"INSERT INTO t(a, b) VALUES (1, 2), (3, 4);";
+ insertCommand.ExecuteNonQuery();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Select(IDbConnection connection)
+ {
+ using var command = connection.CreateCommand();
+ command.CommandText = "SELECT * FROM t;";
+ using var reader = command.ExecuteReader();
+ var sum = 0;
+ while (reader.Read())
+ sum += reader.GetInt32(0);
+
+ GC.KeepAlive(sum);
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Benchmarks/Benchmarks.csproj b/bindings/dotnet/src/Benchmarks/Benchmarks.csproj
new file mode 100644
index 0000000000..2961ebd05d
--- /dev/null
+++ b/bindings/dotnet/src/Benchmarks/Benchmarks.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bindings/dotnet/src/Benchmarks/Program.cs b/bindings/dotnet/src/Benchmarks/Program.cs
new file mode 100644
index 0000000000..d63b5e6de5
--- /dev/null
+++ b/bindings/dotnet/src/Benchmarks/Program.cs
@@ -0,0 +1,4 @@
+
+using BenchmarkDotNet.Running;
+
+BenchmarkRunner.Run();
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Data/TursoNativeArray.cs b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeArray.cs
new file mode 100644
index 0000000000..2b7d451941
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeArray.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Turso.Raw.Data;
+
+[StructLayout(LayoutKind.Sequential)]
+internal struct TursoNativeArray
+{
+ public IntPtr Data;
+ public UInt64 Length;
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Data/TursoNativeRowValueUnion.cs b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeRowValueUnion.cs
new file mode 100644
index 0000000000..0edf110cc5
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeRowValueUnion.cs
@@ -0,0 +1,19 @@
+using System.Runtime.InteropServices;
+
+namespace Turso.Raw.Data;
+
+[StructLayout(LayoutKind.Explicit)]
+internal struct TursoNativeRowValueUnion
+{
+ [FieldOffset(0)]
+ public Int64 IntValue;
+
+ [FieldOffset(0)]
+ public Double RealValue;
+
+ [FieldOffset(0)]
+ public TursoNativeArray StringValue;
+
+ [FieldOffset(0)]
+ public TursoNativeArray BlobValue;
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Data/TursoNativeValue.cs b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeValue.cs
new file mode 100644
index 0000000000..66e57f6e71
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Data/TursoNativeValue.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+using Turso.Raw.Public.Value;
+
+namespace Turso.Raw.Data;
+
+[StructLayout(LayoutKind.Explicit)]
+internal ref struct TursoNativeValue
+{
+ [FieldOffset(0)]
+ public TursoValueType ValueType;
+
+ [FieldOffset(8)]
+ public TursoNativeRowValueUnion RowValueUnion;
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoDatabaseHandle.cs b/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoDatabaseHandle.cs
new file mode 100644
index 0000000000..f693a1f3ee
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoDatabaseHandle.cs
@@ -0,0 +1,27 @@
+using System.Runtime.InteropServices;
+
+namespace Turso.Raw.Public.Handles;
+
+public class TursoDatabaseHandle() : SafeHandle(IntPtr.Zero, true)
+{
+ protected override bool ReleaseHandle()
+ {
+ TursoInterop.CloseDatabase(handle);
+ return true;
+ }
+
+ public void ThrowIfInvalid()
+ {
+ if (IsInvalid)
+ throw new NullReferenceException("database is invalid");
+ }
+
+ public static TursoDatabaseHandle FromPtr(IntPtr ptr)
+ {
+ var handle = new TursoDatabaseHandle();
+ handle.SetHandle(ptr);
+ return handle;
+ }
+
+ public override bool IsInvalid => handle == IntPtr.Zero;
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoStatementHandle.cs b/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoStatementHandle.cs
new file mode 100644
index 0000000000..8eda790324
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/Handles/TursoStatementHandle.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+
+namespace Turso.Raw.Public.Handles;
+
+public class TursoStatementHandle() : SafeHandle(IntPtr.Zero, true)
+{
+ protected override bool ReleaseHandle()
+ {
+ TursoInterop.FreeStatement(handle);
+ return true;
+ }
+
+ public void ThrowIfInvalid()
+ {
+ if (IsInvalid)
+ throw new NullReferenceException("statement is invalid");
+ }
+
+ public static TursoStatementHandle FromPtr(IntPtr ptr)
+ {
+ var handle = new TursoStatementHandle();
+ handle.SetHandle(ptr);
+ return handle;
+ }
+
+ public override bool IsInvalid => handle == IntPtr.Zero;
+
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/TursoBindings.cs b/bindings/dotnet/src/Turso.Raw/Public/TursoBindings.cs
new file mode 100644
index 0000000000..a958927143
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/TursoBindings.cs
@@ -0,0 +1,190 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using Turso.Raw.Data;
+using Turso.Raw.Public.Handles;
+using Turso.Raw.Public.Value;
+
+namespace Turso.Raw.Public;
+
+public static class TursoBindings
+{
+ public static TursoDatabaseHandle OpenDatabase(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+
+ var dbPtr = TursoInterop.OpenDatabase(path, out var errorPtr);
+ if (errorPtr != IntPtr.Zero)
+ ThrowException(errorPtr);
+
+ return TursoDatabaseHandle.FromPtr(dbPtr);
+ }
+
+ public static TursoStatementHandle PrepareStatement(TursoDatabaseHandle db, string sql)
+ {
+ db.ThrowIfInvalid();
+ ArgumentNullException.ThrowIfNull(sql);
+
+ var statementPtr = TursoInterop.PrepareStatement(db, sql, out var errorPtr);
+ if (errorPtr != IntPtr.Zero)
+ ThrowException(errorPtr);
+
+ return TursoStatementHandle.FromPtr(statementPtr);
+ }
+
+ public static void BindParameter(TursoStatementHandle statement, int index, TursoValue parameter)
+ {
+ statement.ThrowIfInvalid();
+ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(index);
+
+ var nativeValue = FromValue(parameter, out var handle);
+ try
+ {
+ unsafe
+ {
+ var ptr = &nativeValue;
+ TursoInterop.BindParameter(statement, index, (IntPtr)ptr);
+ }
+ }
+ finally
+ {
+ if (handle.HasValue)
+ handle.Value.Free();
+ }
+ }
+
+ public static void BindNamedParameter(TursoStatementHandle statement, string name, TursoValue parameter)
+ {
+ statement.ThrowIfInvalid();
+ ArgumentNullException.ThrowIfNull(name);
+
+ var nativeValue = FromValue(parameter, out var handle);
+ try
+ {
+ unsafe
+ {
+ var ptr = &nativeValue;
+ TursoInterop.BindNamedParameter(statement, name, (IntPtr)ptr);
+ }
+ }
+ finally
+ {
+ if (handle.HasValue)
+ handle.Value.Free();
+ }
+ }
+
+ public static bool Read(TursoStatementHandle statement)
+ {
+ statement.ThrowIfInvalid();
+
+ var hasData = TursoInterop.StatementExecuteStep(statement, out var errorPtr);
+ if (errorPtr != IntPtr.Zero)
+ ThrowException(errorPtr);
+ return hasData;
+ }
+
+ public static TursoValue GetValue(TursoStatementHandle statement, int columnIndex)
+ {
+ statement.ThrowIfInvalid();
+ ArgumentOutOfRangeException.ThrowIfNegative(columnIndex);
+
+ var rowValue = TursoInterop.GetValueFromStatement(statement, columnIndex);
+ return rowValue.ValueType switch
+ {
+ TursoValueType.Empty => TursoValue.Empty(),
+ TursoValueType.Null => TursoValue.Null(),
+ TursoValueType.Integer => TursoValue.Int(rowValue.RowValueUnion.IntValue),
+ TursoValueType.Real => TursoValue.Real(rowValue.RowValueUnion.RealValue),
+ TursoValueType.Text => TursoValue.String(
+ Encoding.UTF8.GetString(ToArray(rowValue.RowValueUnion.StringValue))),
+ TursoValueType.Blob => TursoValue.Blob(ToArray(rowValue.RowValueUnion.BlobValue)),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public static string GetName(TursoStatementHandle statement, int ordinal)
+ {
+ statement.ThrowIfInvalid();
+ ArgumentOutOfRangeException.ThrowIfNegative(ordinal);
+
+ var cname = TursoInterop.StatementColumnName(statement, ordinal);
+ try
+ {
+ return Marshal.PtrToStringUTF8(cname) ?? "";
+ }
+ finally
+ {
+ TursoInterop.FreeString(cname);
+ }
+ }
+
+ public static int GetFieldCount(TursoStatementHandle statement)
+ {
+ statement.ThrowIfInvalid();
+
+ return TursoInterop.StatementNumColumns(statement);
+ }
+
+ public static int RowsAffected(TursoStatementHandle statement)
+ {
+ statement.ThrowIfInvalid();
+
+ return (int)TursoInterop.StatementRowsAffected(statement);
+ }
+
+
+ public static bool HasRows(TursoStatementHandle statement)
+ {
+ statement.ThrowIfInvalid();
+
+ return TursoInterop.StatementHasRows(statement);
+ }
+
+
+ private static TursoNativeValue FromValue(TursoValue value, out GCHandle? handle)
+ {
+ handle = null;
+ var union = new TursoNativeRowValueUnion();
+ if (value.ValueType == TursoValueType.Integer)
+ union.IntValue = value.IntValue;
+ if (value.ValueType == TursoValueType.Real)
+ union.RealValue = value.RealValue;
+ if (value.ValueType == TursoValueType.Text)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value.StringValue);
+ handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
+ union.StringValue = new TursoNativeArray
+ { Data = handle.Value.AddrOfPinnedObject(), Length = (ulong)bytes.Length };
+ }
+
+ if (value.ValueType == TursoValueType.Blob)
+ {
+ handle = GCHandle.Alloc(value.BlobValue, GCHandleType.Pinned);
+ union.BlobValue = new TursoNativeArray
+ { Data = handle.Value.AddrOfPinnedObject(), Length = (ulong)value.BlobValue.Length };
+ }
+
+ return new TursoNativeValue
+ {
+ ValueType = value.ValueType,
+ RowValueUnion = union,
+ };
+ }
+
+ private static byte[] ToArray(TursoNativeArray array)
+ {
+ unsafe
+ {
+ var data = new Span((void*)array.Data, (int)array.Length);
+ return data.ToArray();
+ }
+ }
+
+ private static void ThrowException(IntPtr errorPtr)
+ {
+ var errorMessage = Marshal.PtrToStringUTF8(errorPtr);
+ var exception = new TursoException(errorMessage ?? "Internal error");
+ TursoInterop.FreeString(errorPtr);
+ throw exception;
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/TursoException.cs b/bindings/dotnet/src/Turso.Raw/Public/TursoException.cs
new file mode 100644
index 0000000000..7b9ae8b734
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/TursoException.cs
@@ -0,0 +1,3 @@
+namespace Turso.Raw.Public;
+
+public class TursoException(string message) : Exception(message);
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValue.cs b/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValue.cs
new file mode 100644
index 0000000000..856612ba3d
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValue.cs
@@ -0,0 +1,17 @@
+namespace Turso.Raw.Public.Value;
+
+public struct TursoValue
+{
+ public TursoValueType ValueType;
+ public long IntValue;
+ public double RealValue;
+ public string StringValue;
+ public byte[] BlobValue;
+
+ public static TursoValue Empty() => new() { ValueType = TursoValueType.Empty };
+ public static TursoValue Null() => new() { ValueType = TursoValueType.Null };
+ public static TursoValue Int(Int64 value) => new() { ValueType = TursoValueType.Integer, IntValue = value };
+ public static TursoValue Real(Double value) => new() { ValueType = TursoValueType.Real, RealValue = value };
+ public static TursoValue String(string value) => new() { ValueType = TursoValueType.Text, StringValue = value };
+ public static TursoValue Blob(byte[] value) => new() { ValueType = TursoValueType.Blob, BlobValue = value };
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValueType.cs b/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValueType.cs
new file mode 100644
index 0000000000..84de4f503e
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Public/Value/TursoValueType.cs
@@ -0,0 +1,11 @@
+namespace Turso.Raw.Public.Value;
+
+public enum TursoValueType
+{
+ Empty = 0,
+ Null = 1,
+ Integer = 2,
+ Real = 3,
+ Text = 4,
+ Blob = 5,
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Raw/Turso.Raw.csproj b/bindings/dotnet/src/Turso.Raw/Turso.Raw.csproj
new file mode 100644
index 0000000000..09e103ace6
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/Turso.Raw.csproj
@@ -0,0 +1,74 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+
+ Turso.Raw
+ Dotnet bindings for turso database
+ 0.0.1
+
+
+
+
+ Always
+ runtimes\win-x64\native\turso_dotnet.dll
+
+
+ Always
+ runtimes\linux-x64\native\turso_dotnet.so
+
+
+ Always
+ runtimes\osx-x64\native\turso_dotnet.dylib
+
+
+ Always
+ runtimes\osx-arm\native\turso_dotnet.dylib
+
+
+
+
+ Always
+ runtimes\win-x64\native\turso_dotnet.dll
+ true
+ runtimes\win-x64\native\turso_dotnet.dll
+
+
+ Always
+ runtimes\linux-x64\native\turso_dotnet.so
+ true
+ runtimes\linux-x64\native\turso_dotnet.so
+
+
+ Always
+ runtimes\osx-x64\native\turso_dotnet.dylib
+ true
+ runtimes\osx-x64\native\turso_dotnet.dylib
+
+
+ Always
+ runtimes\osx-arm64\native\turso_dotnet.dylib
+ true
+ runtimes\osx-arm64\native\turso_dotnet.dylib
+
+
+
diff --git a/bindings/dotnet/src/Turso.Raw/TursoInterop.cs b/bindings/dotnet/src/Turso.Raw/TursoInterop.cs
new file mode 100644
index 0000000000..dbf09aedc1
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Raw/TursoInterop.cs
@@ -0,0 +1,52 @@
+using System.Runtime.InteropServices;
+using Turso.Raw.Data;
+using Turso.Raw.Public.Handles;
+
+namespace Turso.Raw;
+
+internal static class TursoInterop
+{
+ private const string DllName = "turso_dotnet";
+
+ [DllImport(DllName, EntryPoint = "db_open", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr OpenDatabase(string path, out IntPtr errorPtr);
+
+ [DllImport(DllName, EntryPoint = "db_close", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void CloseDatabase(IntPtr db);
+
+ [DllImport(DllName, EntryPoint = "free_string", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void FreeString(IntPtr stringPtr);
+
+ [DllImport(DllName, EntryPoint = "db_prepare_statement", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr PrepareStatement(TursoDatabaseHandle db, string sql, out IntPtr errorPtr);
+
+ [DllImport(DllName, EntryPoint = "free_statement", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void FreeStatement(IntPtr statement);
+
+ [DllImport(DllName, EntryPoint = "bind_parameter", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void BindParameter(TursoStatementHandle statement, int index, IntPtr tursoValue);
+
+ [DllImport(DllName, EntryPoint = "bind_named_parameter", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void BindNamedParameter(TursoStatementHandle statement, string parameterName, IntPtr tursoValue);
+
+ [DllImport(DllName, EntryPoint = "db_statement_execute_step", CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.I1)]
+ public static extern bool StatementExecuteStep(TursoStatementHandle statement, out IntPtr errorPtr);
+
+ [DllImport(DllName, EntryPoint = "db_statement_nchange", CallingConvention = CallingConvention.Cdecl)]
+ public static extern long StatementRowsAffected(TursoStatementHandle statement);
+
+ [DllImport(DllName, EntryPoint = "db_statement_get_value", CallingConvention = CallingConvention.Cdecl)]
+ public static extern TursoNativeValue GetValueFromStatement(TursoStatementHandle statement, int columnIndex);
+
+ [DllImport(DllName, EntryPoint = "db_statement_num_columns", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int StatementNumColumns(TursoStatementHandle statement);
+
+ [DllImport(DllName, EntryPoint = "db_statement_column_name", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr StatementColumnName(TursoStatementHandle statement, int index);
+
+ [DllImport(DllName, EntryPoint = "db_statement_has_rows", CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.I1)]
+ public static extern bool StatementHasRows(TursoStatementHandle statement);
+
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso.Tests/Turso.Tests.csproj b/bindings/dotnet/src/Turso.Tests/Turso.Tests.csproj
new file mode 100644
index 0000000000..cb830b9a76
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Tests/Turso.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ latest
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bindings/dotnet/src/Turso.Tests/TursoTests.cs b/bindings/dotnet/src/Turso.Tests/TursoTests.cs
new file mode 100644
index 0000000000..9e01c605e9
--- /dev/null
+++ b/bindings/dotnet/src/Turso.Tests/TursoTests.cs
@@ -0,0 +1,260 @@
+using System.Data.Common;
+using AwesomeAssertions;
+using Turso.Raw.Public;
+
+namespace Turso.Tests;
+
+public class TursoTests
+{
+ [Test]
+ public void TestSimpleQuery()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(1,2,1)");
+
+ using var reader = cmd.ExecuteReader();
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(1);
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(2);
+
+ reader.Read().Should().BeFalse();
+ }
+
+ [Test]
+ public void TestPrepareStatement()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(?,?,?)");
+ cmd.Parameters.Add(1);
+ cmd.Parameters.Add(2);
+ cmd.Parameters.Add(1);
+ cmd.Prepare();
+
+ using var reader = cmd.ExecuteReader();
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(1);
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(2);
+
+ reader.Read().Should().BeFalse();
+ }
+
+ [TestCase("stringValue", TestName = "TestStringValue")]
+ [TestCase(new byte[] { 1, 2, 3, 4, 5 }, TestName = "TestBlobValue")]
+ [TestCase(1, TestName = "TestIntValue")]
+ [TestCase(2.5, TestName = "TestRealValue")]
+ public void TestDifferentTypes(object typedValue)
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using (var create = new TursoCommand(connection, "CREATE TABLE t(v)"))
+ {
+ create.ExecuteNonQuery().Should().Be(0);
+ }
+
+ using (var insert = new TursoCommand(connection, "INSERT INTO t VALUES (?)"))
+ {
+ insert.Parameters.Add(typedValue);
+ insert.ExecuteNonQuery().Should().Be(1);
+ }
+
+ using var select = new TursoCommand(connection, "SELECT v FROM t");
+ using var reader = select.ExecuteReader();
+
+ reader.Read().Should().BeTrue();
+
+ switch (typedValue)
+ {
+ case string s:
+ reader.GetString(0).Should().Be(s);
+ break;
+ case byte[] bytes:
+ ((byte[])reader.GetValue(0)).SequenceEqual(bytes).Should().BeTrue();
+ break;
+ case int i:
+ reader.GetInt32(0).Should().Be(i);
+ break;
+ case double d:
+ reader.GetDouble(0).Should().Be(d);
+ break;
+ default:
+ throw new AssertionException($"Unsupported test type: {typedValue.GetType()}");
+ }
+
+ reader.Read().Should().BeFalse();
+ }
+
+
+ [Test]
+ public void TestBindNamedParameter()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var cmd = new TursoCommand(connection, "SELECT * FROM generate_series(?,:stop,?1)");
+ cmd.Parameters.Add(1);
+ cmd.Parameters.AddWithValue(":stop", 2);
+ cmd.Prepare();
+
+ using var reader = cmd.ExecuteReader();
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(1);
+
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(2);
+
+ reader.Read().Should().BeFalse();
+ }
+
+ [Test]
+ public void TestInsertData()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT)");
+ create.ExecuteNonQuery().Should().Be(0);
+
+ using var insert = new TursoCommand(connection, "INSERT INTO t(id, name) VALUES (1, 'alice'), (2, 'bob')");
+ insert.ExecuteNonQuery().Should().Be(2);
+
+ using var countCmd = new TursoCommand(connection, "SELECT COUNT(*) FROM t");
+ using var reader = countCmd.ExecuteReader();
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(2);
+ reader.Read().Should().BeFalse();
+ }
+
+ [Test]
+ public void TestFetchSpecificColumns()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT, age INTEGER)");
+ create.ExecuteNonQuery().Should().Be(0);
+
+ using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1,'alice',30),(2,'bob',40)");
+ insert.ExecuteNonQuery().Should().Be(2);
+
+ using var select = new TursoCommand(connection, "SELECT name, age FROM t WHERE id = 2");
+ using var reader = select.ExecuteReader();
+ reader.Read().Should().BeTrue();
+ reader.GetString(0).Should().Be("bob");
+ reader.GetInt32(1).Should().Be(40);
+ reader.Read().Should().BeFalse();
+ }
+
+ [Test]
+ public void TestQueryError()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var cmd = new TursoCommand(connection, "SELECT * FROM table_that_does_not_exist");
+ cmd.Invoking(x => x.ExecuteReader()).Should().Throw()
+ .WithMessage("Unable to prepare statement: Parse error: no such table: table_that_does_not_exist");
+ }
+
+ [Test]
+ [Ignore("https://github.com/tursodatabase/turso/pull/3591")]
+ public void TestCommitTransaction()
+ {
+ using var connection = new TursoConnection("Data Source=./turso.db");
+ connection.Open();
+
+ using var connection2 = new TursoConnection("Data Source=./turso.db");
+ connection2.Open();
+
+ connection.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS t(id INTEGER)");
+ connection.ExecuteNonQuery("DELETE FROM t");
+
+ using var tx = connection.BeginTransaction();
+
+ using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1),(2)");
+ insert.ExecuteNonQuery().Should().Be(2);
+
+ using var selectBefore = new TursoCommand(connection2, "SELECT COUNT(*) FROM t");
+ using (var readerBefore = selectBefore.ExecuteReader())
+ {
+ readerBefore.Read().Should().BeTrue();
+ readerBefore.GetInt32(0).Should().Be(0);
+ }
+
+ tx.Commit();
+
+ using var selectAfter = new TursoCommand(connection2, "SELECT COUNT(*) FROM t");
+ using var readerAfter = selectAfter.ExecuteReader();
+ readerAfter.Read().Should().BeTrue();
+ readerAfter.GetInt32(0).Should().Be(2);
+ }
+
+ [Test]
+ public void TestRollbackTransaction()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+
+ using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER)");
+ create.ExecuteNonQuery().Should().Be(0);
+
+ using var tx = connection.BeginTransaction();
+ using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1),(2)");
+ insert.ExecuteNonQuery().Should().Be(2);
+
+ using var select = new TursoCommand(connection, "SELECT COUNT(*) FROM t");
+ using var reader = select.ExecuteReader();
+ reader.Read().Should().BeTrue();
+ reader.GetInt32(0).Should().Be(2);
+
+ tx.Rollback();
+
+ using var select2 = new TursoCommand(connection, "SELECT COUNT(*) FROM t");
+ using var reader2 = select2.ExecuteReader();
+ reader2.Read().Should().BeTrue();
+ reader2.GetInt32(0).Should().Be(0);
+ }
+
+ [Test]
+ public void TestDataReaderEnumerable()
+ {
+ using var connection = new TursoConnection();
+ connection.Open();
+
+ using var create = new TursoCommand(connection, "CREATE TABLE t(id INTEGER, name TEXT, age INTEGER)");
+ create.ExecuteNonQuery().Should().Be(0);
+
+ using var insert = new TursoCommand(connection, "INSERT INTO t VALUES (1,'alice',30),(2,'bob',40),(3,'charlie',50)");
+ insert.ExecuteNonQuery().Should().Be(3);
+
+ using var select = new TursoCommand(connection, "SELECT id, name, age FROM t ORDER BY id");
+ using var reader = select.ExecuteReader();
+
+ var results = new List<(long id, string name, long age)>();
+
+ foreach (DbDataRecord record in reader)
+ {
+ var id = record.GetInt64(0);
+ var name = record.GetString(1);
+ var age = record.GetInt64(2);
+ results.Add((id, name, age));
+ }
+
+ results.Should().HaveCount(3);
+ results[0].Should().Be((1, "alice", 30));
+ results[1].Should().Be((2, "bob", 40));
+ results[2].Should().Be((3, "charlie", 50));
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/Turso.csproj b/bindings/dotnet/src/Turso/Turso.csproj
new file mode 100644
index 0000000000..4b840e4b4e
--- /dev/null
+++ b/bindings/dotnet/src/Turso/Turso.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Library
+ net9.0
+ enable
+ enable
+ Turso
+
+ Turso
+ ADO.NET provider for turso bindings
+ 0.0.1
+
+
+
+
+
+
+
+
+
+
diff --git a/bindings/dotnet/src/Turso/TursoCommand.cs b/bindings/dotnet/src/Turso/TursoCommand.cs
new file mode 100644
index 0000000000..829c4a8099
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoCommand.cs
@@ -0,0 +1,123 @@
+using System.Data;
+using System.Data.Common;
+using Turso.Raw.Public;
+using Turso.Raw.Public.Handles;
+
+namespace Turso;
+
+public class TursoCommand : DbCommand
+{
+ private TursoConnection _connection;
+ private TursoParameterCollection _parameterCollection = new();
+
+ private TursoTransaction? _transaction;
+ private TursoStatementHandle? _statement;
+
+ public TursoCommand(TursoConnection connection, TursoTransaction? transaction = null)
+ {
+ _connection = connection;
+ _transaction = transaction;
+ }
+
+ public TursoCommand(TursoConnection connection, string command)
+ {
+ _connection = connection;
+ _transaction = null;
+ CommandText = command;
+ }
+
+
+ public override string CommandText { get; set; } = "";
+ public override int CommandTimeout { get; set; } = 30;
+
+ public override CommandType CommandType
+ {
+ get => CommandType.Text;
+ set => throw new NotSupportedException();
+ }
+
+ public override bool DesignTimeVisible { get; set; }
+ public override UpdateRowSource UpdatedRowSource { get; set; }
+
+ protected override DbConnection? DbConnection
+ {
+ get => _connection;
+ set => _connection = value as TursoConnection ?? throw new ArgumentException();
+ }
+
+ protected override DbParameterCollection DbParameterCollection => _parameterCollection;
+
+ public new virtual TursoParameterCollection Parameters => _parameterCollection;
+
+
+ protected override DbTransaction? DbTransaction
+ {
+ get => _transaction;
+ set => _transaction = value as TursoTransaction ?? throw new ArgumentException();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _statement?.Dispose();
+ }
+
+ public override void Cancel()
+ {
+ }
+
+ public override int ExecuteNonQuery()
+ {
+ var reader = Execute();
+ reader.NextResult();
+ return reader.RecordsAffected;
+ }
+
+ public override object? ExecuteScalar()
+ {
+ using var reader = Execute();
+ return reader.Read()
+ ? reader.GetValue(0)
+ : null;
+ }
+
+ public override void Prepare()
+ {
+ _statement = TursoBindings.PrepareStatement(_connection.Turso, CommandText);
+ for (var i = 0; i < _parameterCollection.Count; i++)
+ {
+ var parameter = _parameterCollection[i] as TursoParameter;
+ if (parameter == null)
+ throw new ArgumentException("Parameter must be of type TursoParameter");
+
+ if (!string.IsNullOrEmpty(parameter.ParameterName))
+ {
+ TursoBindings.BindNamedParameter(_statement, parameter.ParameterName, parameter.ToValue());
+ }
+ else
+ {
+ TursoBindings.BindParameter(_statement, i + 1, parameter.ToValue());
+ }
+ }
+ }
+
+ protected override DbParameter CreateDbParameter()
+ {
+ return new TursoParameter();
+ }
+
+
+ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
+ {
+ return Execute(behavior);
+ }
+
+ private DbDataReader Execute(CommandBehavior behavior = CommandBehavior.Default)
+ {
+ if (_statement is null)
+ Prepare();
+
+ var reader = new TursoDataReader(this, _statement);
+ return reader;
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/TursoConnection.cs b/bindings/dotnet/src/Turso/TursoConnection.cs
new file mode 100644
index 0000000000..8675e5d6e2
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoConnection.cs
@@ -0,0 +1,89 @@
+using System.Data;
+using System.Data.Common;
+using Turso.Raw.Public;
+using Turso.Raw.Public.Handles;
+
+namespace Turso;
+
+public class TursoConnection : DbConnection
+{
+ private TursoDatabaseHandle? _turso = null;
+
+ private TursoConnectionOptions _connectionOptions;
+
+ public override string ConnectionString
+ {
+ get => _connectionOptions.GetConnectionString();
+ set => _connectionOptions = TursoConnectionOptions.Parse(value);
+ }
+
+ public override string Database => "main";
+
+ public override string DataSource => _connectionOptions["Data Source"] ?? "";
+
+ public override string ServerVersion => throw new NotImplementedException();
+
+ public override ConnectionState State => _turso is not null ? ConnectionState.Open : ConnectionState.Closed;
+
+ public TursoConnection() : this("")
+ {
+ }
+
+ public TursoConnection(string connectionString)
+ {
+ _connectionOptions = TursoConnectionOptions.Parse(connectionString);
+ }
+
+ public override void Open()
+ {
+ var filename = _connectionOptions["Data Source"] ?? ":memory:";
+ _turso = TursoBindings.OpenDatabase(filename);
+ }
+
+ public override void Close()
+ {
+ _turso?.Dispose();
+ _turso = null;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _turso?.Dispose();
+ }
+
+ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel)
+ {
+ if (_turso is null)
+ {
+ throw new Exception("Turso database is closed");
+ }
+
+ return new TursoTransaction(this, isolationLevel);
+ }
+
+ protected override DbCommand CreateDbCommand()
+ {
+ if (_turso is null)
+ {
+ throw new Exception("Turso database is closed");
+ }
+
+ return new TursoCommand(this);
+ }
+
+ public int ExecuteNonQuery(string sql)
+ {
+ using var command = CreateCommand();
+ command.CommandText = sql;
+
+ return command.ExecuteNonQuery();
+ }
+
+ public override void ChangeDatabase(string databaseName)
+ {
+ throw new NotSupportedException();
+ }
+
+ internal TursoDatabaseHandle Turso => _turso;
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/TursoConnectionOptions.cs b/bindings/dotnet/src/Turso/TursoConnectionOptions.cs
new file mode 100644
index 0000000000..8907b51d91
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoConnectionOptions.cs
@@ -0,0 +1,76 @@
+namespace Turso;
+
+public class TursoConnectionOptions
+{
+ private Dictionary _options = new();
+
+
+ private void AddOption(string keyword, string value)
+ {
+ if (!_valid_keywords.Contains(keyword))
+ {
+ throw new InvalidOperationException($"Unsupported keyword: {keyword}");
+ }
+
+ _options[keyword] = value;
+ }
+
+ public string GetConnectionString()
+ {
+ var parts = new List();
+ foreach (var keyword in _valid_keywords)
+ {
+ var option = GetOption(keyword);
+ if (option is not null)
+ {
+ parts.Add($"{keyword}={option}");
+ }
+ }
+
+ return string.Join(";", parts);
+ }
+
+ private string? GetOption(string keyword)
+ {
+ return _options.GetValueOrDefault(keyword);
+ }
+
+ public string? this[string keyword]
+ {
+ get => GetOption(keyword);
+ set => AddOption(keyword, value ?? "");
+ }
+
+ private readonly string[] _valid_keywords = [
+ "Data Source",
+ "Mode",
+ "Cache",
+ "Password",
+ "Foreign Keys",
+ "Recursive Triggers",
+ "Default Timeout",
+ "Pooling",
+ "Vfs"
+ ];
+
+ public static TursoConnectionOptions Parse(string connectionString)
+ {
+ var options = new TursoConnectionOptions();
+
+
+
+ foreach (var optionPart in connectionString.Split(";"))
+ {
+ var separatorIndex = optionPart.IndexOf('=');
+ if (separatorIndex == -1)
+ continue;
+
+ var keyword = optionPart.Substring(0, separatorIndex);
+ var value = optionPart.Substring(separatorIndex + 1);
+
+ options.AddOption(keyword, value);
+ }
+
+ return options;
+ }
+}
diff --git a/bindings/dotnet/src/Turso/TursoDataReader.cs b/bindings/dotnet/src/Turso/TursoDataReader.cs
new file mode 100644
index 0000000000..5de590ac8c
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoDataReader.cs
@@ -0,0 +1,248 @@
+using System.Collections;
+using System.ComponentModel;
+using System.Data.Common;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using Turso.Raw.Public;
+using Turso.Raw.Public.Handles;
+using Turso.Raw.Public.Value;
+
+namespace Turso;
+
+public class TursoDataReader : DbDataReader
+{
+ private readonly TursoCommand _command;
+ private readonly TursoStatementHandle _statement;
+
+ public TursoDataReader(TursoCommand command, TursoStatementHandle statement)
+ {
+ _command = command;
+ _statement = statement;
+ }
+
+ public override bool GetBoolean(int ordinal)
+ {
+ return TursoBindings.GetValue(_statement, ordinal).IntValue != 0;
+ }
+
+ public override byte GetByte(int ordinal)
+ {
+ return (byte)TursoBindings.GetValue(_statement, ordinal).IntValue;
+ }
+
+ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length)
+ {
+ return GetArray(ordinal, dataOffset, buffer, bufferOffset, length);
+ }
+
+ public override char GetChar(int ordinal)
+ {
+ var value = TursoBindings.GetValue(_statement, ordinal);
+ if (value.ValueType == TursoValueType.Text && value.StringValue.Length == 1)
+ {
+ return value.StringValue[0];
+ }
+
+ return (char)TursoBindings.GetValue(_statement, ordinal).IntValue;
+ }
+
+ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length)
+ {
+ return GetArray(ordinal, dataOffset, buffer, bufferOffset, length);
+ }
+
+ public override string GetDataTypeName(int ordinal)
+ {
+ var value = TursoBindings.GetValue(_statement, ordinal);
+ return GetTypeName(value.ValueType);
+ }
+
+ public override DateTime GetDateTime(int ordinal)
+ {
+ var value = TursoBindings.GetValue(_statement, ordinal);
+ switch (value.ValueType)
+ {
+ case TursoValueType.Text:
+ return DateTime.Parse(GetString(ordinal), CultureInfo.InvariantCulture);
+ default:
+ return DateTime.MinValue;
+ }
+ }
+
+ public override decimal GetDecimal(int ordinal)
+ {
+ return (decimal)TursoBindings.GetValue(_statement, ordinal).RealValue;
+ }
+
+ public override double GetDouble(int ordinal)
+ {
+ return TursoBindings.GetValue(_statement, ordinal).RealValue;
+ }
+
+ public override Type GetFieldType(int ordinal)
+ {
+ var value = TursoBindings.GetValue(_statement, ordinal);
+ return value.ValueType switch
+ {
+ TursoValueType.Integer => typeof(long),
+ TursoValueType.Real => typeof(double),
+ TursoValueType.Text => typeof(string),
+ TursoValueType.Blob => typeof(byte[]),
+ _ => typeof(object)
+ };
+ }
+
+ public override float GetFloat(int ordinal)
+ {
+ return (float)TursoBindings.GetValue(_statement, ordinal).RealValue;
+ }
+
+ public override Guid GetGuid(int ordinal)
+ {
+ return Guid.Parse(TursoBindings.GetValue(_statement, ordinal).StringValue);
+ }
+
+ public override short GetInt16(int ordinal)
+ {
+ return (short)TursoBindings.GetValue(_statement, ordinal).IntValue;
+ }
+
+ public override int GetInt32(int ordinal)
+ {
+ return (int)TursoBindings.GetValue(_statement, ordinal).IntValue;
+ }
+
+ public override long GetInt64(int ordinal)
+ {
+ return TursoBindings.GetValue(_statement, ordinal).IntValue;
+ }
+
+ public override string GetName(int ordinal)
+ {
+ return TursoBindings.GetName(_statement, ordinal);
+ }
+
+ public override int GetOrdinal(string name)
+ {
+ var fields = TursoBindings.GetFieldCount(_statement);
+ for (var i = 0; i < fields; i++)
+ {
+ var columnName = TursoBindings.GetName(_statement, i);
+ if (columnName == name)
+ return i;
+ }
+
+ throw new IndexOutOfRangeException($"column {name} not found");
+ }
+
+ public override string GetString(int ordinal)
+ {
+ return TursoBindings.GetValue(_statement, ordinal).StringValue;
+ }
+
+ public override object? GetValue(int ordinal)
+ {
+ var value = TursoBindings.GetValue(_statement, ordinal);
+ return value.ValueType switch
+ {
+ TursoValueType.Null or TursoValueType.Empty => null,
+ TursoValueType.Integer => value.IntValue,
+ TursoValueType.Real => value.RealValue,
+ TursoValueType.Text => value.StringValue,
+ TursoValueType.Blob => value.BlobValue,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ public override int GetValues(object[] values)
+ {
+ var i = 0;
+ for (; i < FieldCount; i++)
+ {
+ values[i] = GetValue(i)!;
+ }
+
+ return i;
+ }
+
+ public override bool IsDBNull(int ordinal)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override int FieldCount => TursoBindings.GetFieldCount(_statement);
+
+ public override object this[int ordinal] => GetValue(ordinal)!;
+
+ public override object this[string name]
+ {
+ get
+ {
+ var ordinal = GetOrdinal(name);
+ return GetValue(ordinal)!;
+ }
+ }
+
+ public override int RecordsAffected => TursoBindings.RowsAffected(_statement);
+ public override bool HasRows => TursoBindings.HasRows(_statement);
+ public override bool IsClosed => _statement.IsInvalid;
+
+ public override bool NextResult()
+ {
+ while (TursoBindings.Read(_statement)) ;
+ return true;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _command.Dispose();
+ }
+
+ public override bool Read()
+ {
+ return TursoBindings.Read(_statement);
+ }
+
+ public override int Depth => 0;
+
+ public override IEnumerator GetEnumerator()
+ {
+ return new DbEnumerator(this, closeReader: false);
+ }
+
+ private long GetArray(int ordinal, long dataOffset, T[]? buffer, int bufferOffset, int length)
+ where T : struct
+ {
+ var bytes = TursoBindings.GetValue(_statement, ordinal).BlobValue;
+ if (buffer is null)
+ {
+ return Math.Min(bytes.Length - dataOffset, length);
+ }
+
+ var position = 0;
+ for (; position < length; position++)
+ {
+ if (bufferOffset + position >= buffer.Length || position + dataOffset >= bytes.Length)
+ break;
+
+ buffer[bufferOffset + position] = Unsafe.As(ref bytes[position + dataOffset]);
+ }
+
+ return position;
+ }
+
+ private static string GetTypeName(TursoValueType valueType)
+ {
+ return valueType switch
+ {
+ TursoValueType.Empty => "",
+ TursoValueType.Null => "NULL",
+ TursoValueType.Integer => "INTEGER",
+ TursoValueType.Real => "REAL",
+ TursoValueType.Text => "TEXT",
+ TursoValueType.Blob => "BLOB",
+ _ => throw new InvalidEnumArgumentException(nameof(valueType))
+ };
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/TursoParameter.cs b/bindings/dotnet/src/Turso/TursoParameter.cs
new file mode 100644
index 0000000000..528b97443a
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoParameter.cs
@@ -0,0 +1,113 @@
+using System.Data;
+using System.Data.Common;
+using Turso.Raw.Public.Value;
+
+namespace Turso;
+
+public class TursoParameter : DbParameter
+{
+ private static readonly Dictionary TursoTypeMapping =
+ new()
+ {
+ { typeof(bool), TursoValueType.Integer },
+ { typeof(byte), TursoValueType.Integer },
+ { typeof(byte[]), TursoValueType.Blob },
+ { typeof(char), TursoValueType.Text },
+ { typeof(DateTime), TursoValueType.Text },
+ { typeof(DateTimeOffset), TursoValueType.Text },
+ { typeof(DateOnly), TursoValueType.Text },
+ { typeof(TimeOnly), TursoValueType.Text },
+ { typeof(DBNull), TursoValueType.Null },
+ { typeof(decimal), TursoValueType.Text },
+ { typeof(double), TursoValueType.Real },
+ { typeof(float), TursoValueType.Real },
+ { typeof(Guid), TursoValueType.Text },
+ { typeof(int), TursoValueType.Integer },
+ { typeof(long), TursoValueType.Integer },
+ { typeof(sbyte), TursoValueType.Integer },
+ { typeof(short), TursoValueType.Integer },
+ { typeof(string), TursoValueType.Text },
+ { typeof(TimeSpan), TursoValueType.Text },
+ { typeof(uint), TursoValueType.Integer },
+ { typeof(ulong), TursoValueType.Integer },
+ { typeof(ushort), TursoValueType.Integer }
+ };
+
+ public TursoParameter()
+ {
+ }
+
+ public TursoParameter(object value)
+ {
+ Value = value;
+ }
+
+ public TursoParameter(string parameterName, object value)
+ {
+ ParameterName = parameterName;
+ Value = value;
+ }
+
+ public TursoParameter(string parameterName, DbType dbType, object value)
+ {
+ ParameterName = parameterName;
+ DbType = dbType;
+ Value = value;
+ }
+
+ public override void ResetDbType()
+ {
+ DbType = DbType.String;
+ }
+
+ public override DbType DbType { get; set; } = DbType.String;
+
+ public override ParameterDirection Direction
+ {
+ get => ParameterDirection.Input;
+ set => throw new NotImplementedException();
+ }
+ public override bool IsNullable { get; set; }
+ public override string? ParameterName { get; set; }
+ public override string? SourceColumn { get; set; }
+ public override object? Value { get; set; }
+ public override bool SourceColumnNullMapping { get; set; }
+
+ public TursoValue ToValue()
+ {
+ if (Value is null)
+ return new TursoValue { ValueType = TursoValueType.Null };
+
+ var valueType = Value.GetType();
+ if (!TursoTypeMapping.TryGetValue(valueType, out var tursoValueType))
+ {
+ throw new ArgumentException($"Parameter type {valueType} is not supported");
+ }
+
+ return GetTursoValue(Value, tursoValueType);
+ }
+
+ public override int Size
+ {
+ get => Value is string s
+ ? s.Length
+ : Value is byte[] bytes
+ ? bytes.Length
+ : 0;
+ set => throw new NotImplementedException();
+ }
+
+ private TursoValue GetTursoValue(object value, TursoValueType tursoValueType)
+ {
+ return tursoValueType switch
+ {
+ TursoValueType.Empty => new TursoValue() { ValueType = TursoValueType.Empty },
+ TursoValueType.Null => new TursoValue() { ValueType = TursoValueType.Null },
+ TursoValueType.Integer => new TursoValue() { ValueType = TursoValueType.Integer, IntValue = Convert.ToInt64(value) },
+ TursoValueType.Real => new TursoValue() { ValueType = TursoValueType.Real, RealValue = Convert.ToDouble(value) },
+ TursoValueType.Text => new TursoValue() { ValueType = TursoValueType.Text, StringValue = value.ToString()! },
+ TursoValueType.Blob => new TursoValue() { ValueType = TursoValueType.Blob, BlobValue = (byte[])value },
+ _ => throw new ArgumentOutOfRangeException(nameof(tursoValueType), tursoValueType, null)
+ };
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/TursoParameterCollection.cs b/bindings/dotnet/src/Turso/TursoParameterCollection.cs
new file mode 100644
index 0000000000..e18b509979
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoParameterCollection.cs
@@ -0,0 +1,122 @@
+using System.Collections;
+using System.Data.Common;
+
+namespace Turso;
+
+public class TursoParameterCollection : DbParameterCollection
+{
+ private readonly List _parameters = new();
+ public override int Count => _parameters.Count;
+
+ public override object SyncRoot => throw new NotImplementedException();
+
+ public override int Add(object value)
+ {
+ _parameters.Add(value as TursoParameter ?? new TursoParameter(value));
+ return _parameters.Count - 1;
+ }
+
+ public int AddWithValue(string parameterName, object value)
+ {
+ _parameters.Add(new TursoParameter(parameterName, value));
+ return _parameters.Count - 1;
+ }
+
+ public override void AddRange(Array values)
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ var value = values.GetValue(i)!;
+ var parameter = value as TursoParameter ?? new TursoParameter(value);
+ _parameters.Add(parameter);
+ }
+ }
+
+ public override void Clear()
+ {
+ _parameters.Clear();
+ }
+
+ public override bool Contains(object value)
+ {
+ return _parameters.Any(p => value is TursoParameter ? p == value : p.Value == value);
+ }
+
+ public override bool Contains(string value)
+ {
+ return _parameters.Any(p => p.ParameterName == value);
+ }
+
+ public override void CopyTo(Array array, int index)
+ {
+ _parameters.CopyTo((TursoParameter[])array, index);
+ }
+
+ public override IEnumerator GetEnumerator()
+ {
+ return _parameters.GetEnumerator();
+ }
+
+ public override int IndexOf(object value)
+ {
+ return _parameters.FindIndex(p => value is TursoParameter ? p == value : p.Value == value);
+ }
+
+ public override int IndexOf(string parameterName)
+ {
+ return _parameters.FindIndex(p => p.ParameterName == parameterName);
+ }
+
+ public override void Insert(int index, object value)
+ {
+ _parameters.Insert(index, value as TursoParameter ?? new TursoParameter(value));
+ }
+
+ public override void Remove(object value)
+ {
+ var index = IndexOf(value);
+ if (index == -1)
+ throw new ArgumentException($"Parameter {value} not found");
+ _parameters.RemoveAt(index);
+ }
+
+ public override void RemoveAt(int index)
+ {
+ _parameters.RemoveAt(index);
+ }
+
+ public override void RemoveAt(string parameterName)
+ {
+ var index = IndexOf(parameterName);
+ if (index == -1)
+ throw new ArgumentException($"Parameter {parameterName} not found");
+
+ _parameters.RemoveAt(index);
+ }
+
+ protected override DbParameter GetParameter(int index)
+ {
+ return _parameters[index];
+ }
+
+ protected override DbParameter GetParameter(string parameterName)
+ {
+ return _parameters.Find(p => p.ParameterName == parameterName)
+ ?? throw new ArgumentException($"Parameter {parameterName} not found");
+ }
+
+ protected override void SetParameter(int index, DbParameter value)
+ {
+ _parameters[index] = value as TursoParameter
+ ?? throw new ArgumentException($"Parameter {value} is not a TursoParameter");
+ }
+
+ protected override void SetParameter(string parameterName, DbParameter value)
+ {
+ var index = IndexOf(parameterName);
+ if (index == -1)
+ throw new ArgumentException($"Parameter {parameterName} not found");
+ _parameters[index] = value as TursoParameter
+ ?? throw new ArgumentException($"Parameter {value} is not a TursoParameter");
+ }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/Turso/TursoTransaction.cs b/bindings/dotnet/src/Turso/TursoTransaction.cs
new file mode 100644
index 0000000000..a54829fa9b
--- /dev/null
+++ b/bindings/dotnet/src/Turso/TursoTransaction.cs
@@ -0,0 +1,59 @@
+using System.Data.Common;
+using IsolationLevel = System.Data.IsolationLevel;
+
+namespace Turso;
+
+public class TursoTransaction : DbTransaction
+{
+ private TursoConnection _connection;
+ private IsolationLevel _isolationLevel;
+ private bool _completed = false;
+
+ public TursoTransaction(TursoConnection connection, IsolationLevel isolationLevel)
+ {
+ _connection = connection;
+ _isolationLevel = isolationLevel;
+
+ if (isolationLevel == IsolationLevel.ReadUncommitted)
+ connection.ExecuteNonQuery("PRAGMA read_uncommitted = 1;");
+
+ connection.ExecuteNonQuery("BEGIN");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_completed)
+ {
+ Rollback();
+ }
+ }
+
+ public override IsolationLevel IsolationLevel => _isolationLevel;
+
+ protected override DbConnection? DbConnection => _connection;
+
+ public override void Commit()
+ {
+ _connection.ExecuteNonQuery("COMMIT;");
+ CompleteTransaction();
+ }
+
+ public override void Rollback()
+ {
+ try
+ {
+ _connection.ExecuteNonQuery("ROLLBACK;");
+ }
+ finally
+ {
+ CompleteTransaction();
+ }
+ }
+
+ private void CompleteTransaction()
+ {
+ if (_isolationLevel == IsolationLevel.ReadUncommitted)
+ _connection.ExecuteNonQuery("PRAGMA read_uncommitted = 0;");
+ _completed = true;
+ }
+}
\ No newline at end of file