Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion bindings/go/bindings_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const (
type TursoSyncDatabaseConfig struct {
// Path to the main database file (auxiliary files will derive names from this path)
Path string
// optional remote url (libsql://..., https://... or http://...)
// this URL will be saved in the database metadata file in order to be able to reuse it if later client will be constructed without explicit remote url
RemoteUrl string
// Arbitrary client name used as a prefix for unique client id
ClientName string
// Long poll timeout for pull method in milliseconds
Expand Down Expand Up @@ -79,6 +82,7 @@ type TursoSyncStats struct {

// HTTP request description used by IO layer.
type TursoSyncIoHttpRequest struct {
Url string
Method string
Path string
Body []byte
Expand Down Expand Up @@ -106,6 +110,7 @@ type TursoSyncIoFullWriteRequest struct {

type turso_sync_database_config_t struct {
path uintptr // const char*
remote_url uintptr // const char*
client_name uintptr // const char*
long_poll_timeout_ms int32
bootstrap_if_empty bool
Expand All @@ -117,6 +122,7 @@ type turso_sync_database_config_t struct {
}

type turso_sync_io_http_request_t struct {
url turso_slice_ref_t
method turso_slice_ref_t
path turso_slice_ref_t
body turso_slice_ref_t
Expand Down Expand Up @@ -387,8 +393,9 @@ func turso_sync_database_new(dbConfig TursoDatabaseConfig, syncConfig TursoSyncD

// Build C sync config
var csync turso_sync_database_config_t
var syncPathBytes, clientNameBytes, queryBytes []byte
var syncPathBytes, remoteUrlBytes, clientNameBytes, queryBytes []byte
syncPathBytes, csync.path = makeCStringBytes(syncConfig.Path)
remoteUrlBytes, csync.remote_url = makeCStringBytes(syncConfig.RemoteUrl)
clientNameBytes, csync.client_name = makeCStringBytes(syncConfig.ClientName)
csync.long_poll_timeout_ms = int32(syncConfig.LongPollTimeoutMs)
csync.bootstrap_if_empty = syncConfig.BootstrapIfEmpty
Expand All @@ -406,6 +413,7 @@ func turso_sync_database_new(dbConfig TursoDatabaseConfig, syncConfig TursoSyncD

// Keep Go memory alive during C call
runtime.KeepAlive(pathBytes)
runtime.KeepAlive(remoteUrlBytes)
runtime.KeepAlive(expBytes)
runtime.KeepAlive(syncPathBytes)
runtime.KeepAlive(clientNameBytes)
Expand Down Expand Up @@ -637,6 +645,7 @@ func turso_sync_database_io_request_http(self TursoSyncIoItem) (TursoSyncIoHttpR
return TursoSyncIoHttpRequest{}, statusToError(TursoStatusCode(status), "")
}
return TursoSyncIoHttpRequest{
Url: sliceRefToStringCopy(creq.url),
Method: sliceRefToStringCopy(creq.method),
Path: sliceRefToStringCopy(creq.path),
Body: sliceRefToBytesCopy(creq.body),
Expand Down
4 changes: 1 addition & 3 deletions bindings/go/driver_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@ func NewTursoSyncDb(ctx context.Context, config TursoSyncDbConfig) (*TursoSyncDb
if strings.TrimSpace(config.Path) == "" {
return nil, errors.New("turso: empty Path in TursoSyncDbConfig")
}
if strings.TrimSpace(config.RemoteUrl) == "" {
return nil, errors.New("turso: empty RemoteUrl in TursoSyncDbConfig")
}
clientName := config.ClientName
if clientName == "" {
clientName = "turso-sync-go"
Expand All @@ -119,6 +116,7 @@ func NewTursoSyncDb(ctx context.Context, config TursoSyncDbConfig) (*TursoSyncDb
}
syncCfg := TursoSyncDatabaseConfig{
Path: config.Path,
RemoteUrl: config.RemoteUrl,
ClientName: clientName,
LongPollTimeoutMs: config.LongPollTimeoutMs,
BootstrapIfEmpty: bootstrap,
Expand Down
59 changes: 59 additions & 0 deletions bindings/go/driver_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"math/rand"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -192,6 +193,64 @@ func TestSyncBootstrap(t *testing.T) {
require.Equal(t, values, []string{"hello", "turso", "sync-go"})
}

func TestSyncConfigPersistence(t *testing.T) {
server, err := NewTursoServer()
require.Nil(t, err)
t.Cleanup(func() { server.Close() })

_, err = server.DbSql("CREATE TABLE t(x)")
require.Nil(t, err)
_, err = server.DbSql("INSERT INTO t VALUES (42)")
require.Nil(t, err)

dir := t.TempDir()
local := path.Join(dir, "local.db")

db1, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{
Path: local,
ClientName: "turso-sync-go",
RemoteUrl: server.DbUrl,
})
require.Nil(t, err)
conn, err := db1.Connect(context.Background())
require.Nil(t, err)
rows, err := conn.QueryContext(context.Background(), "SELECT * FROM t")
require.Nil(t, err)
values := make([]int64, 0)
for rows.Next() {
var value int64
require.Nil(t, rows.Scan(&value))
values = append(values, value)
}
require.Equal(t, values, []int64{42})
rows.Close()
conn.Close()

_, err = server.DbSql("INSERT INTO t VALUES (41)")
require.Nil(t, err)

db2, err := NewTursoSyncDb(context.Background(), TursoSyncDbConfig{
Path: local,
RemoteUrl: server.DbUrl,
})
require.Nil(t, err)

_, err = db2.Pull(context.Background())
require.Nil(t, err)

conn, err = db2.Connect(context.Background())
require.Nil(t, err)
rows, err = conn.QueryContext(context.Background(), "SELECT * FROM t")
require.Nil(t, err)
values = make([]int64, 0)
for rows.Next() {
var value int64
require.Nil(t, rows.Scan(&value))
values = append(values, value)
}
require.Equal(t, values, []int64{42, 41})
}

func TestSyncBootstrapPersistent(t *testing.T) {
server, err := NewTursoServer()
require.Nil(t, err)
Expand Down
2 changes: 1 addition & 1 deletion bindings/javascript/sync/packages/common/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ async function process(opts: RunOpts, io: ProtocolIo, request: any) {
const requestType = request.request();
const completion = request.completion();
if (requestType.type == 'Http') {
let url: string | null = null;
let url: string | null = requestType.url;
if (typeof opts.url == "function") {
url = opts.url();
} else {
Expand Down
2 changes: 1 addition & 1 deletion bindings/javascript/sync/packages/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface DatabaseOpts {
* optional url of the remote database (e.g. libsql://db-org.turso.io)
* (if omitted - local-only database will be created)
*
* you can also promide function which will return URL or null
* you can also provide function which will return URL or null
* in this case local database will be created and sync will be "switched-on" whenever the url will return non-empty value
* note, that all other parameters (like encryption) must be set in advance in order for the "deferred" sync to work properly
*/
Expand Down
3 changes: 3 additions & 0 deletions bindings/javascript/sync/src/js_protocol_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
#[napi]
pub enum JsProtocolRequest {
Http {
url: Option<String>,
method: String,
path: String,
body: Option<Vec<u8>>,
Expand Down Expand Up @@ -189,12 +190,14 @@ impl SyncEngineIo for JsProtocolIo {

fn http(
&self,
url: Option<&str>,
method: &str,
path: &str,
body: Option<Vec<u8>>,
headers: &[(&str, &str)],
) -> turso_sync_engine::Result<JsDataCompletion> {
Ok(self.add_request(JsProtocolRequest::Http {
url: url.map(|x| x.to_string()),
method: method.to_string(),
path: path.to_string(),
body,
Expand Down
10 changes: 7 additions & 3 deletions bindings/javascript/sync/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub struct JsPartialSyncOpts {
#[napi(object, object_to_js = false)]
pub struct SyncEngineOpts {
pub path: String,
pub remote_url: Option<String>,
pub client_name: Option<String>,
pub wal_pull_batch_size: Option<u32>,
pub long_poll_timeout_ms: Option<u32>,
Expand All @@ -143,6 +144,7 @@ pub struct SyncEngineOpts {

struct SyncEngineOptsFilled {
pub path: String,
pub remote_url: Option<String>,
pub client_name: String,
pub wal_pull_batch_size: u32,
pub long_poll_timeout: Option<std::time::Duration>,
Expand Down Expand Up @@ -238,6 +240,7 @@ impl SyncEngine {
)?));
let opts_filled = SyncEngineOptsFilled {
path: opts.path,
remote_url: opts.remote_url,
client_name: opts.client_name.unwrap_or("turso-sync-js".to_string()),
wal_pull_batch_size: opts.wal_pull_batch_size.unwrap_or(100),
long_poll_timeout: opts
Expand Down Expand Up @@ -268,14 +271,14 @@ impl SyncEngine {
partial_sync_opts: match opts.partial_sync_opts {
Some(partial_sync_opts) => match partial_sync_opts.bootstrap_strategy {
JsPartialBootstrapStrategy::Prefix { length } => Some(PartialSyncOpts {
bootstrap_strategy: PartialBootstrapStrategy::Prefix {
bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix {
length: length as usize,
},
}),
segment_size: partial_sync_opts.segment_size.unwrap_or(0) as usize,
prefetch: partial_sync_opts.prefetch.unwrap_or(false),
}),
JsPartialBootstrapStrategy::Query { query } => Some(PartialSyncOpts {
bootstrap_strategy: PartialBootstrapStrategy::Query { query },
bootstrap_strategy: Some(PartialBootstrapStrategy::Query { query }),
segment_size: partial_sync_opts.segment_size.unwrap_or(0) as usize,
prefetch: partial_sync_opts.prefetch.unwrap_or(false),
}),
Expand All @@ -298,6 +301,7 @@ impl SyncEngine {
pub fn connect(&mut self) -> napi::Result<GeneratorHolder> {
let opts = DatabaseSyncEngineOpts {
client_name: self.opts.client_name.clone(),
remote_url: self.opts.remote_url.clone(),
wal_pull_batch_size: self.opts.wal_pull_batch_size as u64,
long_poll_timeout: self.opts.long_poll_timeout,
tables_ignore: self.opts.tables_ignore.clone(),
Expand Down
18 changes: 15 additions & 3 deletions bindings/python/src/turso_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ impl PyTursoPartialSyncOpts {
pub struct PyTursoSyncDatabaseConfig {
// path to the main database file (auxilary files like metadata, WAL, revert, changes will derive names from this path)
pub path: String,
// optional remote url (libsql://..., https://... or http://...)
// this URL will be saved in the database metadata file in order to be able to reuse it if later client will be constructed without explicit remote url
pub remote_url: Option<String>,
// arbitrary client name which will be used as a prefix for unique client id
pub client_name: String,
// long poll timeout for pull method (if set, server will hold connection for the given timeout until new changes will appear)
Expand All @@ -75,6 +78,7 @@ impl PyTursoSyncDatabaseConfig {
#[pyo3(signature = (
path,
client_name,
remote_url=None,
long_poll_timeout_ms=None,
bootstrap_if_empty=true,
reserved_bytes=None,
Expand All @@ -83,13 +87,15 @@ impl PyTursoSyncDatabaseConfig {
fn new(
path: String,
client_name: String,
remote_url: Option<String>,
long_poll_timeout_ms: Option<u32>,
bootstrap_if_empty: bool,
reserved_bytes: Option<usize>,
partial_sync_opts: Option<&PyTursoPartialSyncOpts>,
) -> Self {
Self {
path,
remote_url,
client_name,
long_poll_timeout_ms,
bootstrap_if_empty,
Expand All @@ -116,6 +122,7 @@ pub fn py_turso_sync_new(
};
let sync_config = rsapi::TursoDatabaseSyncConfig {
path: sync_config.path.clone(),
remote_url: sync_config.remote_url.clone(),
client_name: sync_config.client_name.clone(),
bootstrap_if_empty: sync_config.bootstrap_if_empty,
long_poll_timeout_ms: sync_config.long_poll_timeout_ms,
Expand All @@ -124,7 +131,7 @@ pub fn py_turso_sync_new(
Some(config) => {
if let Some(length) = config.bootstrap_strategy_prefix {
Some(PartialSyncOpts {
bootstrap_strategy: PartialBootstrapStrategy::Prefix { length },
bootstrap_strategy: Some(PartialBootstrapStrategy::Prefix { length }),
segment_size: config.segment_size.unwrap_or(0),
prefetch: config.prefetch.unwrap_or(false),
})
Expand All @@ -133,9 +140,9 @@ pub fn py_turso_sync_new(
.bootstrap_strategy_query
.as_ref()
.map(|query| PartialSyncOpts {
bootstrap_strategy: PartialBootstrapStrategy::Query {
bootstrap_strategy: Some(PartialBootstrapStrategy::Query {
query: query.clone(),
},
}),
segment_size: config.segment_size.unwrap_or(0),
prefetch: config.prefetch.unwrap_or(false),
})
Expand Down Expand Up @@ -341,6 +348,9 @@ pub enum PyTursoSyncIoItemRequestKind {

#[pyclass]
pub struct PyTursoSyncIoItemHttpRequest {
/// optional HTTP url
#[pyo3(get)]
pub url: Option<String>,
/// HTTP method (e.g. POST / GET)
#[pyo3(get)]
pub method: String,
Expand Down Expand Up @@ -395,6 +405,7 @@ impl PyTursoSyncIoItem {
pub fn request(&self, py: pyo3::Python) -> PyResult<PyTursoSyncIoItemRequest> {
match self.item.get_request() {
turso_sync_sdk_kit::sync_engine_io::SyncEngineIoRequest::Http {
url,
method,
path,
body,
Expand All @@ -406,6 +417,7 @@ impl PyTursoSyncIoItem {
http: Some(pyo3::Py::new(
py,
PyTursoSyncIoItemHttpRequest {
url: url.clone(),
method: method.clone(),
path: path.clone(),
body: body
Expand Down
13 changes: 9 additions & 4 deletions bindings/python/tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", force=True)


def connect(provider, database):
if provider == "turso":
return turso.connect(database)
Expand Down Expand Up @@ -1464,14 +1465,18 @@ def test_connection_exception_attributes_present(provider):
assert issubclass(conn.ProgrammingError, Exception)
conn.close()


@pytest.mark.parametrize("provider", ["sqlite3", "turso"])
def test_insert_returning_single_and_multiple_commit_without_consuming(provider):
# turso.setup_logging(level=logging.DEBUG)
conn = connect(provider, ":memory:")
try:
cur = conn.cursor()
cur.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
cur.execute("INSERT INTO t(name) VALUES (?), (?) RETURNING id", ("bob", "alice"),)
cur.execute(
"INSERT INTO t(name) VALUES (?), (?) RETURNING id",
("bob", "alice"),
)
cur.fetchone()
with pytest.raises(Exception):
conn.commit()
Expand All @@ -1495,15 +1500,15 @@ def test_pragma_integrity_check(provider):

conn.close()


def test_encryption_enabled(tmp_path):
tmp_path = tmp_path / "local.db"
conn = turso.connect(
str(tmp_path),
experimental_features="encryption",
encryption=turso.EncryptionOpts(
cipher="aegis256",
hexkey="b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"
)
cipher="aegis256", hexkey="b1bbfda4f589dc9daaf004fe21111e00dc00c98237102f5c7002a5669fc76327"
),
)
cursor = conn.cursor()
cursor.execute("create table t(x)")
Expand Down
Loading