├── .github └── workflows │ ├── test.yml │ └── update-libsql.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── update_libs.md ├── example ├── go.mod ├── go.sum ├── local │ └── main.go ├── remote │ └── main.go └── sync │ └── main.go ├── go.mod ├── go.sum ├── lib ├── darwin_arm64 │ └── libsql_experimental.a ├── include │ └── libsql.h ├── linux_amd64 │ └── libsql_experimental.a └── linux_arm64 │ └── libsql_experimental.a ├── libsql.go └── libsql_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | inputs: 10 | libsql-server-release: 11 | description: 'LibSQL Server Release' 12 | required: true 13 | default: 'libsql-server-v0.24.32' 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | go-version: [ '1.24' ] 20 | os: [ubuntu-latest, macos-latest] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v3 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | 30 | - name: Install sqld 31 | run: | 32 | 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 33 | echo "$HOME/.sqld/bin" >> $GITHUB_PATH 34 | sqld --version 35 | 36 | - name: Start sqld server 37 | run: | 38 | sqld & 39 | while ! curl -s http://localhost:8080/health > /dev/null; do 40 | echo "Waiting for sqld..." 41 | sleep 1 42 | done 43 | echo "sqld is ready!" 44 | 45 | - name: Build 46 | run: go build -v ./... 47 | 48 | - name: Test 49 | env: 50 | LIBSQL_PRIMARY_URL: "http://localhost:8080" 51 | LIBSQL_AUTH_TOKEN: "" 52 | run: go test -v ./... 53 | -------------------------------------------------------------------------------- /.github/workflows/update-libsql.yml: -------------------------------------------------------------------------------- 1 | name: Build libsql libraries 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | libsql_tag: 7 | description: 'libsql repository tag to build (e.g. libsql-0.9.4)' 8 | required: true 9 | default: 'libsql-0.9.4' 10 | 11 | jobs: 12 | build-linux: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu] 17 | include: 18 | - target: x86_64-unknown-linux-gnu 19 | output_dir: linux_amd64 20 | - target: aarch64-unknown-linux-gnu 21 | output_dir: linux_arm64 22 | steps: 23 | - name: Checkout go-libsql 24 | uses: actions/checkout@v4 25 | with: 26 | path: go-libsql 27 | 28 | - name: Set libsql tag 29 | id: set-tag 30 | run: | 31 | TAG=${{ github.event.inputs.libsql_tag }} 32 | echo "Using tag: $TAG" 33 | echo "LIBSQL_TAG=$TAG" >> $GITHUB_ENV 34 | 35 | - name: Checkout libsql at tag 36 | uses: actions/checkout@v4 37 | with: 38 | repository: tursodatabase/libsql 39 | ref: ${{ env.LIBSQL_TAG }} 40 | path: libsql 41 | 42 | - name: Set up Rust 43 | uses: dtolnay/rust-toolchain@stable 44 | with: 45 | toolchain: stable 46 | override: true 47 | 48 | - name: Install cross 49 | run: cargo install cross 50 | 51 | - name: Build libsql for ${{ matrix.target }} 52 | working-directory: libsql/bindings/c 53 | run: | 54 | cross build --release --target ${{ matrix.target }} 55 | 56 | - name: Create output directory 57 | run: | 58 | mkdir -p go-libsql/lib/${{ matrix.output_dir }} 59 | 60 | - name: Copy library files 61 | run: | 62 | cp libsql/target/${{ matrix.target }}/release/libsql_experimental.a go-libsql/lib/${{ matrix.output_dir }}/ 63 | 64 | - name: Upload artifacts 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: libsql-${{ matrix.output_dir }} 68 | path: go-libsql/lib/${{ matrix.output_dir }} 69 | 70 | build-macos: 71 | runs-on: macos-latest 72 | steps: 73 | - name: Checkout go-libsql 74 | uses: actions/checkout@v4 75 | with: 76 | path: go-libsql 77 | 78 | - name: Set libsql tag 79 | id: set-tag 80 | run: | 81 | TAG=${{ github.event.inputs.libsql_tag }} 82 | echo "Using tag: $TAG" 83 | echo "LIBSQL_TAG=$TAG" >> $GITHUB_ENV 84 | 85 | - name: Checkout libsql at tag 86 | uses: actions/checkout@v4 87 | with: 88 | repository: tursodatabase/libsql 89 | ref: ${{ env.LIBSQL_TAG }} 90 | path: libsql 91 | 92 | - name: Set up Rust 93 | uses: dtolnay/rust-toolchain@stable 94 | with: 95 | targets: aarch64-apple-darwin 96 | 97 | - name: Build libsql for macOS 98 | working-directory: libsql/bindings/c 99 | run: | 100 | cargo build --release 101 | 102 | - name: Create output directory 103 | run: | 104 | mkdir -p go-libsql/lib/darwin_arm64 105 | 106 | - name: Copy library files 107 | run: | 108 | cp libsql/target/release/libsql_experimental.a go-libsql/lib/darwin_arm64/ 109 | 110 | - name: Upload artifacts 111 | uses: actions/upload-artifact@v4 112 | with: 113 | name: libsql-darwin_arm64 114 | path: go-libsql/lib/darwin_arm64 115 | 116 | verify-linux-amd64: 117 | needs: [build-linux] 118 | runs-on: ubuntu-latest 119 | steps: 120 | - name: Checkout go-libsql 121 | uses: actions/checkout@v4 122 | 123 | - name: Download Linux AMD64 artifact 124 | uses: actions/download-artifact@v4 125 | with: 126 | name: libsql-linux_amd64 127 | path: lib/linux_amd64 128 | 129 | - name: Check binary details 130 | run: | 131 | echo "Linux AMD64 library size:" 132 | ls -la lib/linux_amd64/libsql_experimental.a 133 | 134 | - name: Set up Go 135 | uses: actions/setup-go@v4 136 | with: 137 | go-version: '1.20' 138 | 139 | - name: Verify Linux AMD64 build 140 | run: | 141 | echo "Building example/local/main.go for Linux AMD64..." 142 | cd example/local 143 | GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -v 144 | echo "Linux AMD64 build successful!" 145 | 146 | verify-linux-arm64: 147 | needs: [build-linux] 148 | runs-on: ubuntu-latest 149 | steps: 150 | - name: Checkout go-libsql 151 | uses: actions/checkout@v4 152 | 153 | - name: Download Linux ARM64 artifact 154 | uses: actions/download-artifact@v4 155 | with: 156 | name: libsql-linux_arm64 157 | path: lib/linux_arm64 158 | 159 | - name: Check binary details 160 | run: | 161 | echo "Linux ARM64 library size:" 162 | ls -la lib/linux_arm64/libsql_experimental.a 163 | 164 | - name: Set up Go 165 | uses: actions/setup-go@v4 166 | with: 167 | go-version: '1.20' 168 | 169 | - name: Install cross-compiler for ARM64 170 | run: | 171 | sudo apt-get update 172 | sudo apt-get install -y gcc-aarch64-linux-gnu 173 | 174 | - name: Verify Linux ARM64 build 175 | run: | 176 | echo "Building example/local/main.go for Linux ARM64..." 177 | cd example/local 178 | CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build -v 179 | echo "Linux ARM64 build successful!" 180 | 181 | verify-darwin-arm64: 182 | needs: [build-macos] 183 | runs-on: macos-latest 184 | steps: 185 | - name: Checkout go-libsql 186 | uses: actions/checkout@v4 187 | 188 | - name: Download Darwin ARM64 artifact 189 | uses: actions/download-artifact@v4 190 | with: 191 | name: libsql-darwin_arm64 192 | path: lib/darwin_arm64 193 | 194 | - name: Check binary details 195 | run: | 196 | echo "Darwin ARM64 library size:" 197 | ls -la lib/darwin_arm64/libsql_experimental.a 198 | 199 | - name: Set up Go 200 | uses: actions/setup-go@v4 201 | with: 202 | go-version: '1.20' 203 | 204 | - name: Verify Darwin ARM64 build 205 | run: | 206 | echo "Building example/local/main.go for Darwin ARM64..." 207 | cd example/local 208 | GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -v 209 | echo "Darwin ARM64 build successful!" 210 | 211 | update-repository: 212 | needs: [verify-linux-amd64, verify-linux-arm64, verify-darwin-arm64] 213 | runs-on: ubuntu-latest 214 | if: github.event_name != 'pull_request' 215 | steps: 216 | - name: Checkout go-libsql 217 | uses: actions/checkout@v4 218 | 219 | - name: Download all artifacts 220 | uses: actions/download-artifact@v4 221 | with: 222 | path: artifacts 223 | 224 | - name: Copy artifacts to repository 225 | run: | 226 | mkdir -p lib/linux_amd64 lib/linux_arm64 lib/darwin_arm64 227 | cp -r artifacts/libsql-linux_amd64/* lib/linux_amd64/ 228 | cp -r artifacts/libsql-linux_arm64/* lib/linux_arm64/ 229 | cp -r artifacts/libsql-darwin_arm64/* lib/darwin_arm64/ 230 | # Clean up artifacts directory to prevent it from being included in the PR 231 | rm -rf artifacts 232 | 233 | - name: Set libsql tag 234 | id: set-tag 235 | run: | 236 | TAG=${{ github.event.inputs.libsql_tag }} 237 | echo "LIBSQL_TAG=$TAG" >> $GITHUB_ENV 238 | 239 | - name: Create Pull Request 240 | uses: peter-evans/create-pull-request@v5 241 | with: 242 | commit-message: "Update libsql libraries to `${{ env.LIBSQL_TAG }}`" 243 | title: "Update libsql libraries to `${{ env.LIBSQL_TAG }}`" 244 | body: | 245 | This PR updates the libsql static libraries to version `${{ env.LIBSQL_TAG }}`. 246 | 247 | Libraries updated: 248 | - `linux_amd64/libsql_experimental.a` 249 | - `linux_arm64/libsql_experimental.a` 250 | - `darwin_arm64/libsql_experimental.a` 251 | 252 | This update was generated automatically by the [update-libsql workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). 253 | branch: update-${{ env.LIBSQL_TAG }} 254 | delete-branch: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Joshua Wise 4 | Copyright (c) 2023 Pekka Enberg 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibSQL package for Go 2 | 3 | [libSQL](https://github.com/tursodatabase/libsql) is an open source, open contribution fork of SQLite. 4 | This source repository contains libSQL API bindings for Go. 5 | 6 | ## Notice 7 | This package comes with a precompiled native libraries. 8 | Currently only `linux amd64`, `linux arm64`, `darwin amd64` and `darwin arm64` are supported. 9 | We're working on adding support for more platforms. 10 | 11 | ## Features 12 | 13 | * In-memory databases and local database files, like SQLite 14 | * Remote database access to libSQL server 15 | * In-app replica that syncs with a libSQL server 16 | 17 | ## Installing 18 | 19 | ``` 20 | go get github.com/tursodatabase/go-libsql 21 | ``` 22 | 23 | `go-libsql` uses `CGO` to make calls to LibSQL. You must build your binaries with `CGO_ENABLED=1`. 24 | 25 | ## Getting Started 26 | 27 | ### Connecting to the database 28 | 29 | To connect to the database one needs to create a `libsql.Connector` using one of the factory functions: `libsql.NewEmbeddedReplicaConnector` or `libsql.NewEmbeddedReplicaConnectorWithAutoSync`. 30 | 31 | Here's an example of obtaining a `sql.DB` object from `database/sql` package: 32 | 33 | ``` 34 | dbPath := // Path do db file on local disk 35 | primaryUrl := // URL to primary database instance 36 | connector := NewEmbeddedReplicaConnector(dbPath, primaryUrl, authToken) 37 | db := sql.OpenDB(connector) 38 | defer db.Close() 39 | ``` 40 | 41 | Once `sql.DB` object is created one can use it as any other database that supports `database/sql` package. 42 | 43 | ### Fetching updates from primary database instance 44 | 45 | If the connector is created with `libsql.NewEmbeddedReplicaConnectorWithAutoSync` then it will automatically fetch updates from a primary periodically. 46 | 47 | For connectors created with `libsql.NewEmbeddedReplicaConnector` we need to fetch updates manually by calling `connector.Sync` 48 | 49 | ## Examples 50 | 51 | Module with usage examples can be found in [example directory]. 52 | 53 | ## License 54 | 55 | This project is licensed under the [MIT license]. 56 | 57 | ### Contribution 58 | 59 | Unless you explicitly state otherwise, any contribution intentionally submitted 60 | for inclusion in libSQL by you, shall be licensed as MIT, without any additional 61 | terms or conditions. 62 | 63 | [MIT license]: https://github.com/tursodatabase/go-libsql/blob/main/LICENSE 64 | [example directory]: https://github.com/tursodatabase/go-libsql/tree/main/example 65 | -------------------------------------------------------------------------------- /docs/update_libs.md: -------------------------------------------------------------------------------- 1 | Install `cross` on your machine: 2 | 3 | ``` 4 | cargo install cross 5 | ``` 6 | 7 | Then build `libsql_experimental` library: 8 | 9 | ``` 10 | git clone https://github.com/tursodatabase/libsql.git 11 | cd bindings/c 12 | cross build --release --target x86_64-unknown-linux-gnu 13 | cross build --release --target aarch64-unknown-linux-gnu 14 | ``` 15 | 16 | Finally copy library to `libs` directory of this repo: 17 | 18 | ``` 19 | libsql/target//release/libsql_experimental.a 20 | ``` 21 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tursodatabase/go-libsql/example 2 | 3 | go 1.20 4 | 5 | require github.com/tursodatabase/go-libsql v0.0.0-20230829151150-d30caadc3a7c 6 | 7 | require ( 8 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 9 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect 10 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 11 | ) 12 | 13 | replace github.com/tursodatabase/go-libsql => ../../go-libsql 14 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 2 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 3 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 4 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= 5 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= 6 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 7 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= 8 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 9 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 10 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 11 | -------------------------------------------------------------------------------- /example/local/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/tursodatabase/go-libsql" 7 | "os" 8 | ) 9 | 10 | func run() (err error) { 11 | dir, err := os.MkdirTemp("", "libsql-*") 12 | if err != nil { 13 | return err 14 | } 15 | defer os.RemoveAll(dir) 16 | db, err := sql.Open("libsql", "file:"+dir+"/test.db") 17 | if err != nil { 18 | return err 19 | } 20 | defer func() { 21 | if closeError := db.Close(); closeError != nil { 22 | fmt.Println("Error closing database", closeError) 23 | if err == nil { 24 | err = closeError 25 | } 26 | } 27 | }() 28 | 29 | _, err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") 30 | if err != nil { 31 | return err 32 | } 33 | 34 | for i := 0; i < 10; i++ { 35 | _, err = db.Exec(fmt.Sprintf("INSERT INTO test (id, name) VALUES (%d, 'test-%d')", i, i)) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | 41 | rows, err := db.Query("SELECT * FROM test") 42 | if err != nil { 43 | return err 44 | } 45 | defer func() { 46 | if closeError := rows.Close(); closeError != nil { 47 | fmt.Println("Error closing rows", closeError) 48 | if err == nil { 49 | err = closeError 50 | } 51 | } 52 | }() 53 | i := 0 54 | for rows.Next() { 55 | var id int 56 | var name string 57 | err = rows.Scan(&id, &name) 58 | if err != nil { 59 | return err 60 | } 61 | if id != i { 62 | return fmt.Errorf("expected id %d, got %d", i, id) 63 | } 64 | if name != fmt.Sprintf("test-%d", i) { 65 | return fmt.Errorf("expected name %s, got %s", fmt.Sprintf("test-%d", i), name) 66 | } 67 | i++ 68 | } 69 | if rows.Err() != nil { 70 | return rows.Err() 71 | } 72 | return nil 73 | } 74 | 75 | func main() { 76 | if err := run(); err != nil { 77 | panic(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/remote/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | _ "github.com/tursodatabase/go-libsql" 10 | ) 11 | 12 | func main() { 13 | if err := run(); err != nil { 14 | fmt.Fprintf(os.Stderr, "error running example: %v\n", err) 15 | os.Exit(1) 16 | } 17 | } 18 | 19 | func run() (err error) { 20 | // Get database URL and auth token from environment variables 21 | dbUrl := os.Getenv("TURSO_URL") 22 | if dbUrl == "" { 23 | return fmt.Errorf("TURSO_URL environment variable not set") 24 | } 25 | 26 | authToken := os.Getenv("TURSO_AUTH_TOKEN") 27 | if authToken != "" { 28 | dbUrl += "?authToken=" + authToken 29 | } 30 | 31 | // Open database connection 32 | db, err := sql.Open("libsql", dbUrl) 33 | if err != nil { 34 | return fmt.Errorf("error opening cloud db: %w", err) 35 | } 36 | defer db.Close() 37 | 38 | // Configure connection pool 39 | db.SetConnMaxIdleTime(9 * time.Second) 40 | 41 | // Create test table 42 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)") 43 | if err != nil { 44 | return fmt.Errorf("error creating table: %w", err) 45 | } 46 | 47 | // Check if test data already exists 48 | var exists bool 49 | err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM test WHERE id = 1)").Scan(&exists) 50 | if err != nil { 51 | return fmt.Errorf("error checking existing data: %w", err) 52 | } 53 | 54 | // Insert test data only if it doesn't exist 55 | if !exists { 56 | _, err = db.Exec("INSERT INTO test (id, name) VALUES (?, ?)", 1, "remote test") 57 | if err != nil { 58 | return fmt.Errorf("error inserting data: %w", err) 59 | } 60 | fmt.Println("Inserted test data") 61 | } else { 62 | fmt.Println("Test data already exists, skipping insert") 63 | } 64 | 65 | // Query the data 66 | rows, err := db.Query("SELECT * FROM test") 67 | if err != nil { 68 | return fmt.Errorf("error querying data: %w", err) 69 | } 70 | defer rows.Close() 71 | 72 | // Print results 73 | for rows.Next() { 74 | var id int 75 | var name string 76 | if err := rows.Scan(&id, &name); err != nil { 77 | return fmt.Errorf("error scanning row: %w", err) 78 | } 79 | fmt.Printf("Row: id=%d, name=%s\n", id, name) 80 | } 81 | if err := rows.Err(); err != nil { 82 | return fmt.Errorf("error iterating rows: %w", err) 83 | } 84 | 85 | fmt.Printf("Successfully connected and executed queries on %s\n", dbUrl) 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /example/sync/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/tursodatabase/go-libsql" 10 | ) 11 | 12 | func run() (err error) { 13 | primaryUrl := os.Getenv("TURSO_URL") 14 | if primaryUrl == "" { 15 | return fmt.Errorf("TURSO_URL environment variable not set") 16 | } 17 | authToken := os.Getenv("TURSO_AUTH_TOKEN") 18 | dir, err := os.MkdirTemp("", "libsql-*") 19 | if err != nil { 20 | return err 21 | } 22 | defer os.RemoveAll(dir) 23 | 24 | connector, err := libsql.NewEmbeddedReplicaConnector(dir+"/test.db", primaryUrl, libsql.WithAuthToken(authToken)) 25 | if err != nil { 26 | return err 27 | } 28 | defer func() { 29 | if closeError := connector.Close(); closeError != nil { 30 | fmt.Println("Error closing connector", closeError) 31 | if err == nil { 32 | err = closeError 33 | } 34 | } 35 | }() 36 | 37 | db := sql.OpenDB(connector) 38 | defer func() { 39 | if closeError := db.Close(); closeError != nil { 40 | fmt.Println("Error closing database", closeError) 41 | if err == nil { 42 | err = closeError 43 | } 44 | } 45 | }() 46 | 47 | for { 48 | fmt.Println("What would you like to do?") 49 | fmt.Println("1. Sync with primary") 50 | fmt.Println("2. Select from test table") 51 | fmt.Println("3. Insert row to test table") 52 | fmt.Println("4. Exit") 53 | var choice int 54 | _, err := fmt.Scanln(&choice) 55 | if err != nil { 56 | return err 57 | } 58 | switch choice { 59 | case 1: 60 | replicated, err := connector.Sync() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | fmt.Println("%d frames synced", replicated.FramesSynced) 66 | case 2: 67 | err = func() (err error) { 68 | rows, err := db.Query("SELECT * FROM test") 69 | if err != nil { 70 | if strings.Contains(err.Error(), "`no such table: test`") { 71 | fmt.Println("Table test not found. Please run `CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` on primary first and then sync.") 72 | return nil 73 | } 74 | return err 75 | } 76 | defer func() { 77 | if closeError := rows.Close(); closeError != nil { 78 | fmt.Println("Error closing rows", closeError) 79 | if err == nil { 80 | err = closeError 81 | } 82 | } 83 | }() 84 | count := 0 85 | for rows.Next() { 86 | var id int 87 | var name string 88 | err = rows.Scan(&id, &name) 89 | if err != nil { 90 | return err 91 | } 92 | fmt.Println(id, name) 93 | count++ 94 | } 95 | if rows.Err() != nil { 96 | return rows.Err() 97 | } 98 | if count == 0 { 99 | fmt.Println("Empty table. Please run `INSERT INTO test (id, name) VALUES (random(), lower(hex(randomblob(16))))` on primary and then sync.") 100 | } 101 | return nil 102 | }() 103 | if err != nil { 104 | return err 105 | } 106 | case 3: 107 | _, err := db.Exec("INSERT INTO test (id, name) VALUES (random(), lower(hex(randomblob(16))))") 108 | if err != nil { 109 | return err 110 | } 111 | case 4: 112 | return nil 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func main() { 120 | if err := run(); err != nil { 121 | panic(err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tursodatabase/go-libsql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/antlr4-go/antlr/v4 v4.13.0 7 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 8 | golang.org/x/sync v0.6.0 9 | gotest.tools v2.2.0+incompatible 10 | ) 11 | 12 | require ( 13 | github.com/google/go-cmp v0.5.9 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 2 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 3 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 4 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= 6 | github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= 7 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 8 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= 10 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 11 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 12 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 13 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 14 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 15 | -------------------------------------------------------------------------------- /lib/darwin_arm64/libsql_experimental.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tursodatabase/go-libsql/9c24e0e7fa971ce3dfdf660aacb8df0b8535668e/lib/darwin_arm64/libsql_experimental.a -------------------------------------------------------------------------------- /lib/include/libsql.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBSQL_EXPERIMENTAL_H 2 | #define LIBSQL_EXPERIMENTAL_H 3 | 4 | #include 5 | 6 | #define LIBSQL_INT 1 7 | 8 | #define LIBSQL_FLOAT 2 9 | 10 | #define LIBSQL_TEXT 3 11 | 12 | #define LIBSQL_BLOB 4 13 | 14 | #define LIBSQL_NULL 5 15 | 16 | typedef struct libsql_connection libsql_connection; 17 | 18 | typedef struct libsql_database libsql_database; 19 | 20 | typedef struct libsql_row libsql_row; 21 | 22 | typedef struct libsql_rows libsql_rows; 23 | 24 | typedef struct libsql_rows_future libsql_rows_future; 25 | 26 | typedef struct libsql_stmt libsql_stmt; 27 | 28 | typedef const libsql_database *libsql_database_t; 29 | 30 | typedef struct { 31 | int frame_no; 32 | int frames_synced; 33 | } replicated; 34 | 35 | typedef struct { 36 | const char *db_path; 37 | const char *primary_url; 38 | const char *auth_token; 39 | char read_your_writes; 40 | const char *encryption_key; 41 | int sync_interval; 42 | char with_webpki; 43 | char offline; 44 | } libsql_config; 45 | 46 | typedef const libsql_connection *libsql_connection_t; 47 | 48 | typedef const libsql_stmt *libsql_stmt_t; 49 | 50 | typedef const libsql_rows *libsql_rows_t; 51 | 52 | typedef const libsql_rows_future *libsql_rows_future_t; 53 | 54 | typedef const libsql_row *libsql_row_t; 55 | 56 | typedef struct { 57 | const char *ptr; 58 | int len; 59 | } blob; 60 | 61 | #ifdef __cplusplus 62 | extern "C" { 63 | #endif // __cplusplus 64 | 65 | int libsql_enable_internal_tracing(void); 66 | 67 | int libsql_sync(libsql_database_t db, const char **out_err_msg); 68 | 69 | int libsql_sync2(libsql_database_t db, replicated *out_replicated, const char **out_err_msg); 70 | 71 | int libsql_open_sync(const char *db_path, 72 | const char *primary_url, 73 | const char *auth_token, 74 | char read_your_writes, 75 | const char *encryption_key, 76 | libsql_database_t *out_db, 77 | const char **out_err_msg); 78 | 79 | int libsql_open_sync_with_webpki(const char *db_path, 80 | const char *primary_url, 81 | const char *auth_token, 82 | char read_your_writes, 83 | const char *encryption_key, 84 | libsql_database_t *out_db, 85 | const char **out_err_msg); 86 | 87 | int libsql_open_sync_with_config(libsql_config config, libsql_database_t *out_db, const char **out_err_msg); 88 | 89 | int libsql_open_ext(const char *url, libsql_database_t *out_db, const char **out_err_msg); 90 | 91 | int libsql_open_file(const char *url, libsql_database_t *out_db, const char **out_err_msg); 92 | 93 | int libsql_open_remote(const char *url, const char *auth_token, libsql_database_t *out_db, const char **out_err_msg); 94 | 95 | int libsql_open_remote_with_webpki(const char *url, 96 | const char *auth_token, 97 | libsql_database_t *out_db, 98 | const char **out_err_msg); 99 | 100 | void libsql_close(libsql_database_t db); 101 | 102 | int libsql_connect(libsql_database_t db, libsql_connection_t *out_conn, const char **out_err_msg); 103 | 104 | int libsql_load_extension(libsql_connection_t conn, 105 | const char *path, 106 | const char *entry_point, 107 | const char **out_err_msg); 108 | 109 | int libsql_reset(libsql_connection_t conn, const char **out_err_msg); 110 | 111 | void libsql_disconnect(libsql_connection_t conn); 112 | 113 | int libsql_prepare(libsql_connection_t conn, const char *sql, libsql_stmt_t *out_stmt, const char **out_err_msg); 114 | 115 | int libsql_bind_int(libsql_stmt_t stmt, int idx, long long value, const char **out_err_msg); 116 | 117 | int libsql_bind_float(libsql_stmt_t stmt, int idx, double value, const char **out_err_msg); 118 | 119 | int libsql_bind_null(libsql_stmt_t stmt, int idx, const char **out_err_msg); 120 | 121 | int libsql_bind_string(libsql_stmt_t stmt, int idx, const char *value, const char **out_err_msg); 122 | 123 | int libsql_bind_blob(libsql_stmt_t stmt, int idx, const unsigned char *value, int value_len, const char **out_err_msg); 124 | 125 | int libsql_query_stmt(libsql_stmt_t stmt, libsql_rows_t *out_rows, const char **out_err_msg); 126 | 127 | int libsql_execute_stmt(libsql_stmt_t stmt, const char **out_err_msg); 128 | 129 | int libsql_reset_stmt(libsql_stmt_t stmt, const char **out_err_msg); 130 | 131 | void libsql_free_stmt(libsql_stmt_t stmt); 132 | 133 | int libsql_query(libsql_connection_t conn, const char *sql, libsql_rows_t *out_rows, const char **out_err_msg); 134 | 135 | int libsql_execute(libsql_connection_t conn, const char *sql, const char **out_err_msg); 136 | 137 | void libsql_free_rows(libsql_rows_t res); 138 | 139 | void libsql_free_rows_future(libsql_rows_future_t res); 140 | 141 | void libsql_wait_result(libsql_rows_future_t res); 142 | 143 | int libsql_column_count(libsql_rows_t res); 144 | 145 | int libsql_column_name(libsql_rows_t res, int col, const char **out_name, const char **out_err_msg); 146 | 147 | int libsql_column_type(libsql_rows_t res, libsql_row_t row, int col, int *out_type, const char **out_err_msg); 148 | 149 | uint64_t libsql_changes(libsql_connection_t conn); 150 | 151 | int64_t libsql_last_insert_rowid(libsql_connection_t conn); 152 | 153 | int libsql_next_row(libsql_rows_t res, libsql_row_t *out_row, const char **out_err_msg); 154 | 155 | void libsql_free_row(libsql_row_t res); 156 | 157 | int libsql_get_string(libsql_row_t res, int col, const char **out_value, const char **out_err_msg); 158 | 159 | void libsql_free_string(const char *ptr); 160 | 161 | int libsql_get_int(libsql_row_t res, int col, long long *out_value, const char **out_err_msg); 162 | 163 | int libsql_get_float(libsql_row_t res, int col, double *out_value, const char **out_err_msg); 164 | 165 | int libsql_get_blob(libsql_row_t res, int col, blob *out_blob, const char **out_err_msg); 166 | 167 | void libsql_free_blob(blob b); 168 | 169 | #ifdef __cplusplus 170 | } // extern "C" 171 | #endif // __cplusplus 172 | 173 | #endif /* LIBSQL_EXPERIMENTAL_H */ 174 | -------------------------------------------------------------------------------- /lib/linux_amd64/libsql_experimental.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tursodatabase/go-libsql/9c24e0e7fa971ce3dfdf660aacb8df0b8535668e/lib/linux_amd64/libsql_experimental.a -------------------------------------------------------------------------------- /lib/linux_arm64/libsql_experimental.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tursodatabase/go-libsql/9c24e0e7fa971ce3dfdf660aacb8df0b8535668e/lib/linux_arm64/libsql_experimental.a -------------------------------------------------------------------------------- /libsql.go: -------------------------------------------------------------------------------- 1 | //go:build cgo 2 | // +build cgo 3 | 4 | package libsql 5 | 6 | /* 7 | #cgo CFLAGS: -I${SRCDIR}/lib/include 8 | #cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/lib/darwin_amd64 9 | #cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/darwin_arm64 10 | #cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux_amd64 11 | #cgo linux,arm64 LDFLAGS: -L${SRCDIR}/lib/linux_arm64 12 | #cgo LDFLAGS: -lsql_experimental 13 | #cgo LDFLAGS: -lm 14 | #cgo darwin LDFLAGS: -framework Security 15 | #cgo darwin LDFLAGS: -framework CoreFoundation 16 | #include 17 | #include 18 | */ 19 | import "C" 20 | 21 | import ( 22 | "context" 23 | "database/sql" 24 | sqldriver "database/sql/driver" 25 | "errors" 26 | "fmt" 27 | "io" 28 | "net/url" 29 | "regexp" 30 | "strings" 31 | "time" 32 | "unsafe" 33 | 34 | "github.com/antlr4-go/antlr/v4" 35 | "github.com/libsql/sqlite-antlr4-parser/sqliteparser" 36 | "github.com/libsql/sqlite-antlr4-parser/sqliteparserutils" 37 | ) 38 | 39 | func init() { 40 | sql.Register("libsql", driver{}) 41 | } 42 | 43 | type config struct { 44 | authToken *string 45 | readYourWrites *bool 46 | encryptionKey *string 47 | syncInterval *time.Duration 48 | } 49 | 50 | type Option interface { 51 | apply(*config) error 52 | } 53 | 54 | type option func(*config) error 55 | 56 | type Replicated struct { 57 | FrameNo int 58 | FramesSynced int 59 | } 60 | 61 | func (o option) apply(c *config) error { 62 | return o(c) 63 | } 64 | 65 | func WithAuthToken(authToken string) Option { 66 | return option(func(o *config) error { 67 | if o.authToken != nil { 68 | return fmt.Errorf("authToken already set") 69 | } 70 | if authToken == "" { 71 | return fmt.Errorf("authToken must not be empty") 72 | } 73 | o.authToken = &authToken 74 | return nil 75 | }) 76 | } 77 | 78 | func WithReadYourWrites(readYourWrites bool) Option { 79 | return option(func(o *config) error { 80 | if o.readYourWrites != nil { 81 | return fmt.Errorf("read your writes already set") 82 | } 83 | o.readYourWrites = &readYourWrites 84 | return nil 85 | }) 86 | } 87 | 88 | func WithEncryption(key string) Option { 89 | return option(func(o *config) error { 90 | if o.encryptionKey != nil { 91 | return fmt.Errorf("encryption key already set") 92 | } 93 | if key == "" { 94 | return fmt.Errorf("encryption key must not be empty") 95 | } 96 | o.encryptionKey = &key 97 | return nil 98 | }) 99 | } 100 | 101 | func WithSyncInterval(interval time.Duration) Option { 102 | return option(func(o *config) error { 103 | if o.syncInterval != nil { 104 | return fmt.Errorf("sync interval already set") 105 | } 106 | o.syncInterval = &interval 107 | return nil 108 | }) 109 | } 110 | 111 | func NewEmbeddedReplicaConnector(dbPath string, primaryUrl string, opts ...Option) (*Connector, error) { 112 | var config config 113 | errs := make([]error, 0, len(opts)) 114 | for _, opt := range opts { 115 | if err := opt.apply(&config); err != nil { 116 | errs = append(errs, err) 117 | } 118 | } 119 | if len(errs) > 0 { 120 | return nil, errors.Join(errs...) 121 | } 122 | authToken := "" 123 | if config.authToken != nil { 124 | authToken = *config.authToken 125 | } 126 | readYourWrites := true 127 | if config.readYourWrites != nil { 128 | readYourWrites = *config.readYourWrites 129 | } 130 | encryptionKey := "" 131 | if config.encryptionKey != nil { 132 | encryptionKey = *config.encryptionKey 133 | } 134 | syncInterval := time.Duration(0) 135 | if config.syncInterval != nil { 136 | syncInterval = *config.syncInterval 137 | } 138 | return openSyncConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval, false) 139 | } 140 | 141 | func NewSyncedDatabaseConnector(dbPath string, primaryUrl string, opts ...Option) (*Connector, error) { 142 | var config config 143 | errs := make([]error, 0, len(opts)) 144 | for _, opt := range opts { 145 | if err := opt.apply(&config); err != nil { 146 | errs = append(errs, err) 147 | } 148 | } 149 | if len(errs) > 0 { 150 | return nil, errors.Join(errs...) 151 | } 152 | authToken := "" 153 | if config.authToken != nil { 154 | authToken = *config.authToken 155 | } 156 | readYourWrites := true 157 | if config.readYourWrites != nil { 158 | readYourWrites = *config.readYourWrites 159 | } 160 | encryptionKey := "" 161 | if config.encryptionKey != nil { 162 | encryptionKey = *config.encryptionKey 163 | } 164 | syncInterval := time.Duration(0) 165 | if config.syncInterval != nil { 166 | syncInterval = *config.syncInterval 167 | } 168 | return openSyncConnector(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, syncInterval, true) 169 | } 170 | 171 | type driver struct{} 172 | 173 | func (d driver) Open(dbAddress string) (sqldriver.Conn, error) { 174 | connector, err := d.OpenConnector(dbAddress) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return connector.Connect(context.Background()) 179 | } 180 | 181 | func (d driver) OpenConnector(dbAddress string) (sqldriver.Connector, error) { 182 | if strings.HasPrefix(dbAddress, ":memory:") { 183 | return openLocalConnector(dbAddress) 184 | } 185 | u, err := url.Parse(dbAddress) 186 | if err != nil { 187 | return nil, err 188 | } 189 | switch u.Scheme { 190 | case "file": 191 | return openLocalConnector(dbAddress) 192 | case "http": 193 | fallthrough 194 | case "https": 195 | fallthrough 196 | case "libsql": 197 | authToken := u.Query().Get("authToken") 198 | u.RawQuery = "" 199 | return openRemoteConnector(u.String(), authToken) 200 | } 201 | return nil, fmt.Errorf("unsupported URL scheme: %s\nThis driver supports only URLs that start with libsql://, file:, https:// or http://", u.Scheme) 202 | } 203 | 204 | func libsqlSync(nativeDbPtr C.libsql_database_t) (Replicated, error) { 205 | var errMsg *C.char 206 | var rep C.replicated 207 | statusCode := C.libsql_sync2(nativeDbPtr, &rep, &errMsg) 208 | if statusCode != 0 { 209 | return Replicated{0, 0}, libsqlError("failed to sync database ", statusCode, errMsg) 210 | } 211 | 212 | return Replicated{FrameNo: int(rep.frame_no), FramesSynced: int(rep.frames_synced)}, nil 213 | } 214 | 215 | func openLocalConnector(dbPath string) (*Connector, error) { 216 | nativeDbPtr, err := libsqlOpenLocal(dbPath) 217 | if err != nil { 218 | return nil, err 219 | } 220 | return &Connector{nativeDbPtr: nativeDbPtr}, nil 221 | } 222 | 223 | func openRemoteConnector(primaryUrl, authToken string) (*Connector, error) { 224 | nativeDbPtr, err := libsqlOpenRemote(primaryUrl, authToken) 225 | if err != nil { 226 | return nil, err 227 | } 228 | return &Connector{nativeDbPtr: nativeDbPtr}, nil 229 | } 230 | 231 | func openSyncConnector(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string, syncInterval time.Duration, offline bool) (*Connector, error) { 232 | var closeCh chan struct{} 233 | var closeAckCh chan struct{} 234 | nativeDbPtr, err := libsqlOpenWithSync(dbPath, primaryUrl, authToken, readYourWrites, encryptionKey, offline) 235 | if err != nil { 236 | return nil, err 237 | } 238 | if _, err := libsqlSync(nativeDbPtr); err != nil { 239 | C.libsql_close(nativeDbPtr) 240 | return nil, err 241 | } 242 | if syncInterval != 0 { 243 | closeCh = make(chan struct{}, 1) 244 | closeAckCh = make(chan struct{}, 1) 245 | go func() { 246 | for { 247 | timerCh := make(chan struct{}, 1) 248 | go func() { 249 | time.Sleep(syncInterval) 250 | timerCh <- struct{}{} 251 | }() 252 | select { 253 | case <-closeCh: 254 | closeAckCh <- struct{}{} 255 | return 256 | case <-timerCh: 257 | if _, err := libsqlSync(nativeDbPtr); err != nil { 258 | fmt.Println(err) 259 | } 260 | } 261 | } 262 | }() 263 | } 264 | return &Connector{nativeDbPtr: nativeDbPtr, closeCh: closeCh, closeAckCh: closeAckCh}, nil 265 | } 266 | 267 | type Connector struct { 268 | nativeDbPtr C.libsql_database_t 269 | closeCh chan<- struct{} 270 | closeAckCh <-chan struct{} 271 | } 272 | 273 | func (c *Connector) Sync() (Replicated, error) { 274 | return libsqlSync(c.nativeDbPtr) 275 | } 276 | 277 | func (c *Connector) Close() error { 278 | if c.closeCh != nil { 279 | c.closeCh <- struct{}{} 280 | <-c.closeAckCh 281 | c.closeCh = nil 282 | c.closeAckCh = nil 283 | } 284 | if c.nativeDbPtr != nil { 285 | C.libsql_close(c.nativeDbPtr) 286 | } 287 | c.nativeDbPtr = nil 288 | return nil 289 | } 290 | 291 | func (c *Connector) Connect(ctx context.Context) (sqldriver.Conn, error) { 292 | nativeConnPtr, err := libsqlConnect(c.nativeDbPtr) 293 | if err != nil { 294 | return nil, err 295 | } 296 | return &conn{nativePtr: nativeConnPtr}, nil 297 | } 298 | 299 | func (c *Connector) Driver() sqldriver.Driver { 300 | return driver{} 301 | } 302 | 303 | func libsqlError(message string, statusCode C.int, errMsg *C.char) error { 304 | code := int(statusCode) 305 | if errMsg != nil { 306 | msg := C.GoString(errMsg) 307 | C.libsql_free_string(errMsg) 308 | return fmt.Errorf("%s\nerror code = %d: %v", message, code, msg) 309 | } else { 310 | return fmt.Errorf("%s\nerror code = %d", message, code) 311 | } 312 | } 313 | 314 | func libsqlOpenLocal(dataSourceName string) (C.libsql_database_t, error) { 315 | connectionString := C.CString(dataSourceName) 316 | defer C.free(unsafe.Pointer(connectionString)) 317 | 318 | var db C.libsql_database_t 319 | var errMsg *C.char 320 | statusCode := C.libsql_open_file(connectionString, &db, &errMsg) 321 | if statusCode != 0 { 322 | return nil, libsqlError(fmt.Sprint("failed to open local database ", dataSourceName), statusCode, errMsg) 323 | } 324 | return db, nil 325 | } 326 | 327 | func libsqlOpenRemote(url, authToken string) (C.libsql_database_t, error) { 328 | connectionString := C.CString(url) 329 | defer C.free(unsafe.Pointer(connectionString)) 330 | authTokenNativeString := C.CString(authToken) 331 | defer C.free(unsafe.Pointer(authTokenNativeString)) 332 | 333 | var db C.libsql_database_t 334 | var errMsg *C.char 335 | statusCode := C.libsql_open_remote(connectionString, authTokenNativeString, &db, &errMsg) 336 | if statusCode != 0 { 337 | return nil, libsqlError(fmt.Sprint("failed to open remote database ", url), statusCode, errMsg) 338 | } 339 | return db, nil 340 | } 341 | 342 | func libsqlOpenWithSync(dbPath, primaryUrl, authToken string, readYourWrites bool, encryptionKey string, offline bool) (C.libsql_database_t, error) { 343 | dbPathNativeString := C.CString(dbPath) 344 | defer C.free(unsafe.Pointer(dbPathNativeString)) 345 | primaryUrlNativeString := C.CString(primaryUrl) 346 | defer C.free(unsafe.Pointer(primaryUrlNativeString)) 347 | authTokenNativeString := C.CString(authToken) 348 | defer C.free(unsafe.Pointer(authTokenNativeString)) 349 | 350 | var readYourWritesNative C.char = 0 351 | if readYourWrites { 352 | readYourWritesNative = 1 353 | } 354 | 355 | var offlineNative C.char = 0 356 | if offline { 357 | offlineNative = 1 358 | } 359 | 360 | var encrytionKeyNativeString *C.char 361 | if encryptionKey != "" { 362 | encrytionKeyNativeString = C.CString(encryptionKey) 363 | defer C.free(unsafe.Pointer(encrytionKeyNativeString)) 364 | } 365 | 366 | config := C.libsql_config{ 367 | db_path: dbPathNativeString, 368 | auth_token: authTokenNativeString, 369 | primary_url: primaryUrlNativeString, 370 | read_your_writes: readYourWritesNative, 371 | encryption_key: encrytionKeyNativeString, 372 | offline: offlineNative, 373 | } 374 | 375 | var db C.libsql_database_t 376 | var errMsg *C.char 377 | statusCode := C.libsql_open_sync_with_config(config, &db, &errMsg) 378 | if statusCode != 0 { 379 | return nil, libsqlError(fmt.Sprintf("failed to open database %s %s", dbPath, primaryUrl), statusCode, errMsg) 380 | } 381 | return db, nil 382 | } 383 | 384 | func libsqlConnect(db C.libsql_database_t) (C.libsql_connection_t, error) { 385 | var conn C.libsql_connection_t 386 | var errMsg *C.char 387 | statusCode := C.libsql_connect(db, &conn, &errMsg) 388 | if statusCode != 0 { 389 | return nil, libsqlError("failed to connect to database", statusCode, errMsg) 390 | } 391 | return conn, nil 392 | } 393 | 394 | type conn struct { 395 | nativePtr C.libsql_connection_t 396 | } 397 | 398 | func (c *conn) Prepare(query string) (sqldriver.Stmt, error) { 399 | return c.PrepareContext(context.Background(), query) 400 | } 401 | 402 | func (c *conn) Begin() (sqldriver.Tx, error) { 403 | return c.BeginTx(context.Background(), sqldriver.TxOptions{}) 404 | } 405 | 406 | func (c *conn) Close() error { 407 | C.libsql_disconnect(c.nativePtr) 408 | return nil 409 | } 410 | 411 | type ParamsInfo struct { 412 | NamedParameters []string 413 | PositionalParametersCount int 414 | } 415 | 416 | func isPositionalParameter(param string) (ok bool, err error) { 417 | re := regexp.MustCompile(`\?([0-9]*).*`) 418 | match := re.FindSubmatch([]byte(param)) 419 | if match == nil { 420 | return false, nil 421 | } 422 | 423 | posS := string(match[1]) 424 | if posS == "" { 425 | return true, nil 426 | } 427 | 428 | return true, fmt.Errorf("unsuppoted positional parameter. This driver does not accept positional parameters with indexes (like ?)") 429 | } 430 | 431 | func removeParamPrefix(paramName string) (string, error) { 432 | if paramName[0] == ':' || paramName[0] == '@' || paramName[0] == '$' { 433 | return paramName[1:], nil 434 | } 435 | return "", fmt.Errorf("all named parameters must start with ':', or '@' or '$'") 436 | } 437 | 438 | func extractParameters(stmt string) (nameParams []string, positionalParamsCount int, err error) { 439 | statementStream := antlr.NewInputStream(stmt) 440 | sqliteparser.NewSQLiteLexer(statementStream) 441 | lexer := sqliteparser.NewSQLiteLexer(statementStream) 442 | 443 | allTokens := lexer.GetAllTokens() 444 | 445 | nameParamsSet := make(map[string]bool) 446 | 447 | for _, token := range allTokens { 448 | tokenType := token.GetTokenType() 449 | if tokenType == sqliteparser.SQLiteLexerBIND_PARAMETER { 450 | parameter := token.GetText() 451 | 452 | isPositionalParameter, err := isPositionalParameter(parameter) 453 | if err != nil { 454 | return []string{}, 0, err 455 | } 456 | 457 | if isPositionalParameter { 458 | positionalParamsCount++ 459 | } else { 460 | paramWithoutPrefix, err := removeParamPrefix(parameter) 461 | if err != nil { 462 | return []string{}, 0, err 463 | } else { 464 | nameParamsSet[paramWithoutPrefix] = true 465 | } 466 | } 467 | } 468 | } 469 | nameParams = make([]string, 0, len(nameParamsSet)) 470 | for k := range nameParamsSet { 471 | nameParams = append(nameParams, k) 472 | } 473 | 474 | return nameParams, positionalParamsCount, nil 475 | } 476 | 477 | func parseStatement(sql string) ([]string, []ParamsInfo, error) { 478 | stmts, _ := sqliteparserutils.SplitStatement(sql) 479 | 480 | stmtsParams := make([]ParamsInfo, len(stmts)) 481 | for idx, stmt := range stmts { 482 | nameParams, positionalParamsCount, err := extractParameters(stmt) 483 | if err != nil { 484 | return nil, nil, err 485 | } 486 | stmtsParams[idx] = ParamsInfo{nameParams, positionalParamsCount} 487 | } 488 | return stmts, stmtsParams, nil 489 | } 490 | 491 | func (c *conn) PrepareContext(ctx context.Context, query string) (sqldriver.Stmt, error) { 492 | stmts, paramInfos, err := parseStatement(query) 493 | if err != nil { 494 | return nil, err 495 | } 496 | if len(stmts) != 1 { 497 | return nil, fmt.Errorf("only one statement is supported got %d", len(stmts)) 498 | } 499 | numInput := -1 500 | if len(paramInfos[0].NamedParameters) == 0 { 501 | numInput = paramInfos[0].PositionalParametersCount 502 | } 503 | return &stmt{c, query, numInput}, nil 504 | } 505 | 506 | func (c *conn) BeginTx(ctx context.Context, opts sqldriver.TxOptions) (sqldriver.Tx, error) { 507 | if opts.ReadOnly { 508 | return nil, fmt.Errorf("read only transactions are not supported") 509 | } 510 | if opts.Isolation != sqldriver.IsolationLevel(sql.LevelDefault) { 511 | return nil, fmt.Errorf("isolation level %d is not supported", opts.Isolation) 512 | } 513 | _, err := c.ExecContext(ctx, "BEGIN", nil) 514 | if err != nil { 515 | return nil, err 516 | } 517 | return &tx{c}, nil 518 | } 519 | 520 | func (c *conn) executeNoArgs(query string, exec bool) (C.libsql_rows_t, error) { 521 | queryCString := C.CString(query) 522 | defer C.free(unsafe.Pointer(queryCString)) 523 | 524 | var rows C.libsql_rows_t 525 | var errMsg *C.char 526 | var statusCode C.int 527 | if exec { 528 | statusCode = C.libsql_execute(c.nativePtr, queryCString, &errMsg) 529 | } else { 530 | statusCode = C.libsql_query(c.nativePtr, queryCString, &rows, &errMsg) 531 | } 532 | if statusCode != 0 { 533 | return nil, libsqlError(fmt.Sprint("failed to execute query ", query), statusCode, errMsg) 534 | } 535 | return rows, nil 536 | } 537 | 538 | func (c *conn) execute(query string, args []sqldriver.NamedValue, exec bool) (C.libsql_rows_t, error) { 539 | if len(args) == 0 { 540 | return c.executeNoArgs(query, exec) 541 | } 542 | queryCString := C.CString(query) 543 | defer C.free(unsafe.Pointer(queryCString)) 544 | 545 | var stmt C.libsql_stmt_t 546 | var errMsg *C.char 547 | statusCode := C.libsql_prepare(c.nativePtr, queryCString, &stmt, &errMsg) 548 | if statusCode != 0 { 549 | return nil, libsqlError(fmt.Sprint("failed to prepare query ", query), statusCode, errMsg) 550 | } 551 | defer C.libsql_free_stmt(stmt) 552 | 553 | for _, arg := range args { 554 | var errMsg *C.char 555 | var statusCode C.int 556 | idx := arg.Ordinal 557 | switch arg.Value.(type) { 558 | case int64: 559 | statusCode = C.libsql_bind_int(stmt, C.int(idx), C.longlong(arg.Value.(int64)), &errMsg) 560 | case float64: 561 | statusCode = C.libsql_bind_float(stmt, C.int(idx), C.double(arg.Value.(float64)), &errMsg) 562 | case []byte: 563 | blob := arg.Value.([]byte) 564 | nativeBlob := C.CBytes(blob) 565 | statusCode = C.libsql_bind_blob(stmt, C.int(idx), (*C.uchar)(nativeBlob), C.int(len(blob)), &errMsg) 566 | C.free(nativeBlob) 567 | case string: 568 | valueStr := C.CString(arg.Value.(string)) 569 | statusCode = C.libsql_bind_string(stmt, C.int(idx), valueStr, &errMsg) 570 | C.free(unsafe.Pointer(valueStr)) 571 | case nil: 572 | statusCode = C.libsql_bind_null(stmt, C.int(idx), &errMsg) 573 | case bool: 574 | var valueInt int 575 | if arg.Value.(bool) { 576 | valueInt = 1 577 | } else { 578 | valueInt = 0 579 | } 580 | statusCode = C.libsql_bind_int(stmt, C.int(idx), C.longlong(valueInt), &errMsg) 581 | case time.Time: 582 | valueStr := C.CString(arg.Value.(time.Time).Format(time.RFC3339Nano)) 583 | statusCode = C.libsql_bind_string(stmt, C.int(idx), valueStr, &errMsg) 584 | C.free(unsafe.Pointer(valueStr)) 585 | default: 586 | return nil, fmt.Errorf("unsupported type %T", arg.Value) 587 | } 588 | if statusCode != 0 { 589 | return nil, libsqlError(fmt.Sprintf("failed to bind argument no. %d with value %v and type %T", idx, arg.Value, arg.Value), statusCode, errMsg) 590 | } 591 | } 592 | 593 | var rows C.libsql_rows_t 594 | if exec { 595 | statusCode = C.libsql_execute_stmt(stmt, &errMsg) 596 | } else { 597 | statusCode = C.libsql_query_stmt(stmt, &rows, &errMsg) 598 | } 599 | if statusCode != 0 { 600 | return nil, libsqlError(fmt.Sprint("failed to execute query ", query), statusCode, errMsg) 601 | } 602 | return rows, nil 603 | } 604 | 605 | type execResult struct { 606 | id int64 607 | changes int64 608 | } 609 | 610 | func (r execResult) LastInsertId() (int64, error) { 611 | return r.id, nil 612 | } 613 | 614 | func (r execResult) RowsAffected() (int64, error) { 615 | return r.changes, nil 616 | } 617 | 618 | func (c *conn) ExecContext(ctx context.Context, query string, args []sqldriver.NamedValue) (sqldriver.Result, error) { 619 | rows, err := c.execute(query, args, true) 620 | if err != nil { 621 | return nil, err 622 | } 623 | id := int64(C.libsql_last_insert_rowid(c.nativePtr)) 624 | changes := int64(C.libsql_changes(c.nativePtr)) 625 | if rows != nil { 626 | C.libsql_free_rows(rows) 627 | } 628 | return execResult{id, changes}, nil 629 | } 630 | 631 | type stmt struct { 632 | conn *conn 633 | sql string 634 | numInput int 635 | } 636 | 637 | func (s *stmt) Close() error { 638 | return nil 639 | } 640 | 641 | func (s *stmt) NumInput() int { 642 | return s.numInput 643 | } 644 | 645 | func convertToNamed(args []sqldriver.Value) []sqldriver.NamedValue { 646 | if len(args) == 0 { 647 | return nil 648 | } 649 | result := make([]sqldriver.NamedValue, 0, len(args)) 650 | for idx := range args { 651 | result = append(result, sqldriver.NamedValue{Ordinal: idx, Value: args[idx]}) 652 | } 653 | return result 654 | } 655 | 656 | func (s *stmt) Exec(args []sqldriver.Value) (sqldriver.Result, error) { 657 | return s.ExecContext(context.Background(), convertToNamed(args)) 658 | } 659 | 660 | func (s *stmt) Query(args []sqldriver.Value) (sqldriver.Rows, error) { 661 | return s.QueryContext(context.Background(), convertToNamed(args)) 662 | } 663 | 664 | func (s *stmt) ExecContext(ctx context.Context, args []sqldriver.NamedValue) (sqldriver.Result, error) { 665 | return s.conn.ExecContext(ctx, s.sql, args) 666 | } 667 | 668 | func (s *stmt) QueryContext(ctx context.Context, args []sqldriver.NamedValue) (sqldriver.Rows, error) { 669 | return s.conn.QueryContext(ctx, s.sql, args) 670 | } 671 | 672 | type tx struct { 673 | conn *conn 674 | } 675 | 676 | func (t tx) Commit() error { 677 | _, err := t.conn.ExecContext(context.Background(), "COMMIT", nil) 678 | return err 679 | } 680 | 681 | func (t tx) Rollback() error { 682 | _, err := t.conn.ExecContext(context.Background(), "ROLLBACK", nil) 683 | return err 684 | } 685 | 686 | const ( 687 | TYPE_INT int = iota + 1 688 | TYPE_FLOAT 689 | TYPE_TEXT 690 | TYPE_BLOB 691 | TYPE_NULL 692 | ) 693 | 694 | func newRows(nativePtr C.libsql_rows_t) (*rows, error) { 695 | if nativePtr == nil { 696 | return &rows{nil, nil}, nil 697 | } 698 | columnCount := int(C.libsql_column_count(nativePtr)) 699 | columns := make([]string, columnCount) 700 | for i := 0; i < columnCount; i++ { 701 | var ptr *C.char 702 | var errMsg *C.char 703 | statusCode := C.libsql_column_name(nativePtr, C.int(i), &ptr, &errMsg) 704 | if statusCode != 0 { 705 | return nil, libsqlError(fmt.Sprint("failed to get column name for index ", i), statusCode, errMsg) 706 | } 707 | columns[i] = C.GoString(ptr) 708 | C.libsql_free_string(ptr) 709 | } 710 | return &rows{nativePtr, columns}, nil 711 | } 712 | 713 | type rows struct { 714 | nativePtr C.libsql_rows_t 715 | columnNames []string 716 | } 717 | 718 | func (r *rows) Columns() []string { 719 | return r.columnNames 720 | } 721 | 722 | func (r *rows) Close() error { 723 | if r.nativePtr != nil { 724 | C.libsql_free_rows(r.nativePtr) 725 | r.nativePtr = nil 726 | } 727 | return nil 728 | } 729 | 730 | func (r *rows) Next(dest []sqldriver.Value) error { 731 | if r.nativePtr == nil { 732 | return io.EOF 733 | } 734 | var row C.libsql_row_t 735 | var errMsg *C.char 736 | statusCode := C.libsql_next_row(r.nativePtr, &row, &errMsg) 737 | if statusCode != 0 { 738 | return libsqlError("failed to get next row", statusCode, errMsg) 739 | } 740 | if row == nil { 741 | r.Close() 742 | return io.EOF 743 | } 744 | defer C.libsql_free_row(row) 745 | count := len(dest) 746 | if count > len(r.columnNames) { 747 | count = len(r.columnNames) 748 | } 749 | 750 | Outerloop: 751 | for i := 0; i < count; i++ { 752 | var columnType C.int 753 | var errMsg *C.char 754 | statusCode := C.libsql_column_type(r.nativePtr, row, C.int(i), &columnType, &errMsg) 755 | if statusCode != 0 { 756 | return libsqlError(fmt.Sprint("failed to get column type for index ", i), statusCode, errMsg) 757 | } 758 | 759 | switch int(columnType) { 760 | case TYPE_NULL: 761 | dest[i] = nil 762 | case TYPE_INT: 763 | var value C.longlong 764 | var errMsg *C.char 765 | statusCode := C.libsql_get_int(row, C.int(i), &value, &errMsg) 766 | if statusCode != 0 { 767 | return libsqlError(fmt.Sprint("failed to get integer for column ", i), statusCode, errMsg) 768 | } 769 | dest[i] = int64(value) 770 | case TYPE_FLOAT: 771 | var value C.double 772 | var errMsg *C.char 773 | statusCode := C.libsql_get_float(row, C.int(i), &value, &errMsg) 774 | if statusCode != 0 { 775 | return libsqlError(fmt.Sprint("failed to get float for column ", i), statusCode, errMsg) 776 | } 777 | dest[i] = float64(value) 778 | case TYPE_BLOB: 779 | var nativeBlob C.blob 780 | var errMsg *C.char 781 | statusCode := C.libsql_get_blob(row, C.int(i), &nativeBlob, &errMsg) 782 | if statusCode != 0 { 783 | return libsqlError(fmt.Sprint("failed to get blob for column ", i), statusCode, errMsg) 784 | } 785 | dest[i] = C.GoBytes(unsafe.Pointer(nativeBlob.ptr), C.int(nativeBlob.len)) 786 | C.libsql_free_blob(nativeBlob) 787 | case TYPE_TEXT: 788 | var ptr *C.char 789 | var errMsg *C.char 790 | statusCode := C.libsql_get_string(row, C.int(i), &ptr, &errMsg) 791 | if statusCode != 0 { 792 | return libsqlError(fmt.Sprint("failed to get string for column ", i), statusCode, errMsg) 793 | } 794 | str := C.GoString(ptr) 795 | C.libsql_free_string(ptr) 796 | for _, format := range []string{ 797 | time.RFC3339Nano, 798 | "2006-01-02 15:04:05.999999999-07:00", 799 | "2006-01-02T15:04:05.999999999-07:00", 800 | "2006-01-02 15:04:05.999999999", 801 | "2006-01-02T15:04:05.999999999", 802 | "2006-01-02 15:04:05", 803 | "2006-01-02T15:04:05", 804 | "2006-01-02 15:04", 805 | "2006-01-02T15:04", 806 | "2006-01-02", 807 | } { 808 | if t, err := time.ParseInLocation(format, str, time.UTC); err == nil { 809 | dest[i] = t 810 | continue Outerloop 811 | } 812 | } 813 | dest[i] = str 814 | } 815 | } 816 | return nil 817 | } 818 | 819 | func (c *conn) QueryContext(ctx context.Context, query string, args []sqldriver.NamedValue) (sqldriver.Rows, error) { 820 | rowsNativePtr, err := c.execute(query, args, false) 821 | if err != nil { 822 | return nil, err 823 | } 824 | return newRows(rowsNativePtr) 825 | } 826 | -------------------------------------------------------------------------------- /libsql_test.go: -------------------------------------------------------------------------------- 1 | package libsql 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "gotest.tools/assert" 11 | "io" 12 | "math/rand" 13 | "net/http" 14 | "os" 15 | "runtime/debug" 16 | "strings" 17 | "testing" 18 | "time" 19 | 20 | "golang.org/x/sync/errgroup" 21 | ) 22 | 23 | type T struct { 24 | *testing.T 25 | } 26 | 27 | func (t T) FatalWithMsg(msg string) { 28 | t.Log(string(debug.Stack())) 29 | t.Fatal(msg) 30 | } 31 | 32 | func (t T) FatalOnError(err error) { 33 | if err != nil { 34 | t.Log(string(debug.Stack())) 35 | t.Fatal(err) 36 | } 37 | } 38 | 39 | func getRemoteDb(t T) *Database { 40 | primaryUrl := os.Getenv("LIBSQL_PRIMARY_URL") 41 | if primaryUrl == "" { 42 | t.Skip("LIBSQL_PRIMARY_URL is not set") 43 | return nil 44 | } 45 | authToken := os.Getenv("LIBSQL_AUTH_TOKEN") 46 | db, err := sql.Open("libsql", primaryUrl+"?authToken="+authToken) 47 | t.FatalOnError(err) 48 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 49 | t.Cleanup(func() { 50 | db.Close() 51 | cancel() 52 | }) 53 | return &Database{db, nil, t, ctx} 54 | } 55 | 56 | func getEmbeddedDb(t T) *Database { 57 | primaryUrl := os.Getenv("LIBSQL_PRIMARY_URL") 58 | if primaryUrl == "" { 59 | t.Skip("LIBSQL_PRIMARY_URL is not set") 60 | return nil 61 | } 62 | authToken := os.Getenv("LIBSQL_AUTH_TOKEN") 63 | dir, err := os.MkdirTemp("", "libsql-*") 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | dbPath := dir + "/test.db" 68 | options := []Option{WithReadYourWrites(false)} 69 | if authToken != "" { 70 | options = append(options, WithAuthToken(authToken)) 71 | } 72 | connector, err := NewEmbeddedReplicaConnector(dbPath, primaryUrl, options...) 73 | t.FatalOnError(err) 74 | db := sql.OpenDB(connector) 75 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 76 | t.Cleanup(func() { 77 | db.Close() 78 | connector.Close() 79 | cancel() 80 | defer os.RemoveAll(dir) 81 | }) 82 | return &Database{db, connector, t, ctx} 83 | } 84 | 85 | type Database struct { 86 | *sql.DB 87 | connector *Connector 88 | t T 89 | ctx context.Context 90 | } 91 | 92 | func (db Database) exec(sql string, args ...any) sql.Result { 93 | res, err := db.ExecContext(db.ctx, sql, args...) 94 | db.t.FatalOnError(err) 95 | return res 96 | } 97 | 98 | func (db Database) query(sql string, args ...any) *sql.Rows { 99 | rows, err := db.QueryContext(db.ctx, sql, args...) 100 | db.t.FatalOnError(err) 101 | return rows 102 | } 103 | 104 | func (db Database) sync() { 105 | if db.connector != nil { 106 | db.connector.Sync() 107 | } 108 | } 109 | 110 | type Table struct { 111 | name string 112 | db Database 113 | } 114 | 115 | func (db Database) createTable() Table { 116 | name := "test_" + fmt.Sprint(rand.Int()) + "_" + time.Now().Format("20060102150405") 117 | db.exec("CREATE TABLE " + name + " (a int, b int)") 118 | db.t.Cleanup(func() { 119 | db.exec("DROP TABLE " + name) 120 | }) 121 | return Table{name, db} 122 | } 123 | 124 | func (db Database) assertTable(name string) { 125 | rows, err := db.QueryContext(db.ctx, "select 1 from "+name) 126 | db.t.FatalOnError(err) 127 | defer rows.Close() 128 | } 129 | 130 | func (t Table) insertRows(start, count int) { 131 | t.insertRowsInternal(start, count, func(i int) sql.Result { 132 | return t.db.exec("INSERT INTO " + t.name + " (a, b) VALUES (" + fmt.Sprint(i) + ", " + fmt.Sprint(i) + ")") 133 | }) 134 | } 135 | 136 | func (t Table) insertRowsWithArgs(start, count int) { 137 | t.insertRowsInternal(start, count, func(i int) sql.Result { 138 | return t.db.exec("INSERT INTO "+t.name+" (a, b) VALUES (?, ?)", i, i) 139 | }) 140 | } 141 | 142 | func (t Table) insertRowsInternal(start, count int, execFn func(i int) sql.Result) { 143 | for i := 0; i < count; i++ { 144 | execFn(i + start) 145 | //Uncomment once RowsAffected is implemented in libsql for remote only dbs 146 | //res := execFn(i + start) 147 | //affected, err := res.RowsAffected() 148 | //t.db.t.FatalOnError(err) 149 | //if affected != 1 { 150 | // t.db.t.FatalWithMsg("expected 1 row affected") 151 | //} 152 | } 153 | } 154 | 155 | func (t Table) assertRowsCount(count int) { 156 | t.assertCount(count, func() *sql.Rows { 157 | return t.db.query("SELECT COUNT(*) FROM " + t.name) 158 | }) 159 | } 160 | 161 | func (t Table) assertRowDoesNotExist(id int) { 162 | t.assertCount(0, func() *sql.Rows { 163 | return t.db.query("SELECT COUNT(*) FROM "+t.name+" WHERE a = ?", id) 164 | }) 165 | } 166 | 167 | func (t Table) assertRowExists(id int) { 168 | t.assertCount(1, func() *sql.Rows { 169 | return t.db.query("SELECT COUNT(*) FROM "+t.name+" WHERE a = ?", id) 170 | }) 171 | } 172 | 173 | func (t Table) assertCount(expectedCount int, queryFn func() *sql.Rows) { 174 | rows := queryFn() 175 | defer rows.Close() 176 | if !rows.Next() { 177 | t.db.t.FatalWithMsg(fmt.Sprintf("expected at least one row: %v", rows.Err())) 178 | } 179 | var rowCount int 180 | t.db.t.FatalOnError(rows.Scan(&rowCount)) 181 | if rowCount != expectedCount { 182 | t.db.t.FatalWithMsg(fmt.Sprintf("expected %d rows, got %d", expectedCount, rowCount)) 183 | } 184 | } 185 | 186 | func (t Table) beginTx() Tx { 187 | tx, err := t.db.BeginTx(t.db.ctx, nil) 188 | t.db.t.FatalOnError(err) 189 | return Tx{tx, t, nil} 190 | } 191 | 192 | func (t Table) beginTxWithContext(ctx context.Context) Tx { 193 | tx, err := t.db.BeginTx(ctx, nil) 194 | t.db.t.FatalOnError(err) 195 | return Tx{tx, t, &ctx} 196 | } 197 | 198 | func (t Table) prepareInsertStmt() PreparedStmt { 199 | stmt, err := t.db.Prepare("INSERT INTO " + t.name + " (a, b) VALUES (?, ?)") 200 | t.db.t.FatalOnError(err) 201 | return PreparedStmt{stmt, t} 202 | } 203 | 204 | type PreparedStmt struct { 205 | *sql.Stmt 206 | t Table 207 | } 208 | 209 | func (s PreparedStmt) exec(args ...any) sql.Result { 210 | res, err := s.ExecContext(s.t.db.ctx, args...) 211 | s.t.db.t.FatalOnError(err) 212 | return res 213 | } 214 | 215 | type Tx struct { 216 | *sql.Tx 217 | t Table 218 | ctx *context.Context 219 | } 220 | 221 | func (t Tx) context() context.Context { 222 | if t.ctx != nil { 223 | return *t.ctx 224 | } 225 | return t.t.db.ctx 226 | } 227 | 228 | func (t Tx) exec(sql string, args ...any) sql.Result { 229 | res, err := t.ExecContext(t.context(), sql, args...) 230 | t.t.db.t.FatalOnError(err) 231 | return res 232 | } 233 | 234 | func (t Tx) query(sql string, args ...any) *sql.Rows { 235 | rows, err := t.QueryContext(t.context(), sql, args...) 236 | t.t.db.t.FatalOnError(err) 237 | return rows 238 | } 239 | 240 | func (t Tx) insertRows(start, count int) { 241 | t.t.insertRowsInternal(start, count, func(i int) sql.Result { 242 | return t.exec("INSERT INTO " + t.t.name + " (a, b) VALUES (" + fmt.Sprint(i) + ", '" + fmt.Sprint(i) + "')") 243 | }) 244 | } 245 | 246 | func (t Tx) insertRowsWithArgs(start, count int) { 247 | t.t.insertRowsInternal(start, count, func(i int) sql.Result { 248 | return t.exec("INSERT INTO "+t.t.name+" (a, b) VALUES (?, ?)", i, fmt.Sprint(i)) 249 | }) 250 | } 251 | 252 | func (t Tx) assertRowsCount(count int) { 253 | t.t.assertCount(count, func() *sql.Rows { 254 | return t.query("SELECT COUNT(*) FROM " + t.t.name) 255 | }) 256 | } 257 | 258 | func (t Tx) assertRowDoesNotExist(id int) { 259 | t.t.assertCount(0, func() *sql.Rows { 260 | return t.query("SELECT COUNT(*) FROM "+t.t.name+" WHERE a = ?", id) 261 | }) 262 | } 263 | 264 | func (t Tx) assertRowExists(id int) { 265 | t.t.assertCount(1, func() *sql.Rows { 266 | return t.query("SELECT COUNT(*) FROM "+t.t.name+" WHERE a = ?", id) 267 | }) 268 | } 269 | 270 | func (t Tx) prepareInsertStmt() PreparedStmt { 271 | stmt, err := t.Prepare("INSERT INTO " + t.t.name + " (a, b) VALUES (?, ?)") 272 | t.t.db.t.FatalOnError(err) 273 | return PreparedStmt{stmt, t.t} 274 | } 275 | 276 | func executeSql(t *testing.T, primaryUrl, authToken, sql string) { 277 | type statement struct { 278 | Query string `json:"q"` 279 | } 280 | type postBody struct { 281 | Statements []statement `json:"statements"` 282 | } 283 | 284 | type resultSet struct { 285 | Columns []string `json:"columns"` 286 | } 287 | 288 | type httpErrObject struct { 289 | Message string `json:"message"` 290 | } 291 | 292 | type httpResults struct { 293 | Results *resultSet `json:"results"` 294 | Error *httpErrObject `json:"error"` 295 | } 296 | 297 | type httpResultsAlternative struct { 298 | Results *resultSet `json:"results"` 299 | Error string `json:"error"` 300 | } 301 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 302 | defer cancel() 303 | 304 | rawReq := postBody{} 305 | 306 | rawReq.Statements = append(rawReq.Statements, statement{Query: sql}) 307 | 308 | body, err := json.Marshal(rawReq) 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | req, err := http.NewRequestWithContext(ctx, "POST", primaryUrl+"", bytes.NewReader(body)) 313 | if err != nil { 314 | t.Fatal(err) 315 | } 316 | req.Header.Set("Content-Type", "application/json") 317 | 318 | if authToken != "" { 319 | req.Header.Set("Authorization", "Bearer "+authToken) 320 | } 321 | 322 | resp, err := http.DefaultClient.Do(req) 323 | if err != nil { 324 | t.Fatal(err) 325 | } 326 | defer resp.Body.Close() 327 | body, err = io.ReadAll(resp.Body) 328 | if err != nil { 329 | t.Fatal(err) 330 | } 331 | if resp.StatusCode != http.StatusOK { 332 | t.Fatal("unexpected status code: ", resp.StatusCode) 333 | } 334 | var results []httpResults 335 | 336 | err = json.Unmarshal(body, &results) 337 | if err != nil { 338 | var alternativeResults []httpResultsAlternative 339 | errArray := json.Unmarshal(body, &alternativeResults) 340 | if errArray != nil { 341 | t.Fatal("failed to unmarshal response: ", err, errArray) 342 | } 343 | if alternativeResults[0].Error != "" { 344 | t.Fatal(errors.New(alternativeResults[0].Error)) 345 | } 346 | } else { 347 | if results[0].Error != nil { 348 | t.Fatal(errors.New(results[0].Error.Message)) 349 | } 350 | if results[0].Results == nil { 351 | t.Fatal(errors.New("no results")) 352 | } 353 | } 354 | } 355 | 356 | func insertRow(t *testing.T, dbUrl, authToken, tableName string, id int) { 357 | executeSql(t, dbUrl, authToken, fmt.Sprintf("INSERT INTO %s (id, name, gpa, cv) VALUES (%d, '%d', %d.5, randomblob(10));", tableName, id, id, id)) 358 | } 359 | 360 | func insertRows(t *testing.T, dbUrl, authToken, tableName string, start, count int) { 361 | for i := 0; i < count; i++ { 362 | insertRow(t, dbUrl, authToken, tableName, start+i) 363 | } 364 | } 365 | 366 | func createTable(t *testing.T, dbPath, authToken string) string { 367 | tableName := fmt.Sprintf("test_%d", time.Now().UnixNano()) 368 | executeSql(t, dbPath, authToken, fmt.Sprintf("CREATE TABLE %s (id INTEGER, name TEXT, gpa REAL, cv BLOB);", tableName)) 369 | return tableName 370 | } 371 | 372 | func removeTable(t *testing.T, dbPath, authToken, tableName string) { 373 | executeSql(t, dbPath, authToken, fmt.Sprintf("DROP TABLE %s;", tableName)) 374 | } 375 | 376 | func testSync(t *testing.T, connect func(dbPath, primaryUrl, authToken string) *Connector, sync func(connector *Connector)) { 377 | primaryUrl := os.Getenv("LIBSQL_PRIMARY_URL") 378 | if primaryUrl == "" { 379 | t.Skip("LIBSQL_PRIMARY_URL is not set") 380 | return 381 | } 382 | authToken := os.Getenv("LIBSQL_AUTH_TOKEN") 383 | tableName := createTable(t, primaryUrl, authToken) 384 | defer removeTable(t, primaryUrl, authToken, tableName) 385 | 386 | initialRowsCount := 5 387 | insertRows(t, primaryUrl, authToken, tableName, 0, initialRowsCount) 388 | dir, err := os.MkdirTemp("", "libsql-*") 389 | if err != nil { 390 | t.Fatal(err) 391 | } 392 | defer os.RemoveAll(dir) 393 | 394 | connector := connect(dir+"/test.db", primaryUrl, authToken) 395 | db := sql.OpenDB(connector) 396 | defer db.Close() 397 | 398 | iterCount := 2 399 | for iter := 0; iter < iterCount; iter++ { 400 | func() { 401 | rows, err := db.QueryContext(context.Background(), "SELECT NULL, id, name, gpa, cv FROM "+tableName) 402 | if err != nil { 403 | t.Fatal(err) 404 | } 405 | columns, err := rows.Columns() 406 | if err != nil { 407 | t.Fatal(err) 408 | } 409 | assert.DeepEqual(t, columns, []string{"NULL", "id", "name", "gpa", "cv"}) 410 | types, err := rows.ColumnTypes() 411 | if err != nil { 412 | t.Fatal(err) 413 | } 414 | if len(types) != 5 { 415 | t.Fatal("types should be 5") 416 | } 417 | defer rows.Close() 418 | idx := 0 419 | for rows.Next() { 420 | if idx > initialRowsCount+iter { 421 | t.Fatal("idx should be <= ", initialRowsCount+iter) 422 | } 423 | var null any 424 | var id int 425 | var name string 426 | var gpa float64 427 | var cv []byte 428 | if err := rows.Scan(&null, &id, &name, &gpa, &cv); err != nil { 429 | t.Fatal(err) 430 | } 431 | if null != nil { 432 | t.Fatal("null should be nil") 433 | } 434 | if id != int(idx) { 435 | t.Fatal("id should be ", idx, " got ", id) 436 | } 437 | if name != fmt.Sprint(idx) { 438 | t.Fatal("name should be", idx) 439 | } 440 | if gpa != float64(idx)+0.5 { 441 | t.Fatal("gpa should be", float64(idx)+0.5) 442 | } 443 | if len(cv) != 10 { 444 | t.Fatal("cv should be 10 bytes") 445 | } 446 | idx++ 447 | } 448 | if idx != initialRowsCount+iter { 449 | t.Fatal("idx should be ", initialRowsCount+iter, " got ", idx) 450 | } 451 | }() 452 | if iter+1 != iterCount { 453 | insertRow(t, primaryUrl, authToken, tableName, initialRowsCount+iter) 454 | sync(connector) 455 | } 456 | } 457 | } 458 | 459 | func TestAutoSync(t *testing.T) { 460 | syncInterval := 1 * time.Second 461 | testSync(t, func(dbPath, primaryUrl, authToken string) *Connector { 462 | options := []Option{WithReadYourWrites(false), WithSyncInterval(syncInterval)} 463 | if authToken != "" { 464 | options = append(options, WithAuthToken(authToken)) 465 | } 466 | connector, err := NewEmbeddedReplicaConnector(dbPath, primaryUrl, options...) 467 | if err != nil { 468 | t.Fatal(err) 469 | } 470 | return connector 471 | }, func(_ *Connector) { 472 | time.Sleep(2 * syncInterval) 473 | }) 474 | } 475 | 476 | func TestSync(t *testing.T) { 477 | testSync(t, func(dbPath, primaryUrl, authToken string) *Connector { 478 | options := []Option{WithReadYourWrites(false)} 479 | if authToken != "" { 480 | options = append(options, WithAuthToken(authToken)) 481 | } 482 | connector, err := NewEmbeddedReplicaConnector(dbPath, primaryUrl, options...) 483 | if err != nil { 484 | t.Fatal(err) 485 | } 486 | return connector 487 | }, func(c *Connector) { 488 | if _, err := c.Sync(); err != nil { 489 | t.Fatal(err) 490 | } 491 | }) 492 | } 493 | 494 | func TestEncryption(tt *testing.T) { 495 | t := T{tt} 496 | primaryUrl := os.Getenv("LIBSQL_PRIMARY_URL") 497 | if primaryUrl == "" { 498 | t.Skip("LIBSQL_PRIMARY_URL is not set") 499 | return 500 | } 501 | authToken := os.Getenv("LIBSQL_AUTH_TOKEN") 502 | dir, err := os.MkdirTemp("", "libsql-*") 503 | if err != nil { 504 | t.Fatal(err) 505 | } 506 | dbPath := dir + "/test.db" 507 | t.Cleanup(func() { 508 | defer os.RemoveAll(dir) 509 | }) 510 | 511 | encryptionKey := "SuperSecretKey" 512 | table := "test_" + fmt.Sprint(rand.Int()) + "_" + time.Now().Format("20060102150405") 513 | 514 | options := []Option{WithReadYourWrites(false)} 515 | if authToken != "" { 516 | options = append(options, WithAuthToken(authToken)) 517 | } 518 | connector, err := NewEmbeddedReplicaConnector(dbPath, primaryUrl, append(options, WithEncryption(encryptionKey))...) 519 | t.FatalOnError(err) 520 | db := sql.OpenDB(connector) 521 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 522 | _, err = db.ExecContext(ctx, "CREATE TABLE "+table+" (id INTEGER PRIMARY KEY, name TEXT)") 523 | if err != nil { 524 | cancel() 525 | db.Close() 526 | connector.Close() 527 | t.FatalOnError(err) 528 | } 529 | _, err = db.ExecContext(ctx, "INSERT INTO "+table+" (id, name) VALUES (1, 'hello')") 530 | if err != nil { 531 | cancel() 532 | db.Close() 533 | connector.Close() 534 | t.FatalOnError(err) 535 | } 536 | err = db.Close() 537 | t.FatalOnError(err) 538 | err = connector.Close() 539 | t.FatalOnError(err) 540 | connector, err = NewEmbeddedReplicaConnector(dbPath, primaryUrl, append(options, WithEncryption(encryptionKey))...) 541 | t.FatalOnError(err) 542 | db = sql.OpenDB(connector) 543 | rows, err := db.QueryContext(ctx, "SELECT * FROM "+table) 544 | if err != nil { 545 | cancel() 546 | db.Close() 547 | connector.Close() 548 | t.FatalOnError(err) 549 | } 550 | defer rows.Close() 551 | if !rows.Next() { 552 | cancel() 553 | db.Close() 554 | connector.Close() 555 | t.Fatal("expected one row") 556 | } 557 | var id int 558 | var name string 559 | err = rows.Scan(&id, &name) 560 | if err != nil { 561 | cancel() 562 | db.Close() 563 | connector.Close() 564 | t.FatalOnError(err) 565 | } 566 | if id != 1 { 567 | cancel() 568 | db.Close() 569 | connector.Close() 570 | t.Fatal("id should be 1") 571 | } 572 | if name != "hello" { 573 | cancel() 574 | db.Close() 575 | connector.Close() 576 | t.Fatal("name should be hello") 577 | } 578 | err = rows.Close() 579 | t.FatalOnError(err) 580 | err = db.Close() 581 | t.FatalOnError(err) 582 | err = connector.Close() 583 | t.FatalOnError(err) 584 | connector, err = NewEmbeddedReplicaConnector(dbPath, primaryUrl, append(options, WithEncryption("WrongKey"))...) 585 | if err == nil { 586 | t.Fatal("using wrong encryption key should have failed") 587 | } 588 | if !strings.Contains(err.Error(), "SQLite error: file is not a database") { 589 | t.Fatal("using wrong encryption key should have failed with a different error") 590 | } 591 | } 592 | 593 | func TestExecAndQuery(t *testing.T) { 594 | db := getRemoteDb(T{t}) 595 | testExecAndQuery(db) 596 | } 597 | 598 | func TestExecAndQueryEmbedded(t *testing.T) { 599 | db := getEmbeddedDb(T{t}) 600 | testExecAndQuery(db) 601 | } 602 | 603 | func testExecAndQuery(db *Database) { 604 | if db == nil { 605 | return 606 | } 607 | table := db.createTable() 608 | table.insertRows(0, 10) 609 | table.insertRowsWithArgs(10, 10) 610 | db.sync() 611 | table.assertRowsCount(20) 612 | table.assertRowDoesNotExist(20) 613 | table.assertRowExists(0) 614 | table.assertRowExists(19) 615 | } 616 | 617 | func TestReadYourWrites(tt *testing.T) { 618 | t := T{tt} 619 | primaryUrl := os.Getenv("LIBSQL_PRIMARY_URL") 620 | if primaryUrl == "" { 621 | t.Skip("LIBSQL_PRIMARY_URL is not set") 622 | return 623 | } 624 | authToken := os.Getenv("LIBSQL_AUTH_TOKEN") 625 | dir, err := os.MkdirTemp("", "libsql-*") 626 | if err != nil { 627 | t.Fatal(err) 628 | } 629 | dbPath := dir + "/test.db" 630 | options := []Option{} 631 | if authToken != "" { 632 | options = append(options, WithAuthToken(authToken)) 633 | } 634 | connector, err := NewEmbeddedReplicaConnector(dbPath, primaryUrl, options...) 635 | t.FatalOnError(err) 636 | database := sql.OpenDB(connector) 637 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 638 | t.Cleanup(func() { 639 | database.Close() 640 | connector.Close() 641 | cancel() 642 | defer os.RemoveAll(dir) 643 | }) 644 | db := &Database{database, connector, t, ctx} 645 | table := db.createTable() 646 | table.insertRows(0, 10) 647 | table.insertRowsWithArgs(10, 10) 648 | table.assertRowsCount(20) 649 | table.assertRowDoesNotExist(20) 650 | table.assertRowExists(0) 651 | table.assertRowExists(19) 652 | } 653 | 654 | func TestPreparedStatements(t *testing.T) { 655 | db := getRemoteDb(T{t}) 656 | testPreparedStatements(db) 657 | } 658 | 659 | func TestPreparedStatementsEmbedded(t *testing.T) { 660 | db := getEmbeddedDb(T{t}) 661 | testPreparedStatements(db) 662 | } 663 | 664 | func testPreparedStatements(db *Database) { 665 | if db == nil { 666 | return 667 | } 668 | table := db.createTable() 669 | stmt := table.prepareInsertStmt() 670 | stmt.exec(1, "1") 671 | db.t.FatalOnError(stmt.Close()) 672 | db.sync() 673 | table.assertRowsCount(1) 674 | table.assertRowExists(1) 675 | } 676 | 677 | func TestTransaction(t *testing.T) { 678 | db := getRemoteDb(T{t}) 679 | testTransaction(db) 680 | } 681 | 682 | func TestTransactionEmbedded(t *testing.T) { 683 | db := getEmbeddedDb(T{t}) 684 | testTransaction(db) 685 | } 686 | 687 | func testTransaction(db *Database) { 688 | if db == nil { 689 | return 690 | } 691 | table := db.createTable() 692 | tx := table.beginTx() 693 | tx.insertRows(0, 10) 694 | tx.insertRowsWithArgs(10, 10) 695 | tx.assertRowsCount(20) 696 | tx.assertRowDoesNotExist(20) 697 | tx.assertRowExists(0) 698 | tx.assertRowExists(19) 699 | db.t.FatalOnError(tx.Commit()) 700 | db.sync() 701 | table.assertRowsCount(20) 702 | table.assertRowDoesNotExist(20) 703 | table.assertRowExists(0) 704 | table.assertRowExists(19) 705 | } 706 | 707 | func TestMultiLineStatement(t *testing.T) { 708 | t.Skip("Make it work") 709 | db := getRemoteDb(T{t}) 710 | if db == nil { 711 | return 712 | } 713 | db.exec("CREATE TABLE IF NOT EXISTS my_table (my_data TEXT); INSERT INTO my_table (my_data) VALUES ('hello');") 714 | t.Cleanup(func() { 715 | db.exec("DROP TABLE my_table") 716 | }) 717 | table := Table{"my_table", *db} 718 | db.assertTable("my_table") 719 | table.assertRowsCount(1) 720 | } 721 | 722 | func TestPreparedStatementInTransaction(t *testing.T) { 723 | db := getRemoteDb(T{t}) 724 | testPreparedStatementInTransaction(db) 725 | } 726 | 727 | func TestPreparedStatementInTransactionEmbedded(t *testing.T) { 728 | db := getEmbeddedDb(T{t}) 729 | testPreparedStatementInTransaction(db) 730 | } 731 | 732 | func testPreparedStatementInTransaction(db *Database) { 733 | if db == nil { 734 | return 735 | } 736 | table := db.createTable() 737 | tx := table.beginTx() 738 | stmt := tx.prepareInsertStmt() 739 | stmt.exec(1, "1") 740 | db.t.FatalOnError(stmt.Close()) 741 | tx.assertRowsCount(1) 742 | tx.assertRowExists(1) 743 | db.t.FatalOnError(tx.Commit()) 744 | db.sync() 745 | table.assertRowsCount(1) 746 | table.assertRowExists(1) 747 | } 748 | 749 | func TestPreparedStatementInTransactionRollback(t *testing.T) { 750 | db := getRemoteDb(T{t}) 751 | testPreparedStatementInTransactionRollback(db) 752 | } 753 | 754 | func TestPreparedStatementInTransactionRollbackEmbedded(t *testing.T) { 755 | db := getEmbeddedDb(T{t}) 756 | testPreparedStatementInTransactionRollback(db) 757 | } 758 | 759 | func testPreparedStatementInTransactionRollback(db *Database) { 760 | if db == nil { 761 | return 762 | } 763 | table := db.createTable() 764 | tx := table.beginTx() 765 | stmt := tx.prepareInsertStmt() 766 | stmt.exec(1, "1") 767 | db.t.FatalOnError(stmt.Close()) 768 | tx.assertRowsCount(1) 769 | tx.assertRowExists(1) 770 | db.t.FatalOnError(tx.Rollback()) 771 | db.sync() 772 | table.assertRowsCount(0) 773 | table.assertRowDoesNotExist(1) 774 | } 775 | 776 | func TestCancelContext(t *testing.T) { 777 | db := getRemoteDb(T{t}) 778 | testCancelContext(db) 779 | } 780 | 781 | func TestCancelContextEmbedded(t *testing.T) { 782 | db := getEmbeddedDb(T{t}) 783 | testCancelContext(db) 784 | } 785 | 786 | func testCancelContext(db *Database) { 787 | if db == nil { 788 | return 789 | } 790 | ctx, cancel := context.WithCancel(context.Background()) 791 | cancel() 792 | _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)") 793 | if err == nil { 794 | db.t.FatalWithMsg("should have failed") 795 | } 796 | if !errors.Is(err, context.Canceled) { 797 | db.t.FatalWithMsg("should have failed with context.Canceled") 798 | } 799 | } 800 | 801 | func TestCancelContextWithTransaction(t *testing.T) { 802 | db := getRemoteDb(T{t}) 803 | testCancelContextWithTransaction(db) 804 | } 805 | 806 | func TestCancelContextWithTransactionEmbedded(t *testing.T) { 807 | db := getEmbeddedDb(T{t}) 808 | testCancelContextWithTransaction(db) 809 | } 810 | 811 | func testCancelContextWithTransaction(db *Database) { 812 | if db == nil { 813 | return 814 | } 815 | table := db.createTable() 816 | ctx, cancel := context.WithCancel(context.Background()) 817 | tx := table.beginTxWithContext(ctx) 818 | tx.insertRows(0, 10) 819 | tx.insertRowsWithArgs(10, 10) 820 | tx.assertRowsCount(20) 821 | tx.assertRowDoesNotExist(20) 822 | tx.assertRowExists(0) 823 | tx.assertRowExists(19) 824 | // let's cancel the context before the commit 825 | cancel() 826 | err := tx.Commit() 827 | if err == nil { 828 | db.t.FatalWithMsg("should have failed") 829 | } 830 | if !errors.Is(err, context.Canceled) { 831 | db.t.FatalWithMsg("should have failed with context.Canceled") 832 | } 833 | // rolling back the transaction should not result in any error 834 | db.t.FatalOnError(tx.Rollback()) 835 | } 836 | 837 | func TestTransactionRollback(t *testing.T) { 838 | db := getRemoteDb(T{t}) 839 | testTransactionRollback(db) 840 | } 841 | 842 | func TestTransactionRollbackEmbedded(t *testing.T) { 843 | db := getEmbeddedDb(T{t}) 844 | testTransactionRollback(db) 845 | } 846 | 847 | func testTransactionRollback(db *Database) { 848 | if db == nil { 849 | return 850 | } 851 | table := db.createTable() 852 | tx := table.beginTx() 853 | tx.insertRows(0, 10) 854 | tx.insertRowsWithArgs(10, 10) 855 | tx.assertRowsCount(20) 856 | tx.assertRowDoesNotExist(20) 857 | tx.assertRowExists(0) 858 | tx.assertRowExists(19) 859 | db.t.FatalOnError(tx.Rollback()) 860 | db.sync() 861 | table.assertRowsCount(0) 862 | } 863 | 864 | func TestArguments(t *testing.T) { 865 | db := getRemoteDb(T{t}) 866 | testArguments(db) 867 | } 868 | 869 | func TestArgumentsEmbedded(t *testing.T) { 870 | db := getEmbeddedDb(T{t}) 871 | testArguments(db) 872 | } 873 | 874 | func testArguments(db *Database) { 875 | if db == nil { 876 | return 877 | } 878 | t := db.t 879 | tableName := fmt.Sprintf("test_%d", time.Now().UnixNano()) 880 | _, err := db.Exec(fmt.Sprintf("CREATE TABLE %s (id INTEGER, name TEXT, gpa REAL, cv BLOB);", tableName)) 881 | if err != nil { 882 | t.Fatal(err) 883 | } 884 | _, err = db.Exec(fmt.Sprintf("INSERT INTO %s (id, name, gpa, cv) VALUES (?, ?, ?, randomblob(10));", tableName), 0, fmt.Sprint(0), 0.5) 885 | if err != nil { 886 | t.Fatal(err) 887 | } 888 | db.sync() 889 | rows, err := db.QueryContext(context.Background(), "SELECT NULL, id, name, gpa, cv FROM "+tableName) 890 | if err != nil { 891 | t.Fatal(err) 892 | } 893 | defer rows.Close() 894 | idx := 0 895 | for rows.Next() { 896 | if idx > 0 { 897 | t.Fatal("idx should be <= ", 0) 898 | } 899 | var null any 900 | var id int 901 | var name string 902 | var gpa float64 903 | var cv []byte 904 | if err := rows.Scan(&null, &id, &name, &gpa, &cv); err != nil { 905 | t.Fatal(err) 906 | } 907 | if null != nil { 908 | t.Fatal("null should be nil") 909 | } 910 | if id != int(idx) { 911 | t.Fatal("id should be ", idx, " got ", id) 912 | } 913 | if name != fmt.Sprint(idx) { 914 | t.Fatal("name should be", idx) 915 | } 916 | if gpa != float64(idx)+0.5 { 917 | t.Fatal("gpa should be", float64(idx)+0.5) 918 | } 919 | if len(cv) != 10 { 920 | t.Fatal("cv should be 10 bytes") 921 | } 922 | idx++ 923 | } 924 | if idx != 1 { 925 | t.Fatal("idx should be 1 got ", idx) 926 | } 927 | } 928 | 929 | func TestPing(t *testing.T) { 930 | db := getRemoteDb(T{t}) 931 | testPing(db) 932 | } 933 | 934 | func TestPingEmbedded(t *testing.T) { 935 | db := getEmbeddedDb(T{t}) 936 | testPing(db) 937 | } 938 | 939 | func testPing(db *Database) { 940 | if db == nil { 941 | return 942 | } 943 | // This ping should succeed because the database is up and running 944 | db.t.FatalOnError(db.Ping()) 945 | 946 | db.t.Cleanup(func() { 947 | db.Close() 948 | 949 | // This ping should return an error because the database is already closed 950 | err := db.Ping() 951 | if err == nil { 952 | db.t.Fatal("db.Ping succeeded when it should have failed") 953 | } 954 | }) 955 | } 956 | 957 | func TestDataTypes(t *testing.T) { 958 | db := getRemoteDb(T{t}) 959 | testDataTypes(db) 960 | } 961 | 962 | func TestDataTypesEmbedded(t *testing.T) { 963 | db := getEmbeddedDb(T{t}) 964 | testDataTypes(db) 965 | } 966 | 967 | func testDataTypes(db *Database) { 968 | if db == nil { 969 | return 970 | } 971 | var ( 972 | text string 973 | nullText sql.NullString 974 | integer sql.NullInt64 975 | nullInteger sql.NullInt64 976 | boolean bool 977 | float8 float64 978 | nullFloat sql.NullFloat64 979 | bytea []byte 980 | Time time.Time 981 | ) 982 | t := db.t 983 | db.t.FatalOnError(db.QueryRowContext(db.ctx, "SELECT 'foobar' as text, NULL as text, NULL as integer, 42 as integer, 1 as boolean, X'000102' as bytea, 3.14 as float8, NULL as float8, '0001-01-01 01:00:00+00:00' as time;").Scan(&text, &nullText, &nullInteger, &integer, &boolean, &bytea, &float8, &nullFloat, &Time)) 984 | switch { 985 | case text != "foobar": 986 | t.Error("value mismatch - text") 987 | case nullText.Valid: 988 | t.Error("null text is valid") 989 | case nullInteger.Valid: 990 | t.Error("null integer is valid") 991 | case !integer.Valid: 992 | t.Error("integer is not valid") 993 | case integer.Int64 != 42: 994 | t.Error("value mismatch - integer") 995 | case !boolean: 996 | t.Error("value mismatch - boolean") 997 | case float8 != 3.14: 998 | t.Error("value mismatch - float8") 999 | case !bytes.Equal(bytea, []byte{0, 1, 2}): 1000 | t.Error("value mismatch - bytea") 1001 | case nullFloat.Valid: 1002 | t.Error("null float is valid") 1003 | case !Time.Equal(time.Time{}.Add(time.Hour)): 1004 | t.Error("value mismatch - time") 1005 | } 1006 | } 1007 | 1008 | func TestConcurrentOnSingleConnection(t *testing.T) { 1009 | db := getRemoteDb(T{t}) 1010 | testConcurrentOnSingleConnection(db) 1011 | } 1012 | 1013 | func TestConcurrentOnSingleConnectionEmbedded(t *testing.T) { 1014 | db := getEmbeddedDb(T{t}) 1015 | testConcurrentOnSingleConnection(db) 1016 | } 1017 | 1018 | func testConcurrentOnSingleConnection(db *Database) { 1019 | if db == nil { 1020 | return 1021 | } 1022 | t1 := db.createTable() 1023 | t2 := db.createTable() 1024 | t3 := db.createTable() 1025 | t1.insertRowsInternal(1, 10, func(i int) sql.Result { 1026 | return t1.db.exec("INSERT INTO "+t1.name+" VALUES(?, ?)", i, i) 1027 | }) 1028 | t2.insertRowsInternal(1, 10, func(i int) sql.Result { 1029 | return t2.db.exec("INSERT INTO "+t2.name+" VALUES(?, ?)", i, -1*i) 1030 | }) 1031 | t3.insertRowsInternal(1, 10, func(i int) sql.Result { 1032 | return t3.db.exec("INSERT INTO "+t3.name+" VALUES(?, ?)", i, 0) 1033 | }) 1034 | db.sync() 1035 | g, ctx := errgroup.WithContext(context.Background()) 1036 | conn, err := db.Conn(context.Background()) 1037 | db.t.FatalOnError(err) 1038 | defer conn.Close() 1039 | worker := func(t Table, check func(int) error) func() error { 1040 | return func() error { 1041 | for i := 1; i < 100; i++ { 1042 | // Each iteration is wrapped into a function to make sure that `defer rows.Close()` 1043 | // is called after each iteration not at the end of the outer function 1044 | err := func() error { 1045 | rows, err := conn.QueryContext(ctx, "SELECT b FROM "+t.name) 1046 | if err != nil { 1047 | return fmt.Errorf("%w: %s", err, string(debug.Stack())) 1048 | } 1049 | defer rows.Close() 1050 | for rows.Next() { 1051 | var v int 1052 | err := rows.Scan(&v) 1053 | if err != nil { 1054 | return fmt.Errorf("%w: %s", err, string(debug.Stack())) 1055 | } 1056 | if err := check(v); err != nil { 1057 | return fmt.Errorf("%w: %s", err, string(debug.Stack())) 1058 | } 1059 | } 1060 | err = rows.Err() 1061 | if err != nil { 1062 | return fmt.Errorf("%w: %s", err, string(debug.Stack())) 1063 | } 1064 | return nil 1065 | }() 1066 | if err != nil { 1067 | return err 1068 | } 1069 | } 1070 | return nil 1071 | } 1072 | } 1073 | g.Go(worker(t1, func(v int) error { 1074 | if v <= 0 { 1075 | return fmt.Errorf("got non-positive value from table1: %d", v) 1076 | } 1077 | return nil 1078 | })) 1079 | g.Go(worker(t2, func(v int) error { 1080 | if v >= 0 { 1081 | return fmt.Errorf("got non-negative value from table2: %d", v) 1082 | } 1083 | return nil 1084 | })) 1085 | g.Go(worker(t3, func(v int) error { 1086 | if v != 0 { 1087 | return fmt.Errorf("got non-zero value from table3: %d", v) 1088 | } 1089 | return nil 1090 | })) 1091 | db.t.FatalOnError(g.Wait()) 1092 | } 1093 | 1094 | func runFileTest(t *testing.T, test func(*testing.T, *sql.DB)) { 1095 | dir, err := os.MkdirTemp("", "libsql-*") 1096 | if err != nil { 1097 | t.Fatal(err) 1098 | } 1099 | defer os.RemoveAll(dir) 1100 | db, err := sql.Open("libsql", "file:"+dir+"/test.db") 1101 | if err != nil { 1102 | t.Fatal(err) 1103 | } 1104 | defer func() { 1105 | if err := db.Close(); err != nil { 1106 | t.Fatal(err) 1107 | } 1108 | }() 1109 | test(t, db) 1110 | } 1111 | 1112 | func runMemoryAndFileTests(t *testing.T, test func(*testing.T, *sql.DB)) { 1113 | t.Parallel() 1114 | t.Run("Memory", func(t *testing.T) { 1115 | t.Parallel() 1116 | db, err := sql.Open("libsql", ":memory:") 1117 | if err != nil { 1118 | t.Fatal(err) 1119 | } 1120 | defer func() { 1121 | if err := db.Close(); err != nil { 1122 | t.Fatal(err) 1123 | } 1124 | }() 1125 | test(t, db) 1126 | }) 1127 | t.Run("File", func(t *testing.T) { 1128 | runFileTest(t, test) 1129 | }) 1130 | } 1131 | 1132 | func TestErrorNonUtf8URL(t *testing.T) { 1133 | t.Parallel() 1134 | db, err := sql.Open("libsql", "file:a\xc5z") 1135 | if err == nil { 1136 | defer func() { 1137 | if err := db.Close(); err != nil { 1138 | t.Fatal(err) 1139 | } 1140 | }() 1141 | t.Fatal("expected error") 1142 | } 1143 | if err.Error() != "failed to open local database file:a\xc5z\nerror code = 1: Wrong URL: invalid utf-8 sequence of 1 bytes from index 6" { 1144 | t.Fatal("unexpected error:", err) 1145 | } 1146 | } 1147 | 1148 | func TestErrorWrongURL(t *testing.T) { 1149 | t.Skip("Does not work with v2") 1150 | t.Parallel() 1151 | db, err := sql.Open("libsql", "http://example.com/test") 1152 | if err == nil { 1153 | defer func() { 1154 | if err := db.Close(); err != nil { 1155 | t.Fatal(err) 1156 | } 1157 | }() 1158 | t.Fatal("expected error") 1159 | } 1160 | if err.Error() != "failed to open database http://example.com/test\nerror code = 1: Error opening URL http://example.com/test: Failed to connect to database: `Unable to open remote database http://example.com/test with Database::open()`" { 1161 | t.Fatal("unexpected error:", err) 1162 | } 1163 | } 1164 | 1165 | func TestErrorCanNotConnect(t *testing.T) { 1166 | t.Parallel() 1167 | db, err := sql.Open("libsql", "file:/root/test.db") 1168 | if err != nil { 1169 | t.Fatal(err) 1170 | } 1171 | defer func() { 1172 | if err := db.Close(); err != nil { 1173 | t.Fatal(err) 1174 | } 1175 | }() 1176 | conn, err := db.Conn(context.Background()) 1177 | if err == nil { 1178 | defer func() { 1179 | if err := conn.Close(); err != nil { 1180 | t.Fatal(err) 1181 | } 1182 | }() 1183 | t.Fatal("expected error") 1184 | } 1185 | if err.Error() != "failed to connect to database\nerror code = 1: Unable to connect: Failed to connect to database: `Unable to open connection to local database file:/root/test.db: 14`" { 1186 | t.Fatal("unexpected error:", err) 1187 | } 1188 | } 1189 | 1190 | func TestExec(t *testing.T) { 1191 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1192 | if _, err := db.ExecContext(context.Background(), "CREATE TABLE test (id INTEGER, name TEXT)"); err != nil { 1193 | t.Fatal(err) 1194 | } 1195 | }) 1196 | } 1197 | 1198 | func TestExecWithQuery(t *testing.T) { 1199 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1200 | if _, err := db.QueryContext(context.Background(), "SELECT 1"); err != nil { 1201 | t.Fatal(err) 1202 | } 1203 | }) 1204 | } 1205 | 1206 | func TestErrorExec(t *testing.T) { 1207 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1208 | _, err := db.ExecContext(context.Background(), "CREATE TABLES test (id INTEGER, name TEXT)") 1209 | if err == nil { 1210 | t.Fatal("expected error") 1211 | } 1212 | if err.Error() != "failed to execute query CREATE TABLES test (id INTEGER, name TEXT)\nerror code = 2: Error executing statement: SQLite failure: `near \"TABLES\": syntax error`" { 1213 | t.Fatal("unexpected error:", err) 1214 | } 1215 | }) 1216 | } 1217 | 1218 | func TestQuery(t *testing.T) { 1219 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1220 | if _, err := db.ExecContext(context.Background(), "CREATE TABLE test (id INTEGER, name TEXT, gpa REAL, cv BLOB)"); err != nil { 1221 | t.Fatal(err) 1222 | } 1223 | for i := 0; i < 10; i++ { 1224 | if _, err := db.ExecContext(context.Background(), "INSERT INTO test VALUES(?, ?, ?, randomblob(10))", i, fmt.Sprint(i), float64(i)+0.5); err != nil { 1225 | t.Fatal(err) 1226 | } 1227 | } 1228 | rows, err := db.QueryContext(context.Background(), "SELECT NULL, id, name, gpa, cv FROM test") 1229 | if err != nil { 1230 | t.Fatal(err) 1231 | } 1232 | columns, err := rows.Columns() 1233 | if err != nil { 1234 | t.Fatal(err) 1235 | } 1236 | assert.DeepEqual(t, columns, []string{"NULL", "id", "name", "gpa", "cv"}) 1237 | types, err := rows.ColumnTypes() 1238 | if err != nil { 1239 | t.Fatal(err) 1240 | } 1241 | if len(types) != 5 { 1242 | t.Fatal("types should be 5") 1243 | } 1244 | defer rows.Close() 1245 | idx := 0 1246 | for rows.Next() { 1247 | var null any 1248 | var id int 1249 | var name string 1250 | var gpa float64 1251 | var cv []byte 1252 | if err := rows.Scan(&null, &id, &name, &gpa, &cv); err != nil { 1253 | t.Fatal(err) 1254 | } 1255 | if null != nil { 1256 | t.Fatal("null should be nil") 1257 | } 1258 | if id != int(idx) { 1259 | t.Fatal("id should be", idx) 1260 | } 1261 | if name != fmt.Sprint(idx) { 1262 | t.Fatal("name should be", idx) 1263 | } 1264 | if gpa != float64(idx)+0.5 { 1265 | t.Fatal("gpa should be", float64(idx)+0.5) 1266 | } 1267 | if len(cv) != 10 { 1268 | t.Fatal("cv should be 10 bytes") 1269 | } 1270 | idx++ 1271 | } 1272 | }) 1273 | } 1274 | 1275 | func TestErrorQuery(t *testing.T) { 1276 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1277 | rows, err := db.QueryContext(context.Background(), "SELECT NULL, id, name, gpa, cv FROM test") 1278 | if rows != nil { 1279 | rows.Close() 1280 | } 1281 | if err == nil { 1282 | t.Fatal("expected error") 1283 | } 1284 | if err.Error() != "failed to execute query SELECT NULL, id, name, gpa, cv FROM test\nerror code = 1: Error executing statement: SQLite failure: `no such table: test`" { 1285 | t.Fatal("unexpected error:", err) 1286 | } 1287 | }) 1288 | } 1289 | 1290 | func TestQueryWithEmptyResult(t *testing.T) { 1291 | runMemoryAndFileTests(t, func(t *testing.T, db *sql.DB) { 1292 | if _, err := db.ExecContext(context.Background(), "CREATE TABLE test (id INTEGER, name TEXT, gpa REAL, cv BLOB)"); err != nil { 1293 | t.Fatal(err) 1294 | } 1295 | rows, err := db.QueryContext(context.Background(), "SELECT NULL, id, name, gpa, cv FROM test") 1296 | if err != nil { 1297 | t.Fatal(err) 1298 | } 1299 | defer rows.Close() 1300 | columns, err := rows.Columns() 1301 | if err != nil { 1302 | t.Fatal(err) 1303 | } 1304 | assert.DeepEqual(t, columns, []string{"NULL", "id", "name", "gpa", "cv"}) 1305 | types, err := rows.ColumnTypes() 1306 | if err != nil { 1307 | t.Fatal(err) 1308 | } 1309 | if len(types) != 5 { 1310 | t.Fatal("types should be 5") 1311 | } 1312 | for rows.Next() { 1313 | t.Fatal("there should be no rows") 1314 | } 1315 | }) 1316 | } 1317 | 1318 | func TestErrorRowsNext(t *testing.T) { 1319 | runFileTest(t, func(t *testing.T, db *sql.DB) { 1320 | db.Exec("PRAGMA journal_mode=DELETE") 1321 | if _, err := db.ExecContext(context.Background(), "CREATE TABLE test (id INTEGER)"); err != nil { 1322 | t.Fatal(err) 1323 | } 1324 | for i := 0; i < 10; i++ { 1325 | if _, err := db.ExecContext(context.Background(), "INSERT INTO test VALUES("+fmt.Sprint(i)+")"); err != nil { 1326 | t.Fatal(err) 1327 | } 1328 | } 1329 | c1, err := db.Conn(context.Background()) 1330 | if err != nil { 1331 | t.Fatal(err) 1332 | } 1333 | defer c1.Close() 1334 | c1.ExecContext(context.Background(), "PRAGMA journal_mode=DELETE") 1335 | c2, err := db.Conn(context.Background()) 1336 | if err != nil { 1337 | t.Fatal(err) 1338 | } 1339 | defer c2.Close() 1340 | c2.ExecContext(context.Background(), "PRAGMA journal_mode=DELETE") 1341 | _, err = c1.ExecContext(context.Background(), "BEGIN EXCLUSIVE TRANSACTION") 1342 | if err != nil { 1343 | t.Fatal(err) 1344 | } 1345 | rows, err := c2.QueryContext(context.Background(), "SELECT id FROM test") 1346 | if err != nil { 1347 | t.Fatal(err) 1348 | } 1349 | defer rows.Close() 1350 | if rows.Next() { 1351 | t.Fatal("there should be no rows") 1352 | } 1353 | err = rows.Err() 1354 | if err == nil { 1355 | t.Fatal("expected error") 1356 | } 1357 | if err.Error() != "failed to get next row\nerror code = 1: Error fetching next row: SQLite failure: `database is locked`" { 1358 | t.Fatal("unexpected error:", err) 1359 | } 1360 | }) 1361 | } 1362 | --------------------------------------------------------------------------------