Repository: tursodatabase/go-libsql
Pull request #64: Add `NewSyncedDatabaseConnector`
Author: login: levydsa
Created: 2025-05-27T20:14:08Z | Merged: 2025-05-28T10:03:01Z

Resolves #60.

Linked issue #60: Offline write support
Let's add a `NewSyncedDatabaseConnector` API.
Linked issue comments: user: login: shynome
created_at: 2025-07-06T05:00:38Z
body: hi, I want to known can I sync single table?

Thanks for any reply~
author_association: NONE

Diff:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..236f146
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,52 @@
+name: Test
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+  workflow_dispatch:
+    inputs:
+      libsql-server-release:
+        description: 'LibSQL Server Release'
+        required: true
+        default: 'libsql-server-v0.24.32'
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version: [ '1.24' ]
+        os: [ubuntu-latest, macos-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.go-version }}
+
+      - name: Install sqld
+        run: |
+          curl --proto '=https' --tlsv1.2 -LsSf https://github.com/tursodatabase/libsql/releases/download/${{ github.event.inputs.libsql-server-release || 'libsql-server-v0.24.32' }}/libsql-server-installer.sh | sh
+          echo "$HOME/.sqld/bin" >> $GITHUB_PATH
+          sqld --version
+
+      - name: Start sqld server
+        run: |
+          sqld &
+          while ! curl -s http://localhost:8080/health > /dev/null; do
+            echo "Waiting for sqld..."
+            sleep 1
+          done
+          echo "sqld is ready!"
+
+      - name: Build
+        run: go build -v ./...
+
+      - name: Test
+        env:
+          LIBSQL_PRIMARY_URL: "http://localhost:8080"
+          LIBSQL_AUTH_TOKEN: ""
+        run: go test -v ./...
diff --git a/lib/include/libsql.h b/lib/include/libsql.h
index 7fdfb4b..505c4b2 100644
--- a/lib/include/libsql.h
+++ b/lib/include/libsql.h
@@ -40,6 +40,7 @@ typedef struct {
   const char *encryption_key;
   int sync_interval;
   char with_webpki;
+  char offline;
 } libsql_config;
 
 typedef const libsql_connection *libsql_connection_t;
@@ -61,6 +62,8 @@ typedef struct {
 extern "C" {
 #endif // __cplusplus
 
+int libsql_enable_internal_tracing(void);
+
 int libsql_sync(libsql_database_t db, const char **out_err_msg);
 
 int libsql_sync2(libsql_database_t db, replicated *out_replicated, const char **out_err_msg);
diff --git a/libsql.go b/libsql.go
index 6ffcd97..f841348 100644
--- a/libsql.go
+++ b/libsql.go
@@ -54,7 +54,7 @@ type Option interface {
 type option func(*config) error
 
 type Replicated struct {
-	FrameNo int
+	FrameNo      int
 	FramesSynced int
 }
 
@@ -135,7 +135,37 @@ func NewEmbeddedReplicaConnector(dbPath string, primaryUrl string, opts ...Optio
 	if config.syncInterval != nil {
 		syncInterval = *config.syncInterval
 	}
-	return openEmbeddedReplicaConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval)
+	return openSyncConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval, false)
+}
+
+func NewSyncedDatabaseConnector(dbPath string, primaryUrl string, opts ...Option) (*Connector, error) {
+	var config config
+	errs := make([]error, 0, len(opts))
+	for _, opt := range opts {
+		if err := opt.apply(&config); err != nil {
+			errs = append(errs, err)
+		}
+	}
+	if len(errs) > 0 {
+		return nil, errors.Join(errs...)
+	}
+	authToken := ""
+	if config.authToken != nil {
+		authToken = *config.authToken
+	}
+	readYourWrites := true
+	if config.readYourWrites != nil {
+		readYourWrites = *config.readYourWrites
+	}
+	encryptionKey := ""
+	if config.encryptionKey != nil {
+		encryptionKey = *config.encryptionKey
+	}
+	syncInterval := time.Duration(0)
+	if config.syncInterval != nil {
+		syncInterval = *config.syncInterval
+	}
+	return openSyncConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval, true)
 }
 
 type driver struct{}
@@ -173,7 +203,7 @@ func (d driver) OpenConnector(dbAddress string) (sqldriver.Connector, error) {
 
 func libsqlSync(nativeDbPtr C.libsql_database_t) (Replicated, error) {
 	var errMsg *C.char
-	var rep C.replicated;
+	var rep C.replicated
 	statusCode := C.libsql_sync2(nativeDbPtr, &rep, &errMsg)
 	if statusCode != 0 {
 		return Replicated{0, 0}, libsqlError("failed to sync database ", statusCode, errMsg)
@@ -198,10 +228,10 @@ func openRemoteConnector(primaryUrl, authToken string) (*Connector, error) {
 	return &Connector{nativeDbPtr: nativeDbPtr}, nil
 }
 
-func openEmbeddedReplicaConnector(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string, syncInterval time.Duration) (*Connector, error) {
+func openSyncConnector(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string, syncInterval time.Duration, offline bool) (*Connector, error) {
 	var closeCh chan struct{}
 	var closeAckCh chan struct{}
-	nativeDbPtr, err := libsqlOpenWithSync(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey)
+	nativeDbPtr, err := libsqlOpenWithSync(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, offline)
 	if err != nil {
 		return nil, err
 	}
@@ -309,7 +339,7 @@ func libsqlOpenRemote(url, authToken string) (C.libsql_database_t, error) {
 	return db, nil
 }
 
-func libsqlOpenWithSync(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string) (C.libsql_database_t, error) {
+func libsqlOpenWithSync(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string, offline bool) (C.libsql_database_t, error) {
 	dbPathNativeString := C.CString(dbPath)
 	defer C.free(unsafe.Pointer(dbPathNativeString))
 	primaryUrlNativeString := C.CString(primaryUrl)
@@ -321,15 +351,30 @@ func libsqlOpenWithSync(dbPath, primaryUrl, authToken string, readYourWrites boo
 	if readYourWrites {
 		readYourWritesNative = 1
 	}
+
+	var offlineNative C.char = 0
+	if offline {
+		offlineNative = 1
+	}
+
 	var encrytionKeyNativeString *C.char
 	if encryptionKey != "" {
 		encrytionKeyNativeString = C.CString(encryptionKey)
 		defer C.free(unsafe.Pointer(encrytionKeyNativeString))
 	}
 
+	config := C.libsql_config{
+		db_path:          dbPathNativeString,
+		auth_token:       authTokenNativeString,
+		primary_url:      primaryUrlNativeString,
+		read_your_writes: readYourWritesNative,
+		encryption_key:   encrytionKeyNativeString,
+		offline:          offlineNative,
+	}
+
 	var db C.libsql_database_t
 	var errMsg *C.char
-	statusCode := C.libsql_open_sync(dbPathNativeString, primaryUrlNativeString, authTokenNativeString, readYourWritesNative, encrytionKeyNativeString, &db, &errMsg)
+	statusCode := C.libsql_open_sync_with_config(config, &db, &errMsg)
 	if statusCode != 0 {
 		return nil, libsqlError(fmt.Sprintf("failed to open database %s %s", dbPath, primaryUrl), statusCode, errMsg)
 	}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Adds a new constructor for synced connectors and refactors existing sync setup to use a unified helper with offline support.

- Introduces `NewSyncedDatabaseConnector` for always-on syncing, with an `offline` mode flag  
- Refactors `openEmbeddedReplicaConnector` into `openSyncConnector` and updates C bindings to use a config struct  
- Extends `libsql_config` in the C header with an `offline` field and updates the workflow for end-to-end testing

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                         | Description                                                      |
| ---------------------------- | ---------------------------------------------------------------- |
| libsql.go                    | Added `NewSyncedDatabaseConnector`, refactored sync helper, and updated CGo binding to use `libsql_open_sync_with_config` |
| lib/include/libsql.h         | Extended `libsql_config` with `offline` and added tracing API   |
| .github/workflows/test.yml   | Added CI steps to install libsql server and run tests on macOS and Linux |


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**lib/include/libsql.h:69**
* The C function `libsql_open_sync_with_config` is invoked from Go but not declared in this header. Add its prototype to `libsql.h` so cgo can locate and link it correctly.
```
int libsql_sync2(libsql_database_t db, replicated *out_replicated, const char **out_err_msg);
```
**libsql.go:141**
* There are no tests for the new offline sync path introduced by `NewSyncedDatabaseConnector`. Consider adding unit or integration tests covering both online and offline modes.
```
func NewSyncedDatabaseConnector(dbPath string, primaryUrl string, opts ...Option) (*Connector, error) {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-28T07:02:32Z
user: login: penberg
body: Looks good, @avinassh can you also review for sanity?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T07:04:43Z
user: login: avinassh
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T08:29:38Z

Review comments:
diff_hunk: @@ -135,7 +135,37 @@ func NewEmbeddedReplicaConnector(dbPath string, primaryUrl string, opts ...Optio
 	if config.syncInterval != nil {
 		syncInterval = *config.syncInterval
 	}
-	return openEmbeddedReplicaConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval)
+	return openSyncConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval, false)
+}
+
path: libsql.go
user: login: Copilot
body: [nitpick] Add a Go doc comment for `NewSyncedDatabaseConnector` explaining its parameters, behavior, and how the `offline` flag differs from `NewEmbeddedReplicaConnector`.
```suggestion

// NewSyncedDatabaseConnector creates a Connector for a synchronized database.
// 
// Parameters:
// - dbPath: The file path to the local database.
// - primaryUrl: The URL of the primary database to synchronize with.
// - opts: A variadic list of options to configure the connector (e.g., encryption key, sync interval).
//
// Behavior:
// This function initializes a Connector that synchronizes the local database with a primary database.
// It applies the provided options to configure the connector. If any errors occur during option application,
// they are aggregated and returned.
//
// Difference from NewEmbeddedReplicaConnector:
// The `offline` flag passed to `openSyncConnector` is set to `true` in this function, indicating that
// the connector operates in synchronized mode. In contrast, `NewEmbeddedReplicaConnector` sets the `offline`
// flag to `false`, indicating embedded replica mode.
```
created_at: 2025-05-28T07:02:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,46 @@
+name: Test
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version: [ '1.20' ]
path: .github/workflows/test.yml
user: login: avinassh
body: nit: this version is old though
created_at: 2025-05-28T08:27:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+name: Test
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+  test:
+    strategy:
+      matrix:
+        go-version: [ '1.20' ]
+        os: [ubuntu-latest, macos-latest]
+        libsql-server-release: [libsql-server-v0.24.32]
path: .github/workflows/test.yml
user: login: avinassh
body: can you also add a workflow_dispatch which lets us specify `libsql-server` version 
created_at: 2025-05-28T08:27:43Z
author_association: MEMBER

PR comments:
user: login: levydsa
created_at: 2025-05-27T22:05:27Z
body: Also tested this with a AWS server and the `TestEncryption` test fails. Is this expected?
author_association: CONTRIBUTOR
user: login: avinassh
created_at: 2025-05-28T08:26:32Z
body: > Also tested this with a AWS server and the TestEncryption test fails. Is this expected?

new server does not support encryption yet 

author_association: MEMBER