├── .envrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── copyright.code-snippets └── extensions.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── auth.go ├── auth_test.go ├── backup.go ├── backup_example_test.go ├── backup_test.go ├── blob.go ├── blob_test.go ├── blocking_step.go ├── cmd └── zombiezen-sqlite-migrate │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── migrate.go │ ├── migrate_test.go │ └── testdata │ ├── TestProcess │ ├── AnonymousInterface │ │ ├── original.go │ │ └── want.go │ ├── Autocommit │ │ ├── original.go │ │ └── want.go │ ├── ErrorCode │ │ ├── original.go │ │ └── want.go │ ├── Exec │ │ ├── original.go │ │ └── want.go │ ├── ExecScriptFS │ │ ├── original.go │ │ └── want.go │ ├── File │ │ ├── original.go │ │ └── want.go │ └── ImportRewrite │ │ ├── original.go │ │ └── want.go │ └── skeleton │ ├── crawshaw.io │ └── sqlite │ │ ├── sqlitex │ │ └── stubs.go │ │ └── stubs.go │ └── zombiezen.com │ └── go │ ├── bass │ └── sql │ │ └── sqlitefile │ │ └── stubs.go │ └── sqlite │ ├── sqlitex │ └── stubs.go │ └── stubs.go ├── doc.go ├── example_test.go ├── export_test.go ├── ext ├── generateseries │ ├── example_test.go │ └── generateseries.go └── refunc │ ├── refunc.go │ └── refunc_test.go ├── flake.lock ├── flake.nix ├── func.go ├── func_test.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── http_example_test.go ├── index_constraint.go ├── internal_test.go ├── op_type.go ├── openflags.go ├── result.go ├── result_test.go ├── session.go ├── session_test.go ├── shell ├── example_test.go └── shell.go ├── sqlite.go ├── sqlite_test.go ├── sqlitefile ├── buffer.go ├── buffer_test.go ├── file.go └── file_test.go ├── sqlitemigration ├── example_test.go ├── sqlitemigration.go └── sqlitemigration_test.go ├── sqlitex ├── doc.go ├── example_test.go ├── exec.go ├── exec_test.go ├── pool.go ├── pool_test.go ├── query.go ├── query_test.go ├── rand_id.go ├── rand_id_test.go ├── savepoint.go └── savepoint_test.go ├── vtable.go └── vtable_example_test.go /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | use flake 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zombiezen 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Roxy Light 2 | # SPDX-License-Identifier: ISC 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: daily 10 | reviewers: [zombiezen] 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Roxy Light 2 | # SPDX-License-Identifier: ISC 3 | 4 | name: Build 5 | on: 6 | - push 7 | - pull_request 8 | permissions: 9 | contents: read 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | go: ["1.24"] 18 | arch: [amd64] 19 | include: 20 | - os: ubuntu-latest 21 | go: "1.24" 22 | arch: "386" 23 | steps: 24 | - name: Check out code 25 | uses: actions/checkout@v4 26 | - name: Set up Go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: ${{ matrix.go }} 30 | - name: Run tests with race detector 31 | run: go test -mod=readonly -race -tags="libc.memgrind" -v ./... 32 | if: ${{ matrix.arch == 'amd64' }} 33 | env: 34 | GOARCH: ${{ matrix.arch }} 35 | - name: Run tests without race detector 36 | run: go test -mod=readonly -v -tags="libc.memgrind" ./... 37 | if: ${{ matrix.arch != 'amd64' }} 38 | env: 39 | GOARCH: ${{ matrix.arch }} 40 | migrate: 41 | name: Migration Tool 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Check out code 45 | uses: actions/checkout@v4 46 | - name: Set up Go 47 | uses: actions/setup-go@v5 48 | with: 49 | go-version: "1.24" 50 | - name: Run tests 51 | run: go test -mod=readonly -race -v ./... 52 | working-directory: ./cmd/zombiezen-sqlite-migrate 53 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Roxy Light 2 | # SPDX-License-Identifier: ISC 3 | 4 | name: Publish 5 | on: 6 | release: 7 | types: [published] 8 | permissions: {} 9 | jobs: 10 | go-get: 11 | name: go get 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Fetch release from proxy 15 | run: | 16 | curl -fsSL "https://proxy.golang.org/zombiezen.com/go/sqlite/@v/$(echo "$GITHUB_REF" | sed -e 's:^refs/tags/::').info" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.vscode/copyright.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Copyright": { 3 | "prefix": "copyright", 4 | "body": [ 5 | "$LINE_COMMENT Copyright $CURRENT_YEAR Roxy Light", 6 | "$LINE_COMMENT SPDX-License-Identifier: ISC", 7 | ], 8 | "description": "Copyright header" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | "recommendations": [ 4 | "golang.go" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at roxy@zombiezen.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # `zombiezen.com/go/sqlite` Contributor's Guide 2 | 3 | `zombiezen.com/go/sqlite` is [@zombiezen's][zombiezen] labor of love 4 | in providing a pleasant, performant, and powerful Go binding to SQLite. 5 | The design philosophy of `zombiezen.com/go/sqlite` 6 | is to be a direct binding to SQLite as much as possible 7 | while adapting the API to use Go idioms. 8 | 9 | @zombiezen is open to accepting bug fixes, 10 | but generally not larger changes or features. 11 | If in doubt, ask them. 12 | And remember, [a "no" is temporary, a "yes" is forever.](https://opensource.guide/best-practices/) 13 | 14 | File a bug on the [issue tracker][] or [open a pull request][]. 15 | Please follow the [Code of Conduct][] for all interactions. 16 | If applicable, add unit tests for your change before sending out for review. 17 | 18 | [Code of Conduct]: CODE_OF_CONDUCT.md 19 | [issue tracker]: https://github.com/zombiezen/go-sqlite/issues/new 20 | [open a pull request]: https://github.com/zombiezen/go-sqlite/compare 21 | [zombiezen]: https://github.com/zombiezen 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Crawshaw 2 | Copyright (c) 2021 Roxy Light 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `zombiezen.com/go/sqlite` 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/zombiezen.com/go/sqlite.svg)][reference docs] 4 | 5 | This package provides a low-level Go interface to [SQLite 3][]. 6 | It is a fork of [`crawshaw.io/sqlite`][] that uses [`modernc.org/sqlite`][], 7 | a CGo-free SQLite package. 8 | It aims to be a mostly drop-in replacement for `crawshaw.io/sqlite`. 9 | 10 | This package deliberately does not provide a `database/sql` driver. 11 | See [David Crawshaw's rationale][] for an in-depth explanation. 12 | If you want to use `database/sql` with SQLite without CGo, 13 | use `modernc.org/sqlite` directly. 14 | 15 | [`crawshaw.io/sqlite`]: https://github.com/crawshaw/sqlite 16 | [David Crawshaw's rationale]: https://crawshaw.io/blog/go-and-sqlite 17 | [`modernc.org/sqlite`]: https://pkg.go.dev/modernc.org/sqlite 18 | [reference docs]: https://pkg.go.dev/zombiezen.com/go/sqlite 19 | [SQLite 3]: https://sqlite.org/ 20 | 21 | ## Features 22 | 23 | - Full SQLite functionality via `modernc.org/sqlite`, 24 | an automatically generated translation of the original C source code of SQLite into Go 25 | - Builds with `CGO_ENABLED=0`, 26 | allowing cross-compiling and data race detection 27 | - Allows access to SQLite-specific features 28 | like [blob I/O][] and [user-defined functions][] 29 | - Includes a simple [schema migration package][] 30 | - Utilities for [running embedded SQL scripts][ExecScriptFS] using the 31 | [Go 1.16 embedding feature][] 32 | - A [`go fix`-like tool][migration docs] for migrating existing code using 33 | `crawshaw.io/sqlite` 34 | - A [simple REPL][] for debugging 35 | 36 | [blob I/O]: https://pkg.go.dev/zombiezen.com/go/sqlite#Blob 37 | [ExecScriptFS]: https://pkg.go.dev/zombiezen.com/go/sqlite/sqlitex#ExecScriptFS 38 | [Go 1.16 embedding feature]: https://pkg.go.dev/embed 39 | [migration docs]: cmd/zombiezen-sqlite-migrate/README.md 40 | [schema migration package]: https://pkg.go.dev/zombiezen.com/go/sqlite/sqlitemigration 41 | [simple REPL]: https://pkg.go.dev/zombiezen.com/go/sqlite/shell 42 | [user-defined functions]: https://pkg.go.dev/zombiezen.com/go/sqlite#Conn.CreateFunction 43 | 44 | ## Install 45 | 46 | ```shell 47 | go get zombiezen.com/go/sqlite 48 | ``` 49 | 50 | While this library does not use CGo, 51 | make sure that you are building for one of the [supported architectures][]. 52 | 53 | [supported architectures]: https://pkg.go.dev/modernc.org/sqlite#hdr-Supported_platforms_and_architectures 54 | 55 | ## Getting Started 56 | 57 | ```go 58 | import ( 59 | "fmt" 60 | 61 | "zombiezen.com/go/sqlite" 62 | "zombiezen.com/go/sqlite/sqlitex" 63 | ) 64 | 65 | // ... 66 | 67 | // Open an in-memory database. 68 | conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite) 69 | if err != nil { 70 | return err 71 | } 72 | defer conn.Close() 73 | 74 | // Execute a query. 75 | err = sqlitex.ExecuteTransient(conn, "SELECT 'hello, world';", &sqlitex.ExecOptions{ 76 | ResultFunc: func(stmt *sqlite.Stmt) error { 77 | fmt.Println(stmt.ColumnText(0)) 78 | return nil 79 | }, 80 | }) 81 | if err != nil { 82 | return err 83 | } 84 | ``` 85 | 86 | If you're creating a new application, 87 | see the [package examples][] or the [reference docs][]. 88 | 89 | If you're looking to switch existing code that uses `crawshaw.io/sqlite`, 90 | take a look at the [migration docs][]. 91 | 92 | [package examples]: https://pkg.go.dev/zombiezen.com/go/sqlite#pkg-examples 93 | 94 | ## License 95 | 96 | [ISC](LICENSE) 97 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "sync" 10 | 11 | "modernc.org/libc" 12 | lib "modernc.org/sqlite/lib" 13 | ) 14 | 15 | // An Authorizer is called during statement preparation to see whether an action 16 | // is allowed by the application. An Authorizer must not modify the database 17 | // connection, including by preparing statements. 18 | // 19 | // See https://sqlite.org/c3ref/set_authorizer.html for a longer explanation. 20 | type Authorizer interface { 21 | Authorize(Action) AuthResult 22 | } 23 | 24 | // SetAuthorizer registers an authorizer for the database connection. 25 | // SetAuthorizer(nil) clears any authorizer previously set. 26 | func (c *Conn) SetAuthorizer(auth Authorizer) error { 27 | if c == nil { 28 | return fmt.Errorf("sqlite: set authorizer: nil connection") 29 | } 30 | if auth == nil { 31 | c.releaseAuthorizer() 32 | res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, 0, 0)) 33 | if err := res.ToError(); err != nil { 34 | return fmt.Errorf("sqlite: set authorizer: %w", err) 35 | } 36 | return nil 37 | } 38 | 39 | authorizers.mu.Lock() 40 | if authorizers.m == nil { 41 | authorizers.m = make(map[uintptr]Authorizer) 42 | } 43 | authorizers.m[c.conn] = auth 44 | authorizers.mu.Unlock() 45 | 46 | xAuth := cFuncPointer(authTrampoline) 47 | 48 | res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, xAuth, c.conn)) 49 | if err := res.ToError(); err != nil { 50 | return fmt.Errorf("sqlite: set authorizer: %w", err) 51 | } 52 | return nil 53 | } 54 | 55 | func (c *Conn) releaseAuthorizer() { 56 | authorizers.mu.Lock() 57 | delete(authorizers.m, c.conn) 58 | authorizers.mu.Unlock() 59 | } 60 | 61 | var authorizers struct { 62 | mu sync.RWMutex 63 | m map[uintptr]Authorizer // sqlite3* -> Authorizer 64 | } 65 | 66 | func authTrampoline(tls *libc.TLS, conn uintptr, op int32, cArg1, cArg2, cDB, cTrigger uintptr) int32 { 67 | authorizers.mu.RLock() 68 | auth := authorizers.m[conn] 69 | authorizers.mu.RUnlock() 70 | return int32(auth.Authorize(Action{ 71 | op: OpType(op), 72 | arg1: libc.GoString(cArg1), 73 | arg2: libc.GoString(cArg2), 74 | database: libc.GoString(cDB), 75 | trigger: libc.GoString(cTrigger), 76 | })) 77 | } 78 | 79 | // AuthorizeFunc is a function that implements Authorizer. 80 | type AuthorizeFunc func(Action) AuthResult 81 | 82 | // Authorize calls f. 83 | func (f AuthorizeFunc) Authorize(action Action) AuthResult { 84 | return f(action) 85 | } 86 | 87 | // AuthResult is the result of a call to an Authorizer. The zero value is 88 | // AuthResultOK. 89 | type AuthResult int32 90 | 91 | // Possible return values from Authorize. 92 | const ( 93 | // AuthResultOK allows the SQL statement to be compiled. 94 | AuthResultOK AuthResult = lib.SQLITE_OK 95 | // AuthResultDeny causes the entire SQL statement to be rejected with an error. 96 | AuthResultDeny AuthResult = lib.SQLITE_DENY 97 | // AuthResultIgnore disallows the specific action but allow the SQL statement 98 | // to continue to be compiled. For OpRead, this substitutes a NULL for the 99 | // column value. For OpDelete, the DELETE operation proceeds but the truncate 100 | // optimization is disabled and all rows are deleted individually. 101 | AuthResultIgnore AuthResult = lib.SQLITE_IGNORE 102 | ) 103 | 104 | // String returns the C constant name of the result. 105 | func (result AuthResult) String() string { 106 | switch result { 107 | case AuthResultOK: 108 | return "SQLITE_OK" 109 | case AuthResultDeny: 110 | return "SQLITE_DENY" 111 | case AuthResultIgnore: 112 | return "SQLITE_IGNORE" 113 | default: 114 | return fmt.Sprintf("AuthResult(%d)", int32(result)) 115 | } 116 | } 117 | 118 | // Action represents an action to be authorized. 119 | type Action struct { 120 | op OpType 121 | arg1 string 122 | arg2 string 123 | database string 124 | trigger string 125 | } 126 | 127 | // Mapping of argument position to concept at: 128 | // https://sqlite.org/c3ref/c_alter_table.html 129 | 130 | // Type returns the type of action being authorized. 131 | func (action Action) Type() OpType { 132 | return action.op 133 | } 134 | 135 | // Accessor returns the name of the inner-most trigger or view that is 136 | // responsible for the access attempt or the empty string if this access attempt 137 | // is directly from top-level SQL code. 138 | func (action Action) Accessor() string { 139 | return action.trigger 140 | } 141 | 142 | // Database returns the name of the database (e.g. "main", "temp", etc.) this 143 | // action affects or the empty string if not applicable. 144 | func (action Action) Database() string { 145 | switch action.op { 146 | case OpDetach, OpAlterTable: 147 | return action.arg1 148 | default: 149 | return action.database 150 | } 151 | } 152 | 153 | // Index returns the name of the index this action affects or the empty string 154 | // if not applicable. 155 | func (action Action) Index() string { 156 | switch action.op { 157 | case OpCreateIndex, OpCreateTempIndex, OpDropIndex, OpDropTempIndex, OpReindex: 158 | return action.arg1 159 | default: 160 | return "" 161 | } 162 | } 163 | 164 | // Table returns the name of the table this action affects or the empty string 165 | // if not applicable. 166 | func (action Action) Table() string { 167 | switch action.op { 168 | case OpCreateTable, OpCreateTempTable, OpDelete, OpDropTable, OpDropTempTable, OpInsert, OpRead, OpUpdate, OpAnalyze, OpCreateVTable, OpDropVTable: 169 | return action.arg1 170 | case OpCreateIndex, OpCreateTempIndex, OpCreateTempTrigger, OpCreateTrigger, OpDropIndex, OpDropTempIndex, OpDropTempTrigger, OpDropTrigger, OpAlterTable: 171 | return action.arg2 172 | default: 173 | return "" 174 | } 175 | } 176 | 177 | // Trigger returns the name of the trigger this action affects or the empty 178 | // string if not applicable. 179 | func (action Action) Trigger() string { 180 | switch action.op { 181 | case OpCreateTempTrigger, OpCreateTrigger, OpDropTempTrigger, OpDropTrigger: 182 | return action.arg1 183 | default: 184 | return "" 185 | } 186 | } 187 | 188 | // View returns the name of the view this action affects or the empty string 189 | // if not applicable. 190 | func (action Action) View() string { 191 | switch action.op { 192 | case OpCreateTempView, OpCreateView, OpDropTempView, OpDropView: 193 | return action.arg1 194 | default: 195 | return "" 196 | } 197 | } 198 | 199 | // Pragma returns the name of the action's PRAGMA command or the empty string 200 | // if the action does not represent a PRAGMA command. 201 | // See https://sqlite.org/pragma.html#toc for a list of possible values. 202 | func (action Action) Pragma() string { 203 | if action.op != OpPragma { 204 | return "" 205 | } 206 | return action.arg1 207 | } 208 | 209 | // PragmaArg returns the argument to the PRAGMA command or the empty string if 210 | // the action does not represent a PRAGMA command or the PRAGMA command does not 211 | // take an argument. 212 | func (action Action) PragmaArg() string { 213 | if action.op != OpPragma { 214 | return "" 215 | } 216 | return action.arg2 217 | } 218 | 219 | // Column returns the name of the column this action affects or the empty string 220 | // if not applicable. For OpRead actions, this will return the empty string if a 221 | // table is referenced but no column values are extracted from that table 222 | // (e.g. a query like "SELECT COUNT(*) FROM tab"). 223 | func (action Action) Column() string { 224 | switch action.op { 225 | case OpRead, OpUpdate: 226 | return action.arg2 227 | default: 228 | return "" 229 | } 230 | } 231 | 232 | // Operation returns one of "BEGIN", "COMMIT", "RELEASE", or "ROLLBACK" for a 233 | // transaction or savepoint statement or the empty string otherwise. 234 | func (action Action) Operation() string { 235 | switch action.op { 236 | case OpTransaction, OpSavepoint: 237 | return action.arg1 238 | default: 239 | return "" 240 | } 241 | } 242 | 243 | // File returns the name of the file being ATTACHed or the empty string if the 244 | // action does not represent an ATTACH DATABASE statement. 245 | func (action Action) File() string { 246 | if action.op != OpAttach { 247 | return "" 248 | } 249 | return action.arg1 250 | } 251 | 252 | // Module returns the module name given to the virtual table statement or the 253 | // empty string if the action does not represent a CREATE VIRTUAL TABLE or 254 | // DROP VIRTUAL TABLE statement. 255 | func (action Action) Module() string { 256 | switch action.op { 257 | case OpCreateVTable, OpDropVTable: 258 | return action.arg2 259 | default: 260 | return "" 261 | } 262 | } 263 | 264 | // Savepoint returns the name given to the SAVEPOINT statement or the empty 265 | // string if the action does not represent a SAVEPOINT statement. 266 | func (action Action) Savepoint() string { 267 | if action.op != OpSavepoint { 268 | return "" 269 | } 270 | return action.arg2 271 | } 272 | 273 | // String returns a debugging representation of the action. 274 | func (action Action) String() string { 275 | sb := new(strings.Builder) 276 | sb.WriteString(action.op.String()) 277 | params := []struct { 278 | name, value string 279 | }{ 280 | {"database", action.Database()}, 281 | {"file", action.File()}, 282 | {"trigger", action.Trigger()}, 283 | {"index", action.Index()}, 284 | {"table", action.Table()}, 285 | {"view", action.View()}, 286 | {"module", action.Module()}, 287 | {"column", action.Column()}, 288 | 289 | {"operation", action.Operation()}, 290 | {"savepoint", action.Savepoint()}, 291 | 292 | {"pragma", action.Pragma()}, 293 | {"arg", action.PragmaArg()}, 294 | } 295 | for _, p := range params { 296 | if p.value != "" { 297 | sb.WriteString(" ") 298 | sb.WriteString(p.name) 299 | sb.WriteString(":") 300 | sb.WriteString(p.value) 301 | } 302 | } 303 | return sb.String() 304 | } 305 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "zombiezen.com/go/sqlite" 10 | ) 11 | 12 | func TestSetAuthorizer(t *testing.T) { 13 | c, err := sqlite.OpenConn(":memory:", 0) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer func() { 18 | if err := c.Close(); err != nil { 19 | t.Error(err) 20 | } 21 | }() 22 | 23 | authResult := sqlite.AuthResult(0) 24 | var lastAction sqlite.Action 25 | auth := sqlite.AuthorizeFunc(func(action sqlite.Action) sqlite.AuthResult { 26 | lastAction = action 27 | return authResult 28 | }) 29 | c.SetAuthorizer(auth) 30 | 31 | t.Run("Allowed", func(t *testing.T) { 32 | authResult = sqlite.AuthResultOK 33 | stmt, _, err := c.PrepareTransient("SELECT 1;") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | stmt.Finalize() 38 | if lastAction.Type() != sqlite.OpSelect { 39 | t.Errorf("action = %v; want %v", lastAction, sqlite.OpSelect) 40 | } 41 | }) 42 | 43 | t.Run("Denied", func(t *testing.T) { 44 | authResult = sqlite.AuthResultDeny 45 | stmt, _, err := c.PrepareTransient("SELECT 1;") 46 | if err == nil { 47 | stmt.Finalize() 48 | t.Fatal("PrepareTransient did not return an error") 49 | } 50 | if got, want := sqlite.ErrCode(err), sqlite.ResultAuth; got != want { 51 | t.Errorf("sqlite.ErrCode(err) = %v; want %v", got, want) 52 | } 53 | if lastAction.Type() != sqlite.OpSelect { 54 | t.Errorf("action = %v; want %v", lastAction, sqlite.OpSelect) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /backup.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "fmt" 8 | 9 | "modernc.org/libc" 10 | lib "modernc.org/sqlite/lib" 11 | ) 12 | 13 | // A Backup represents a copy operation between two database connections. 14 | // See https://www.sqlite.org/backup.html for more details. 15 | type Backup struct { 16 | tls *libc.TLS 17 | ptr uintptr 18 | } 19 | 20 | // NewBackup creates a [Backup] that copies from one connection to another. 21 | // The database name is "main" or "" for the main database, 22 | // "temp" for the temporary database, 23 | // or the name specified after the AS keyword in an ATTACH statement for an attached database. 24 | // If src and dst are the same connection, NewBackup will return an error. 25 | // It is the caller's responsibility to call [Backup.Close] on the returned Backup object. 26 | func NewBackup(dst *Conn, dstName string, src *Conn, srcName string) (*Backup, error) { 27 | cDstName, freeCDstName, err := cDBName(dstName) 28 | if err != nil { 29 | return nil, fmt.Errorf("sqlite: new backup: %w", err) 30 | } 31 | defer freeCDstName() 32 | cSrcName, freeCSrcName, err := cDBName(srcName) 33 | if err != nil { 34 | return nil, fmt.Errorf("sqlite: new backup: %w", err) 35 | } 36 | defer freeCSrcName() 37 | backupPtr := lib.Xsqlite3_backup_init(dst.tls, dst.conn, cDstName, src.conn, cSrcName) 38 | if backupPtr == 0 { 39 | res := ResultCode(lib.Xsqlite3_errcode(dst.tls, dst.conn)) 40 | return nil, fmt.Errorf("sqlite: new backup: %w", dst.extreserr(res)) 41 | } 42 | return &Backup{dst.tls, backupPtr}, nil 43 | } 44 | 45 | // Step copies up to n pages from the source database to the destination database. 46 | // If n is negative, all remaining source pages are copied. 47 | // more will be true if there are pages still remaining to be copied. 48 | // Step may return both an error and that more pages are still remaining: 49 | // this indicates the error is temporary and that Step can be retried. 50 | func (b *Backup) Step(n int) (more bool, err error) { 51 | res := ResultCode(lib.Xsqlite3_backup_step(b.tls, b.ptr, int32(n))) 52 | switch res { 53 | case ResultOK: 54 | return true, nil 55 | case ResultDone: 56 | return false, nil 57 | case ResultBusy, ResultLocked: 58 | // SQLITE_BUSY and SQLITE_LOCKED are retriable errors. 59 | return true, fmt.Errorf("sqlite: backup step: %w", res.ToError()) 60 | default: 61 | return false, fmt.Errorf("sqlite: backup step: %w", res.ToError()) 62 | } 63 | } 64 | 65 | // Remaining returns the number of pages still to be backed up 66 | // at the conclusion of the most recent call to [Backup.Step]. 67 | // The return value of Remaining before calling [Backup.Step] is undefined. 68 | // If the source database is modified in a way that changes the number of pages remaining, 69 | // that change is not reflected in the output until after the next call to [Backup.Step]. 70 | func (b *Backup) Remaining() int { 71 | return int(lib.Xsqlite3_backup_remaining(b.tls, b.ptr)) 72 | } 73 | 74 | // PageCount returns the total number of pages in the source database 75 | // at the conclusion of the most recent call to [Backup.Step]. 76 | // The return value of PageCount before calling [Backup.Step] is undefined. 77 | // If the source database is modified in a way that changes the size of the source database, 78 | // that change is not reflected in the output until after the next call to [Backup.Step]. 79 | func (b *Backup) PageCount() int { 80 | return int(lib.Xsqlite3_backup_pagecount(b.tls, b.ptr)) 81 | } 82 | 83 | // Close releases all resources associated with the backup. 84 | // If [Backup.Step] has not yet returned (false, nil), 85 | // then any active write transaction on the destination database is rolled back. 86 | func (b *Backup) Close() error { 87 | // The error from sqlite3_backup_finish indicates whether 88 | // a previous call to sqlite3_backup_step returned an error. 89 | // Since we're assuming that the caller will handle errors as they arise, 90 | // we always return nil from Close. 91 | // However, I'm not fully confident that Close will always be infallible, 92 | // so I'm not documenting this as part of the API. 93 | lib.Xsqlite3_backup_finish(b.tls, b.ptr) 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /backup_example_test.go: -------------------------------------------------------------------------------- 1 | package sqlite_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "zombiezen.com/go/sqlite" 9 | ) 10 | 11 | // This example shows the basic use of a backup object. 12 | func ExampleBackup() { 13 | // Open database connections. 14 | src, err := sqlite.OpenConn(os.Args[1]) 15 | if err != nil { 16 | fmt.Fprintln(os.Stderr, err) 17 | os.Exit(1) 18 | } 19 | defer src.Close() 20 | dst, err := sqlite.OpenConn(os.Args[2]) 21 | if err != nil { 22 | fmt.Fprintln(os.Stderr, err) 23 | os.Exit(1) 24 | } 25 | defer func() { 26 | if err := dst.Close(); err != nil { 27 | fmt.Fprintln(os.Stderr, err) 28 | } 29 | }() 30 | 31 | // Create Backup object. 32 | backup, err := sqlite.NewBackup(dst, "main", src, "main") 33 | if err != nil { 34 | fmt.Fprintln(os.Stderr, err) 35 | os.Exit(1) 36 | } 37 | 38 | // Perform online backup/copy. 39 | _, err1 := backup.Step(-1) 40 | err2 := backup.Close() 41 | if err1 != nil { 42 | fmt.Fprintln(os.Stderr, err) 43 | os.Exit(1) 44 | } 45 | if err2 != nil { 46 | fmt.Fprintln(os.Stderr, err) 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | // This example shows how to use Step 52 | // to prevent holding a read lock on the source database 53 | // during the entire copy. 54 | func ExampleBackup_Step() { 55 | // Open database connections. 56 | src, err := sqlite.OpenConn(os.Args[1]) 57 | if err != nil { 58 | fmt.Fprintln(os.Stderr, err) 59 | os.Exit(1) 60 | } 61 | defer src.Close() 62 | dst, err := sqlite.OpenConn(os.Args[2]) 63 | if err != nil { 64 | fmt.Fprintln(os.Stderr, err) 65 | os.Exit(1) 66 | } 67 | defer func() { 68 | if err := dst.Close(); err != nil { 69 | fmt.Fprintln(os.Stderr, err) 70 | } 71 | }() 72 | 73 | // Create Backup object. 74 | backup, err := sqlite.NewBackup(dst, "main", src, "main") 75 | if err != nil { 76 | fmt.Fprintln(os.Stderr, err) 77 | os.Exit(1) 78 | } 79 | defer func() { 80 | if err := backup.Close(); err != nil { 81 | fmt.Fprintln(os.Stderr, err) 82 | } 83 | }() 84 | 85 | // Each iteration of this loop copies 5 database pages, 86 | // waiting 250ms between iterations. 87 | for { 88 | more, err := backup.Step(5) 89 | if !more { 90 | if err != nil { 91 | fmt.Fprintln(os.Stderr, err) 92 | os.Exit(1) 93 | } 94 | break 95 | } 96 | time.Sleep(250 * time.Millisecond) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /backup_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "zombiezen.com/go/sqlite" 11 | "zombiezen.com/go/sqlite/sqlitex" 12 | ) 13 | 14 | func TestBackup(t *testing.T) { 15 | for _, isFull := range []bool{false, true} { 16 | name := "Full" 17 | if !isFull { 18 | name = "Incremental" 19 | } 20 | 21 | t.Run(name, func(t *testing.T) { 22 | src, err := sqlite.OpenConn(":memory:") 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | defer src.Close() 27 | err = sqlitex.ExecuteTransient(src, `CREATE TABLE foo (x INTEGER PRIMARY KEY NOT NULL);`, nil) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | err = sqlitex.ExecuteTransient(src, `INSERT INTO foo VALUES (1), (2), (3), (42);`, nil) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | dst, err := sqlite.OpenConn(":memory:") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | defer dst.Close() 41 | 42 | backup, err := sqlite.NewBackup(dst, "", src, "") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | defer func() { 47 | if err := backup.Close(); err != nil { 48 | t.Error(err) 49 | } 50 | }() 51 | 52 | if isFull { 53 | more, err := backup.Step(-1) 54 | if more || err != nil { 55 | t.Errorf("backup.Step(-1) = %t, %v; want false, ", more, err) 56 | } 57 | } else { 58 | for { 59 | more, err := backup.Step(1) 60 | t.Logf("after Step: Remaining() = %d, PageCount() = %d", backup.Remaining(), backup.PageCount()) 61 | if !more { 62 | if err != nil { 63 | t.Error("backup.Step(1):", err) 64 | } 65 | break 66 | } 67 | } 68 | } 69 | 70 | var got []int 71 | err = sqlitex.ExecuteTransient(dst, `SELECT x FROM foo ORDER BY x;`, &sqlitex.ExecOptions{ 72 | ResultFunc: func(stmt *sqlite.Stmt) error { 73 | got = append(got, stmt.ColumnInt(0)) 74 | return nil 75 | }, 76 | }) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | want := []int{1, 2, 3, 42} 81 | if diff := cmp.Diff(want, got); diff != "" { 82 | t.Errorf("foo (-want +got):\n%s", diff) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /blob.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlite 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "io" 24 | "unsafe" 25 | 26 | "modernc.org/libc" 27 | lib "modernc.org/sqlite/lib" 28 | ) 29 | 30 | const blobBufSize = 4096 31 | 32 | var ( 33 | mainCString = mustCString("main") 34 | tempCString = mustCString("temp") 35 | ) 36 | 37 | // OpenBlob opens a blob in a particular {database,table,column,row}. 38 | // 39 | // https://www.sqlite.org/c3ref/blob_open.html 40 | func (c *Conn) OpenBlob(dbn, table, column string, row int64, write bool) (*Blob, error) { 41 | if c == nil { 42 | return nil, fmt.Errorf("sqlite: open blob %q.%q: nil connection", table, column) 43 | } 44 | return c.openBlob(dbn, table, column, row, write) 45 | } 46 | 47 | func (c *Conn) openBlob(dbn, table, column string, row int64, write bool) (_ *Blob, err error) { 48 | cdb, freeCDB, err := cDBName(dbn) 49 | if err != nil { 50 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 51 | } 52 | defer freeCDB() 53 | var writeFlag int32 54 | if write { 55 | writeFlag = 1 56 | } 57 | buf, err := malloc(c.tls, blobBufSize) 58 | if err != nil { 59 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 60 | } 61 | defer func() { 62 | if err != nil { 63 | libc.Xfree(c.tls, buf) 64 | } 65 | }() 66 | 67 | ctable, err := libc.CString(table) 68 | if err != nil { 69 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 70 | } 71 | defer libc.Xfree(c.tls, ctable) 72 | ccolumn, err := libc.CString(column) 73 | if err != nil { 74 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 75 | } 76 | defer libc.Xfree(c.tls, ccolumn) 77 | 78 | blobPtrPtr, err := malloc(c.tls, ptrSize) 79 | if err != nil { 80 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 81 | } 82 | defer libc.Xfree(c.tls, blobPtrPtr) 83 | for { 84 | if err := c.interrupted(); err != nil { 85 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 86 | } 87 | res := ResultCode(lib.Xsqlite3_blob_open( 88 | c.tls, 89 | c.conn, 90 | cdb, 91 | ctable, 92 | ccolumn, 93 | row, 94 | writeFlag, 95 | blobPtrPtr, 96 | )) 97 | switch res { 98 | case ResultLockedSharedCache: 99 | if err := waitForUnlockNotify(c.tls, c.conn, c.unlockNote).ToError(); err != nil { 100 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) 101 | } 102 | // loop 103 | case ResultOK: 104 | blobPtr := *(*uintptr)(unsafe.Pointer(blobPtrPtr)) 105 | return &Blob{ 106 | conn: c, 107 | blob: blobPtr, 108 | buf: buf, 109 | size: lib.Xsqlite3_blob_bytes(c.tls, blobPtr), 110 | }, nil 111 | default: 112 | return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, c.extreserr(res)) 113 | } 114 | } 115 | } 116 | 117 | // Blob provides streaming access to SQLite blobs. 118 | type Blob struct { 119 | conn *Conn 120 | blob uintptr 121 | buf uintptr 122 | off int32 123 | size int32 124 | } 125 | 126 | func (blob *Blob) bufSlice() []byte { 127 | return libc.GoBytes(blob.buf, blobBufSize) 128 | } 129 | 130 | // Read reads up to len(p) bytes from the blob into p. 131 | // https://www.sqlite.org/c3ref/blob_read.html 132 | func (blob *Blob) Read(p []byte) (int, error) { 133 | if blob.blob == 0 { 134 | return 0, fmt.Errorf("sqlite: read blob: %w", errInvalidBlob) 135 | } 136 | if blob.off >= blob.size { 137 | return 0, io.EOF 138 | } 139 | if err := blob.conn.interrupted(); err != nil { 140 | return 0, fmt.Errorf("sqlite: read blob: %w", err) 141 | } 142 | if rem := blob.size - blob.off; len(p) > int(rem) { 143 | p = p[:rem] 144 | } 145 | fullLen := len(p) 146 | for len(p) > 0 { 147 | nn := int32(blobBufSize) 148 | if int(nn) > len(p) { 149 | nn = int32(len(p)) 150 | } 151 | res := ResultCode(lib.Xsqlite3_blob_read(blob.conn.tls, blob.blob, blob.buf, nn, blob.off)) 152 | if err := res.ToError(); err != nil { 153 | return fullLen - len(p), fmt.Errorf("sqlite: read blob: %w", err) 154 | } 155 | copy(p, blob.bufSlice()[:int(nn)]) 156 | p = p[nn:] 157 | blob.off += nn 158 | } 159 | return fullLen, nil 160 | } 161 | 162 | // WriteTo copies the blob to w until there's no more data to write or 163 | // an error occurs. 164 | func (blob *Blob) WriteTo(w io.Writer) (n int64, err error) { 165 | if blob.blob == 0 { 166 | return 0, fmt.Errorf("sqlite: read blob: %w", errInvalidBlob) 167 | } 168 | if blob.off >= blob.size { 169 | return 0, nil 170 | } 171 | if err := blob.conn.interrupted(); err != nil { 172 | return 0, fmt.Errorf("sqlite: read blob: %w", err) 173 | } 174 | for blob.off < blob.size { 175 | buf := blob.bufSlice() 176 | if remaining := int(blob.size - blob.off); len(buf) > remaining { 177 | buf = buf[:remaining] 178 | } 179 | res := ResultCode(lib.Xsqlite3_blob_read(blob.conn.tls, blob.blob, blob.buf, int32(len(buf)), blob.off)) 180 | if err := res.ToError(); err != nil { 181 | return n, fmt.Errorf("sqlite: read blob: %w", err) 182 | } 183 | nn, err := w.Write(buf) 184 | blob.off += int32(nn) 185 | n += int64(nn) 186 | if err != nil { 187 | return n, err 188 | } 189 | } 190 | return n, nil 191 | } 192 | 193 | // Write writes len(p) from p to the blob. 194 | // https://www.sqlite.org/c3ref/blob_write.html 195 | func (blob *Blob) Write(p []byte) (int, error) { 196 | if blob.blob == 0 { 197 | return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) 198 | } 199 | if err := blob.conn.interrupted(); err != nil { 200 | return 0, fmt.Errorf("sqlite: write blob: %w", err) 201 | } 202 | fullLen := len(p) 203 | for len(p) > 0 { 204 | nn := copy(blob.bufSlice(), p) 205 | res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) 206 | if err := res.ToError(); err != nil { 207 | return fullLen - len(p), fmt.Errorf("sqlite: write blob: %w", err) 208 | } 209 | p = p[nn:] 210 | blob.off += int32(nn) 211 | } 212 | return fullLen, nil 213 | } 214 | 215 | // WriteString writes s to the blob. 216 | // https://www.sqlite.org/c3ref/blob_write.html 217 | func (blob *Blob) WriteString(s string) (int, error) { 218 | if blob.blob == 0 { 219 | return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) 220 | } 221 | if err := blob.conn.interrupted(); err != nil { 222 | return 0, fmt.Errorf("sqlite: write blob: %w", err) 223 | } 224 | fullLen := len(s) 225 | for len(s) > 0 { 226 | nn := copy(blob.bufSlice(), s) 227 | res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) 228 | if err := res.ToError(); err != nil { 229 | return fullLen - len(s), fmt.Errorf("sqlite: write blob: %w", err) 230 | } 231 | s = s[nn:] 232 | blob.off += int32(nn) 233 | } 234 | return fullLen, nil 235 | } 236 | 237 | // ReadFrom copies data from r to the blob until EOF or error. 238 | func (blob *Blob) ReadFrom(r io.Reader) (n int64, err error) { 239 | if blob.blob == 0 { 240 | return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) 241 | } 242 | if err := blob.conn.interrupted(); err != nil { 243 | return 0, fmt.Errorf("sqlite: write blob: %w", err) 244 | } 245 | for { 246 | nn, err := r.Read(blob.bufSlice()) 247 | if nn > 0 { 248 | res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) 249 | if err := res.ToError(); err != nil { 250 | return n, fmt.Errorf("sqlite: write blob: %w", err) 251 | } 252 | n += int64(nn) 253 | blob.off += int32(nn) 254 | } 255 | if err != nil { 256 | if err == io.EOF { 257 | err = nil 258 | } 259 | return n, err 260 | } 261 | } 262 | } 263 | 264 | // Seek sets the offset for the next Read or Write and returns the offset. 265 | // Seeking past the end of the blob returns an error. 266 | func (blob *Blob) Seek(offset int64, whence int) (int64, error) { 267 | switch whence { 268 | case io.SeekStart: 269 | // use offset directly 270 | case io.SeekCurrent: 271 | offset += int64(blob.off) 272 | case io.SeekEnd: 273 | offset += int64(blob.size) 274 | default: 275 | return int64(blob.off), fmt.Errorf("sqlite: seek blob: invalid whence %d", whence) 276 | } 277 | if offset < 0 { 278 | return int64(blob.off), fmt.Errorf("sqlite: seek blob: negative offset %d", offset) 279 | } 280 | if offset > int64(blob.size) { 281 | return int64(blob.off), fmt.Errorf("sqlite: seek blob: offset %d is past size %d", offset, blob.size) 282 | } 283 | blob.off = int32(offset) 284 | return offset, nil 285 | } 286 | 287 | // Size returns the number of bytes in the blob. 288 | func (blob *Blob) Size() int64 { 289 | return int64(blob.size) 290 | } 291 | 292 | // Close releases any resources associated with the blob handle. 293 | // https://www.sqlite.org/c3ref/blob_close.html 294 | func (blob *Blob) Close() error { 295 | if blob.blob == 0 { 296 | return errInvalidBlob 297 | } 298 | libc.Xfree(blob.conn.tls, blob.buf) 299 | blob.buf = 0 300 | res := ResultCode(lib.Xsqlite3_blob_close(blob.conn.tls, blob.blob)) 301 | blob.blob = 0 302 | if err := res.ToError(); err != nil { 303 | return fmt.Errorf("sqlite: close blob: %w", err) 304 | } 305 | return nil 306 | } 307 | 308 | var errInvalidBlob = errors.New("invalid blob") 309 | 310 | // cDBName converts a database name into a C string. 311 | func cDBName(dbn string) (uintptr, func(), error) { 312 | switch dbn { 313 | case "", "main": 314 | return mainCString, func() {}, nil 315 | case "temp": 316 | return tempCString, func() {}, nil 317 | default: 318 | cdb, err := libc.CString(dbn) 319 | if err != nil { 320 | return 0, nil, err 321 | } 322 | return cdb, func() { libc.Xfree(nil, cdb) }, nil 323 | } 324 | } 325 | 326 | // TODO: Blob Reopen 327 | -------------------------------------------------------------------------------- /blocking_step.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlite 19 | 20 | import ( 21 | "fmt" 22 | "sync" 23 | "unsafe" 24 | 25 | "modernc.org/libc" 26 | "modernc.org/libc/sys/types" 27 | lib "modernc.org/sqlite/lib" 28 | ) 29 | 30 | // See https://sqlite.org/unlock_notify.html for detailed explanation. 31 | 32 | // unlockNote is a C-allocated struct used as a condition variable. 33 | type unlockNote struct { 34 | mu sync.Mutex 35 | wait sync.Mutex // held while fired == false 36 | fired bool 37 | } 38 | 39 | func allocUnlockNote(tls *libc.TLS) (uintptr, error) { 40 | ptr := libc.Xcalloc(tls, 1, types.Size_t(unsafe.Sizeof(unlockNote{}))) 41 | if ptr == 0 { 42 | return 0, fmt.Errorf("out of memory for unlockNote") 43 | } 44 | un := (*unlockNote)(unsafe.Pointer(ptr)) 45 | un.wait.Lock() 46 | return ptr, nil 47 | } 48 | 49 | func fireUnlockNote(tls *libc.TLS, ptr uintptr) { 50 | un := (*unlockNote)(unsafe.Pointer(ptr)) 51 | un.mu.Lock() 52 | if !un.fired { 53 | un.fired = true 54 | un.wait.Unlock() 55 | } 56 | un.mu.Unlock() 57 | } 58 | 59 | func unlockNotifyCallback(tls *libc.TLS, apArg uintptr, nArg int32) { 60 | for ; nArg > 0; nArg-- { 61 | fireUnlockNote(tls, *(*uintptr)(unsafe.Pointer(apArg))) 62 | // apArg is a C array of pointers. 63 | apArg += unsafe.Sizeof(uintptr(0)) 64 | } 65 | } 66 | 67 | func waitForUnlockNotify(tls *libc.TLS, db uintptr, unPtr uintptr) ResultCode { 68 | un := (*unlockNote)(unsafe.Pointer(unPtr)) 69 | if un.fired { 70 | un.wait.Lock() 71 | } 72 | un.fired = false 73 | 74 | cbPtr := cFuncPointer(unlockNotifyCallback) 75 | 76 | res := ResultCode(lib.Xsqlite3_unlock_notify(tls, db, cbPtr, unPtr)) 77 | 78 | if res == ResultOK { 79 | un.mu.Lock() 80 | fired := un.fired 81 | un.mu.Unlock() 82 | if !fired { 83 | un.wait.Lock() 84 | un.wait.Unlock() 85 | } 86 | } 87 | return res 88 | } 89 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/README.md: -------------------------------------------------------------------------------- 1 | # Migrating from `crawshaw.io/sqlite` 2 | 3 | `zombiezen.com/go/sqlite` is designed to mostly be a drop-in replacement for 4 | `crawshaw.io/sqlite`. However, there are some incompatible API changes. To aid 5 | in migrating, I've prepared a program that rewrites Go source code using 6 | `crawshaw.io/sqlite` to use `zombiezen.com/go/sqlite`. 7 | 8 | ## Installation 9 | 10 | ```shell 11 | go install zombiezen.com/go/sqlite/cmd/zombiezen-sqlite-migrate@latest 12 | ``` 13 | 14 | ## Usage 15 | 16 | Preview changes with: 17 | 18 | ```shell 19 | zombiezen-sqlite-migrate ./... 20 | ``` 21 | 22 | And then apply them with: 23 | 24 | ```shell 25 | zombiezen-sqlite-migrate -w ./... 26 | ``` 27 | 28 | ## Automatically fixed changes 29 | 30 | The `zombiezen-sqlite-migrate` tool automatically makes a number of mechanical 31 | changes beyond changing the import paths to preserve semantics. 32 | 33 | - **`ErrorCode` renamed to `ResultCode`.** The `crawshaw.io/sqlite.ErrorCode` type 34 | actually represents a SQLite [result code][], not just error codes. To better 35 | capture this, the new type is named `zombiezen.com/go/sqlite.ResultCode`. 36 | - **Friendlier constant names.** The constant names in `crawshaw.io/sqlite` 37 | are written in upper snake case with `SQLITE_` prefixed (e.g. 38 | `sqlite.SQLITE_OK`); the constant names in `zombiezen.com/go/sqlite` are 39 | written in upper camel case with the type prefixed (e.g. `sqlite.ResultOK`). 40 | - `sqlitex.File` and `sqlitex.Buffer` are in `zombiezen.com/go/sqlite/sqlitefile` 41 | instead of `zombiezen.com/go/sqlite/sqlitex`. 42 | - The **session API** has some symbols renamed for clarity. 43 | - `sqlitex.ExecFS` will rename to `sqlitex.ExecuteFS`, 44 | `sqlitex.ExecTransientFS` will rename to `sqlitex.ExecuteTransientFS`, 45 | and `sqlitex.ExecScriptFS` will rename to `sqlitex.ExecuteScriptFS`. 46 | 47 | [result code]: https://sqlite.org/rescode.html 48 | 49 | ## Changes that require manual effort 50 | 51 | Other usages of the `crawshaw.io/sqlite` may require manual effort to migrate, 52 | but the `zombiezen-sqlite-migrate` tool will point them out. 53 | 54 | ### Application-Defined Functions 55 | 56 | The `crawshaw.io/sqlite.Conn.CreateFunction` method and supporting APIs like 57 | `Context` and `Value` have been re-tooled in `zombiezen.com/go/sqlite` with 58 | better ergonomics. See the [`CreateFunction` reference][] for more details. 59 | 60 | [`CreateFunction` reference]: https://pkg.go.dev/zombiezen.com/go/sqlite#Conn.CreateFunction 61 | 62 | ### Removed `Blob` methods 63 | 64 | `zombiezen.com/go/sqlite.Blob` does not implement the [`io.ReaderAt`][] or 65 | [`io.WriterAt`][] interfaces. Technically, neither did `crawshaw.io/sqlite.Blob`, 66 | because it was not safe to call in parallel, which these interfaces require. 67 | To avoid these methods being used incorrectly, I removed them. 68 | 69 | I also removed the unused embedded interface fields. 70 | 71 | [`io.ReaderAt`]: https://pkg.go.dev/io#ReaderAt 72 | [`io.WriterAt`]: https://pkg.go.dev/io#WriterAt 73 | 74 | ### No dedicated `sqlite.Error` type 75 | 76 | I don't want to commit to a specific error type in `zombiezen.com/go/sqlite`, so 77 | there I removed the `Error` type. [`zombiezen.com/go/sqlite.ErrCode`][] still 78 | extracts the `ResultCode` from an `error`, which covers most needs. 79 | 80 | [`zombiezen.com/go/sqlite.ErrCode`]: https://pkg.go.dev/zombiezen.com/go/sqlite#ErrCode 81 | 82 | ### Authorizer `Action` 83 | 84 | The [`zombiezen.com/go/sqlite.Action`][] uses accessor methods instead of struct 85 | fields. Custom `Authorizer`s will need to be rewritten. 86 | 87 | [`zombiezen.com/go/sqlite.Action`]: https://pkg.go.dev/zombiezen.com/go/sqlite#Action 88 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/go.mod: -------------------------------------------------------------------------------- 1 | module zombiezen.com/go/sqlite/cmd/zombiezen-sqlite-migrate 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.3 7 | golang.org/x/tools v0.16.0 8 | zombiezen.com/go/bass v0.0.0-20210402215001-cb0af0b391a4 9 | ) 10 | 11 | require ( 12 | golang.org/x/mod v0.14.0 // indirect 13 | golang.org/x/sys v0.14.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/migrate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "go/format" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/google/go-cmp/cmp" 16 | "golang.org/x/tools/go/packages" 17 | "golang.org/x/tools/go/packages/packagestest" 18 | ) 19 | 20 | func TestProcess(t *testing.T) { 21 | crawshawModule, err := skeletonModule("crawshaw.io/sqlite") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | bassModule, err := skeletonModule("zombiezen.com/go/bass") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | sqliteModule, err := skeletonModule("zombiezen.com/go/sqlite") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | rootDir := filepath.Join("testdata", "TestProcess") 34 | testRunContents, err := os.ReadDir(rootDir) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | const mainPkgPath = "example.com/foo" 39 | const mainFilename = "file.go" 40 | for _, ent := range testRunContents { 41 | name := ent.Name() 42 | if !ent.IsDir() || strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { 43 | continue 44 | } 45 | t.Run(name, func(t *testing.T) { 46 | dir := filepath.Join(rootDir, name) 47 | want, err := os.ReadFile(filepath.Join(dir, "want.go")) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | want, err = format.Source(want) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | originalFile := filepath.Join(dir, "original.go") 57 | e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ 58 | crawshawModule, 59 | bassModule, 60 | sqliteModule, 61 | { 62 | Name: mainPkgPath, 63 | Files: map[string]any{ 64 | mainFilename: packagestest.Copy(originalFile), 65 | }, 66 | }, 67 | }) 68 | cfg := new(packages.Config) 69 | *cfg = *e.Config 70 | cfg.Mode = processMode 71 | pkgs, err := packages.Load(cfg, "pattern="+mainPkgPath) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if len(pkgs) != 1 { 76 | t.Fatalf("Found %d packages; want 1", len(pkgs)) 77 | } 78 | pkg := pkgs[0] 79 | if len(pkg.Errors) > 0 { 80 | for _, err := range pkg.Errors { 81 | t.Errorf("Load %s: %v", originalFile, err) 82 | } 83 | return 84 | } 85 | if len(pkg.Syntax) != 1 { 86 | t.Fatalf("Found %d parsed files; want 1", len(pkg.Syntax)) 87 | } 88 | file := pkg.Syntax[0] 89 | 90 | for _, err := range process(pkg, file) { 91 | t.Logf("process: %v", err) 92 | } 93 | 94 | got, err := formatFile(new(bytes.Buffer), originalFile, pkg.Fset, file) 95 | if err != nil { 96 | t.Fatalf("Formatting output: %v", err) 97 | } 98 | if diff := cmp.Diff(want, got); diff != "" { 99 | t.Errorf("diff a/%s b/%s:\n%s", mainFilename, mainFilename, diff) 100 | } 101 | }) 102 | } 103 | } 104 | 105 | func skeletonModule(importPath string) (packagestest.Module, error) { 106 | mod := packagestest.Module{ 107 | Name: importPath, 108 | Files: make(map[string]any), 109 | } 110 | dir := filepath.Join("testdata", "skeleton", filepath.FromSlash(importPath)) 111 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 112 | if err != nil { 113 | return err 114 | } 115 | if info.IsDir() { 116 | return nil 117 | } 118 | relpath := strings.TrimPrefix(path, dir+string(filepath.Separator)) 119 | mod.Files[filepath.ToSlash(relpath)] = packagestest.Copy(path) 120 | return nil 121 | }) 122 | if err != nil { 123 | return packagestest.Module{}, fmt.Errorf("load skeleton module %q: %w", importPath, err) 124 | } 125 | return mod, nil 126 | } 127 | 128 | type logWriter struct { 129 | logger interface{ Logf(string, ...any) } 130 | buf []byte 131 | } 132 | 133 | func (lw *logWriter) Write(p []byte) (int, error) { 134 | lastLF := bytes.LastIndexByte(p, '\n') 135 | if lastLF == -1 { 136 | lw.buf = append(lw.buf, p...) 137 | return len(p), nil 138 | } 139 | if len(lw.buf) > 0 { 140 | lw.buf = append(lw.buf, p[:lastLF]...) 141 | lw.logger.Logf("%s", lw.buf) 142 | lw.buf = lw.buf[:0] 143 | } else { 144 | lw.logger.Logf("%s", p[:lastLF]) 145 | } 146 | lw.buf = append(lw.buf, p[lastLF+1:]...) 147 | return len(p), nil 148 | } 149 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/AnonymousInterface/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | func main() { 7 | var foo interface { 8 | Bar() 9 | } 10 | _ = foo 11 | } 12 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/AnonymousInterface/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | func main() { 7 | var foo interface { 8 | Bar() 9 | } 10 | _ = foo 11 | } 12 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Autocommit/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "crawshaw.io/sqlite" 7 | 8 | func main() { 9 | var db *sqlite.Conn 10 | db, _ = sqlite.OpenConn(":memory:", 0) 11 | db.GetAutocommit() 12 | } 13 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Autocommit/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "zombiezen.com/go/sqlite" 7 | 8 | func main() { 9 | var db *sqlite.Conn 10 | db, _ = sqlite.OpenConn(":memory:", 0) 11 | db.AutocommitEnabled() 12 | } 13 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ErrorCode/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "crawshaw.io/sqlite" 7 | 8 | func main() { 9 | var res sqlite.ErrorCode = sqlite.SQLITE_OK 10 | _ = res 11 | } 12 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ErrorCode/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "zombiezen.com/go/sqlite" 7 | 8 | func main() { 9 | var res sqlite.ResultCode = sqlite.ResultOK 10 | _ = res 11 | } 12 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Exec/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import ( 7 | "crawshaw.io/sqlite" 8 | "crawshaw.io/sqlite/sqlitex" 9 | "zombiezen.com/go/bass/sql/sqlitefile" 10 | ) 11 | 12 | func main() { 13 | var conn *sqlite.Conn 14 | var file *sqlitex.File 15 | sqlitefile.ExecScript(conn, nil, "foo.sql", &sqlitefile.ExecOptions{ 16 | Args: []interface{}{1, "foo"}, 17 | }) 18 | sqlitex.Exec(conn, `SELECT 1;`, nil) 19 | _ = file 20 | } 21 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Exec/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import ( 7 | "zombiezen.com/go/sqlite" 8 | sqlitefile2 "zombiezen.com/go/sqlite/sqlitefile" 9 | "zombiezen.com/go/sqlite/sqlitex" 10 | ) 11 | 12 | func main() { 13 | var conn *sqlite.Conn 14 | var file *sqlitefile2.File 15 | sqlitex.ExecuteScriptFS(conn, nil, "foo.sql", &sqlitex.ExecOptions{ 16 | Args: []interface{}{1, "foo"}, 17 | }) 18 | sqlitex.Exec(conn, `SELECT 1;`, nil) 19 | _ = file 20 | } 21 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ExecScriptFS/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "zombiezen.com/go/sqlite/sqlitex" 7 | 8 | func main() { 9 | _ = sqlitex.ExecScriptFS 10 | } 11 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ExecScriptFS/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "zombiezen.com/go/sqlite/sqlitex" 7 | 8 | func main() { 9 | _ = sqlitex.ExecuteScriptFS 10 | } 11 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/File/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "crawshaw.io/sqlite/sqlitex" 7 | 8 | func main() { 9 | var f *sqlitex.File 10 | f, _ = sqlitex.NewFile(nil) 11 | var buf *sqlitex.Buffer 12 | buf, _ = sqlitex.NewBuffer(nil) 13 | 14 | _, _ = f, buf 15 | } 16 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/File/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import ( 7 | "zombiezen.com/go/sqlite/sqlitefile" 8 | ) 9 | 10 | func main() { 11 | var f *sqlitefile.File 12 | f, _ = sqlitefile.NewFile(nil) 13 | var buf *sqlitefile.Buffer 14 | buf, _ = sqlitefile.NewBuffer(nil) 15 | 16 | _, _ = f, buf 17 | } 18 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ImportRewrite/original.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "crawshaw.io/sqlite" 7 | 8 | func main() { 9 | var db *sqlite.Conn 10 | var err error 11 | db, err = sqlite.OpenConn(":memory:", 0) 12 | _, _ = db, err 13 | } 14 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ImportRewrite/want.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package main 5 | 6 | import "zombiezen.com/go/sqlite" 7 | 8 | func main() { 9 | var db *sqlite.Conn 10 | var err error 11 | db, err = sqlite.OpenConn(":memory:", 0) 12 | _, _ = db, err 13 | } 14 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlite/sqlitex/stubs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Test stubs for crawshaw.io/sqlite/sqlitex. 5 | package sqlitex 6 | 7 | import "crawshaw.io/sqlite" 8 | 9 | type File struct { 10 | io.Reader 11 | io.Writer 12 | io.Seeker 13 | } 14 | 15 | func NewFile(conn *sqlite.Conn) (*File, error) { return nil, nil } 16 | 17 | func NewFileSize(conn *sqlite.Conn, initSize int) (*File, error) { return nil, nil } 18 | 19 | type Buffer struct { 20 | io.Reader 21 | io.Writer 22 | io.ByteScanner 23 | } 24 | 25 | func NewBuffer(conn *sqlite.Conn) (*Buffer, error) { return nil, nil } 26 | 27 | func NewBufferSize(conn *sqlite.Conn, pageSize int) (*Buffer, error) { return nil, nil } 28 | 29 | func Exec(conn *sqlite.Conn, query string, resultFn func(stmt *sqlite.Stmt) error, args ...interface{}) error { 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlite/stubs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Test stubs for crawshaw.io/sqlite. 5 | package sqlite 6 | 7 | type Conn struct { 8 | } 9 | 10 | func OpenConn(path string, flags OpenFlags) (*Conn, error) { return nil, nil } 11 | 12 | func (c *Conn) GetAutocommit() bool { return false } 13 | 14 | type ErrorCode int 15 | 16 | const SQLITE_OK = ErrorCode(0) 17 | 18 | type Stmt struct { 19 | } 20 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/bass/sql/sqlitefile/stubs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Test stubs for zombiezen.com/go/bass/sql/sqlitefile. 5 | package sqlitefile 6 | 7 | import ( 8 | "os" 9 | 10 | "crawshaw.io/sqlite" 11 | ) 12 | 13 | type ExecOptions struct { 14 | Args []interface{} 15 | Named map[string]interface{} 16 | ResultFunc func(stmt *sqlite.Stmt) error 17 | } 18 | 19 | func ExecScript(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) (err error) { 20 | return nil 21 | } 22 | 23 | // FS is a copy of Go 1.16's io/fs.FS interface. 24 | type FS interface { 25 | Open(name string) (File, error) 26 | } 27 | 28 | // File is a copy of Go 1.16's io/fs.File interface. 29 | type File interface { 30 | Stat() (os.FileInfo, error) 31 | Read([]byte) (int, error) 32 | Close() error 33 | } 34 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlite/sqlitex/stubs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Test stubs for zombiezen.com/go/sqlite/sqlitex. 5 | package sqlitex 6 | 7 | import ( 8 | "os" 9 | 10 | "zombiezen.com/go/sqlite" 11 | ) 12 | 13 | type ExecOptions struct { 14 | Args []interface{} 15 | Named map[string]interface{} 16 | ResultFunc func(stmt *sqlite.Stmt) error 17 | } 18 | 19 | func ExecScriptFS(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) error { 20 | return nil 21 | } 22 | 23 | func ExecuteScriptFS(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) error { 24 | return nil 25 | } 26 | 27 | // FS is a copy of Go 1.16's io/fs.FS interface. 28 | type FS interface { 29 | Open(name string) (File, error) 30 | } 31 | 32 | // File is a copy of Go 1.16's io/fs.File interface. 33 | type File interface { 34 | Stat() (os.FileInfo, error) 35 | Read([]byte) (int, error) 36 | Close() error 37 | } 38 | -------------------------------------------------------------------------------- /cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlite/stubs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Test stubs for zombiezen.com/go/sqlite. 5 | package sqlite 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | type Conn struct{} 12 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | /* 19 | Package sqlite provides a Go interface to SQLite 3. 20 | 21 | The semantics of this package are deliberately close to the [SQLite3 C API]. 22 | See the [official C API introduction] for an overview of the basics. 23 | 24 | An SQLite connection is represented by a [*Conn]. 25 | Connections cannot be used concurrently. 26 | A typical Go program will create a pool of connections 27 | (e.g. by using [zombiezen.com/go/sqlite/sqlitex.NewPool] 28 | to create a [*zombiezen.com/go/sqlite/sqlitex.Pool]) 29 | so goroutines can borrow a connection 30 | while they need to talk to the database. 31 | 32 | This package assumes SQLite will be used concurrently by the 33 | process through several connections, so the build options for 34 | SQLite enable multi-threading and the [shared cache]. 35 | 36 | The implementation automatically handles shared cache locking, 37 | see the documentation on [Stmt.Step] for details. 38 | 39 | The optional SQLite 3 extensions compiled in are: 40 | session, FTS5, RTree, JSON1, and GeoPoly. 41 | 42 | This is not a [database/sql] driver. 43 | For helper functions to make it easier to execute statements, 44 | see the [zombiezen.com/go/sqlite/sqlitex] package. 45 | 46 | # Statement Caching 47 | 48 | Statements are prepared with the [Conn.Prepare] and [Conn.PrepareTransient] methods. 49 | When using [Conn.Prepare], statements are keyed inside a connection by the 50 | original query string used to create them. This means long-running 51 | high-performance code paths can write: 52 | 53 | stmt, err := conn.Prepare("SELECT ...") 54 | 55 | After all the connections in a pool have been warmed up by passing 56 | through one of these Prepare calls, subsequent calls are simply a 57 | map lookup that returns an existing statement. 58 | 59 | # Transactions 60 | 61 | SQLite transactions can be managed manually with this package 62 | by directly executing BEGIN / COMMIT / ROLLBACK or 63 | SAVEPOINT / RELEASE / ROLLBACK statements, 64 | but there are also helper functions available in [zombiezen.com/go/sqlite/sqlitex]: 65 | 66 | - [zombiezen.com/go/sqlite/sqlitex.Transaction] 67 | - [zombiezen.com/go/sqlite/sqlitex.ImmediateTransaction] 68 | - [zombiezen.com/go/sqlite/sqlitex.ExclusiveTransaction] 69 | - [zombiezen.com/go/sqlite/sqlitex.Save] 70 | 71 | # Schema Migrations 72 | 73 | For simple schema migration needs, see the [zombiezen.com/go/sqlite/sqlitemigration] package. 74 | 75 | # User-Defined Functions 76 | 77 | Use [Conn.CreateFunction] to register Go functions for use as [SQL functions]. 78 | 79 | # Streaming Blobs 80 | 81 | The sqlite package supports the SQLite incremental I/O interface 82 | for streaming blob data into and out of the the database 83 | without loading the entire blob into a single []byte. 84 | (This is important when working either with very large blobs, 85 | or more commonly, a large number of moderate-sized blobs concurrently.) 86 | See [Conn.OpenBlob] for more details. 87 | 88 | # Deadlines and Cancellation 89 | 90 | Every connection can have a done channel associated with it 91 | using the [Conn.SetInterrupt] method. 92 | This is typically the channel returned by a [context.Context.Done] method. 93 | 94 | As database connections are long-lived, 95 | the [Conn.SetInterrupt] method can be called multiple times 96 | to reset the associated lifetime. 97 | 98 | [official C API introduction]: https://www.sqlite.org/cintro.html 99 | [shared cache]: https://www.sqlite.org/sharedcache.html 100 | [SQL functions]: https://sqlite.org/appfunc.html 101 | [SQLite3 C API]: https://www.sqlite.org/c3ref/intro.html. 102 | */ 103 | package sqlite 104 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite_test 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "regexp" 12 | "time" 13 | 14 | "golang.org/x/text/collate" 15 | "golang.org/x/text/language" 16 | "zombiezen.com/go/sqlite" 17 | "zombiezen.com/go/sqlite/sqlitex" 18 | ) 19 | 20 | func Example() { 21 | // Open an in-memory database. 22 | conn, err := sqlite.OpenConn(":memory:") 23 | if err != nil { 24 | // handle error 25 | } 26 | defer conn.Close() 27 | 28 | // Execute a query. 29 | err = sqlitex.ExecuteTransient(conn, "SELECT 'hello, world';", &sqlitex.ExecOptions{ 30 | ResultFunc: func(stmt *sqlite.Stmt) error { 31 | fmt.Println(stmt.ColumnText(0)) 32 | return nil 33 | }, 34 | }) 35 | if err != nil { 36 | // handle error 37 | } 38 | 39 | // Output: 40 | // hello, world 41 | } 42 | 43 | // This is the same as the main package example, but uses the SQLite 44 | // statement API instead of sqlitex. 45 | func Example_withoutX() { 46 | // Open an in-memory database. 47 | conn, err := sqlite.OpenConn(":memory:") 48 | if err != nil { 49 | // handle error 50 | } 51 | defer conn.Close() 52 | 53 | // Prepare a statement. 54 | stmt, _, err := conn.PrepareTransient("SELECT 'hello, world';") 55 | if err != nil { 56 | // handle error 57 | } 58 | // Transient statements must always be finalized. 59 | defer stmt.Finalize() 60 | 61 | for { 62 | row, err := stmt.Step() 63 | if err != nil { 64 | // handle error 65 | } 66 | if !row { 67 | break 68 | } 69 | fmt.Println(stmt.ColumnText(0)) 70 | } 71 | 72 | // Output: 73 | // hello, world 74 | } 75 | 76 | func ExampleConn_SetInterrupt() { 77 | conn, err := sqlite.OpenConn(":memory:") 78 | if err != nil { 79 | panic(err) 80 | } 81 | defer conn.Close() 82 | 83 | // You can use the Done() channel from a context to set deadlines and timeouts 84 | // on queries. 85 | ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) 86 | defer cancel() 87 | conn.SetInterrupt(ctx.Done()) 88 | } 89 | 90 | // This example shows how to register a basic scalar function. 91 | // 92 | // If you're looking to use regular expressions in your application, 93 | // use [zombiezen.com/go/sqlite/ext/refunc.Register]. 94 | func ExampleConn_CreateFunction() { 95 | conn, err := sqlite.OpenConn(":memory:") 96 | if err != nil { 97 | // handle error 98 | } 99 | defer conn.Close() 100 | 101 | // Add a regexp(pattern, string) function. 102 | err = conn.CreateFunction("regexp", &sqlite.FunctionImpl{ 103 | NArgs: 2, 104 | Deterministic: true, 105 | Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { 106 | re, err := regexp.Compile(args[0].Text()) 107 | if err != nil { 108 | return sqlite.Value{}, fmt.Errorf("regexp: %w", err) 109 | } 110 | found := 0 111 | if re.MatchString(args[1].Text()) { 112 | found = 1 113 | } 114 | return sqlite.IntegerValue(int64(found)), nil 115 | }, 116 | }) 117 | if err != nil { 118 | // handle error 119 | } 120 | 121 | matches, err := sqlitex.ResultBool(conn.Prep(`SELECT regexp('fo+', 'foo');`)) 122 | if err != nil { 123 | // handle error 124 | } 125 | fmt.Println("First matches:", matches) 126 | 127 | matches, err = sqlitex.ResultBool(conn.Prep(`SELECT regexp('fo+', 'bar');`)) 128 | if err != nil { 129 | // handle error 130 | } 131 | fmt.Println("Second matches:", matches) 132 | 133 | // Output: 134 | // First matches: true 135 | // Second matches: false 136 | } 137 | 138 | // This example shows the same regexp function as in the CreateFunction example, 139 | // but it uses auxiliary data to avoid recompiling the regular expression. 140 | // 141 | // This is the implementation used in [zombiezen.com/go/sqlite/ext/refunc]. 142 | func ExampleContext_AuxData() { 143 | conn, err := sqlite.OpenConn(":memory:") 144 | if err != nil { 145 | // handle error 146 | } 147 | defer conn.Close() 148 | 149 | // Add a regexp(pattern, string) function. 150 | usedAux := false 151 | err = conn.CreateFunction("regexp", &sqlite.FunctionImpl{ 152 | NArgs: 2, 153 | Deterministic: true, 154 | Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { 155 | // First: attempt to retrieve the compiled regexp from a previous call. 156 | re, ok := ctx.AuxData(0).(*regexp.Regexp) 157 | if ok { 158 | usedAux = true 159 | } else { 160 | // Auxiliary data not present. Either this is the first call with this 161 | // argument, or SQLite has discarded the auxiliary data. 162 | var err error 163 | re, err = regexp.Compile(args[0].Text()) 164 | if err != nil { 165 | return sqlite.Value{}, fmt.Errorf("regexp: %w", err) 166 | } 167 | // Store the auxiliary data for future calls. 168 | ctx.SetAuxData(0, re) 169 | } 170 | 171 | found := 0 172 | if re.MatchString(args[1].Text()) { 173 | found = 1 174 | } 175 | return sqlite.IntegerValue(int64(found)), nil 176 | }, 177 | }) 178 | if err != nil { 179 | // handle error 180 | } 181 | 182 | const query = `WITH test_strings(i, s) AS (VALUES (1, 'foo'), (2, 'bar')) ` + 183 | `SELECT i, regexp('fo+', s) FROM test_strings ORDER BY i;` 184 | err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ 185 | ResultFunc: func(stmt *sqlite.Stmt) error { 186 | fmt.Printf("Match %d: %t\n", stmt.ColumnInt(0), stmt.ColumnInt(1) != 0) 187 | return nil 188 | }, 189 | }) 190 | if err != nil { 191 | // handle error 192 | } 193 | if usedAux { 194 | fmt.Println("Used aux data to speed up query!") 195 | } 196 | // Output: 197 | // Match 1: true 198 | // Match 2: false 199 | // Used aux data to speed up query! 200 | } 201 | 202 | func ExampleBlob() { 203 | // Create a new database with a "blobs" table with a single column, "myblob". 204 | conn, err := sqlite.OpenConn(":memory:") 205 | if err != nil { 206 | // handle error 207 | } 208 | defer conn.Close() 209 | err = sqlitex.ExecuteTransient(conn, `CREATE TABLE blobs (myblob blob);`, nil) 210 | if err != nil { 211 | // handle error 212 | } 213 | 214 | // Insert a new row with enough space for the data we want to insert. 215 | const dataToInsert = "Hello, World!" 216 | err = sqlitex.ExecuteTransient( 217 | conn, 218 | `INSERT INTO blobs (myblob) VALUES (zeroblob(?));`, 219 | &sqlitex.ExecOptions{ 220 | Args: []any{len(dataToInsert)}, 221 | }, 222 | ) 223 | if err != nil { 224 | // handle error 225 | } 226 | 227 | // Open a handle to the "myblob" column on the row we just inserted. 228 | blob, err := conn.OpenBlob("", "blobs", "myblob", conn.LastInsertRowID(), true) 229 | if err != nil { 230 | // handle error 231 | } 232 | _, writeErr := blob.WriteString(dataToInsert) 233 | closeErr := blob.Close() 234 | if writeErr != nil { 235 | // handle error 236 | } 237 | if closeErr != nil { 238 | // handle error 239 | } 240 | 241 | // Read back the blob. 242 | var data []byte 243 | err = sqlitex.ExecuteTransient(conn, `SELECT myblob FROM blobs;`, &sqlitex.ExecOptions{ 244 | ResultFunc: func(stmt *sqlite.Stmt) error { 245 | data = make([]byte, stmt.ColumnLen(0)) 246 | stmt.ColumnBytes(0, data) 247 | return nil 248 | }, 249 | }) 250 | if err != nil { 251 | // handle error 252 | } 253 | fmt.Printf("%s\n", data) 254 | 255 | // Output: 256 | // Hello, World! 257 | } 258 | 259 | func ExampleConn_SetAuthorizer() { 260 | // Create a new database. 261 | conn, err := sqlite.OpenConn(":memory:") 262 | if err != nil { 263 | // handle error 264 | } 265 | defer conn.Close() 266 | 267 | // Set an authorizer that prevents any mutations. 268 | err = conn.SetAuthorizer(sqlite.AuthorizeFunc(func(action sqlite.Action) sqlite.AuthResult { 269 | typ := action.Type() 270 | if typ == sqlite.OpSelect || 271 | typ == sqlite.OpRead || 272 | // Permit function calls. 273 | typ == sqlite.OpFunction || 274 | // Permit transactions. 275 | typ == sqlite.OpTransaction || 276 | typ == sqlite.OpSavepoint { 277 | return sqlite.AuthResultOK 278 | } 279 | return sqlite.AuthResultDeny 280 | })) 281 | if err != nil { 282 | // handle error 283 | } 284 | 285 | // Authorizers operate during statement preparation, so this will succeed: 286 | stmt, _, err := conn.PrepareTransient(`SELECT 'Hello, World!';`) 287 | if err != nil { 288 | panic(err) 289 | } else { 290 | fmt.Println("Read-only statement prepared!") 291 | if err := stmt.Finalize(); err != nil { 292 | panic(err) 293 | } 294 | } 295 | 296 | // But this will not: 297 | stmt, _, err = conn.PrepareTransient(`CREATE TABLE foo (id INTEGER PRIMARY KEY);`) 298 | if err != nil { 299 | fmt.Println("Prepare CREATE TABLE failed with code", sqlite.ErrCode(err)) 300 | } else if err := stmt.Finalize(); err != nil { 301 | panic(err) 302 | } 303 | // Output: 304 | // Read-only statement prepared! 305 | // Prepare CREATE TABLE failed with code SQLITE_AUTH 306 | } 307 | 308 | // This example shows how to use a changegroup to produce similar results to 309 | // a call to ConcatChangesets. 310 | func ExampleChangegroup() { 311 | // Get changesets from somewhere. 312 | var changeset1, changeset2 io.Reader 313 | 314 | // Create a changegroup. 315 | grp := new(sqlite.Changegroup) 316 | defer grp.Clear() 317 | 318 | // Add changesets to the changegroup. 319 | if err := grp.Add(changeset1); err != nil { 320 | // Handle error 321 | } 322 | if err := grp.Add(changeset2); err != nil { 323 | // Handle error 324 | } 325 | 326 | // Write the changegroup to a buffer. 327 | output := new(bytes.Buffer) 328 | if _, err := grp.WriteTo(output); err != nil { 329 | // Handle error 330 | } 331 | } 332 | 333 | func ExampleConn_SetCollation() { 334 | // Create a new database. 335 | conn, err := sqlite.OpenConn(":memory:") 336 | if err != nil { 337 | // handle error 338 | } 339 | defer conn.Close() 340 | 341 | // Override the built-in NOCASE collating sequence 342 | // to be Unicode aware. 343 | nocaseCollator := collate.New(language.Und, collate.IgnoreCase) 344 | if err := conn.SetCollation("NOCASE", nocaseCollator.CompareString); err != nil { 345 | // handle error 346 | } 347 | 348 | // Create a table that uses the NOCASE collating sequence. 349 | err = sqlitex.ExecuteScript(conn, ` 350 | CREATE TABLE foo (mytext TEXT COLLATE NOCASE); 351 | 352 | INSERT INTO foo VALUES 353 | ('atext'), 354 | ('btext'), 355 | ('ctext'), 356 | ('ątext'), 357 | ('ćtext'); 358 | `, nil) 359 | if err != nil { 360 | // handle error 361 | } 362 | 363 | // The column will be implicitly ordered using its collating sequence. 364 | err = sqlitex.ExecuteTransient(conn, `SELECT mytext FROM foo ORDER BY mytext ASC;`, &sqlitex.ExecOptions{ 365 | ResultFunc: func(stmt *sqlite.Stmt) error { 366 | fmt.Println(stmt.ColumnText(0)) 367 | return nil 368 | }, 369 | }) 370 | if err != nil { 371 | // handle error 372 | } 373 | // Output: 374 | // atext 375 | // ątext 376 | // btext 377 | // ctext 378 | // ćtext 379 | } 380 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | // Expose sqlite internals to tests in sqlite_test package. 19 | 20 | package sqlite 21 | 22 | // TODO(maybe) 23 | // func ConnCount(conn *Conn) int { return conn.count } 24 | 25 | func InterruptedStmt(conn *Conn, query string) *Stmt { 26 | return &Stmt{ 27 | conn: conn, 28 | query: query, 29 | colNames: make(map[string]int), 30 | prepInterrupt: true, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ext/generateseries/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package generateseries_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "zombiezen.com/go/sqlite" 11 | "zombiezen.com/go/sqlite/ext/generateseries" 12 | "zombiezen.com/go/sqlite/sqlitex" 13 | ) 14 | 15 | func Example() { 16 | conn, err := sqlite.OpenConn(":memory:") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer conn.Close() 21 | 22 | if err := generateseries.Register(conn); err != nil { 23 | log.Fatal(err) 24 | } 25 | err = sqlitex.ExecuteTransient( 26 | conn, 27 | `SELECT * FROM generate_series(0, 20, 5);`, 28 | &sqlitex.ExecOptions{ 29 | ResultFunc: func(stmt *sqlite.Stmt) error { 30 | fmt.Printf("%2d\n", stmt.ColumnInt(0)) 31 | return nil 32 | }, 33 | }, 34 | ) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | // Output: 40 | // 0 41 | // 5 42 | // 10 43 | // 15 44 | // 20 45 | } 46 | -------------------------------------------------------------------------------- /ext/generateseries/generateseries.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Package generateseries provides a port of the [generate_series] table-valued function 5 | // from the SQLite tree. 6 | // 7 | // [generate_series]: https://sqlite.org/src/file/ext/misc/series.c 8 | package generateseries 9 | 10 | import ( 11 | "fmt" 12 | 13 | "zombiezen.com/go/sqlite" 14 | ) 15 | 16 | // Module is a virtual table module that can be registered with [sqlite.Conn.SetModule]. 17 | var Module = &sqlite.Module{ 18 | Connect: connect, 19 | } 20 | 21 | // Register registers the "generate_series" table-valued function on the given connection. 22 | func Register(c *sqlite.Conn) error { 23 | return c.SetModule("generate_series", Module) 24 | } 25 | 26 | type vtab struct{} 27 | 28 | const ( 29 | seriesColumnValue = iota 30 | seriesColumnStart 31 | seriesColumnStop 32 | seriesColumnStep 33 | ) 34 | 35 | func connect(c *sqlite.Conn, opts *sqlite.VTableConnectOptions) (sqlite.VTable, *sqlite.VTableConfig, error) { 36 | vtab := new(vtab) 37 | cfg := &sqlite.VTableConfig{ 38 | Declaration: "CREATE TABLE x(value,start hidden,stop hidden,step hidden)", 39 | AllowIndirect: true, 40 | } 41 | return vtab, cfg, nil 42 | } 43 | 44 | // BestIndex looks for equality constraints against the hidden start, stop, and step columns, 45 | // and if present, it uses those constraints to bound the sequence of generated values. 46 | // If the equality constraints are missing, it uses 0 for start, 4294967295 for stop, 47 | // and 1 for step. 48 | // BestIndex returns a small cost when both start and stop are available, 49 | // and a very large cost if either start or stop are unavailable. 50 | // This encourages the query planner to order joins such that the bounds of the 51 | // series are well-defined. 52 | // 53 | // SQLite will invoke this method one or more times 54 | // while planning a query that uses the generate_series virtual table. 55 | // This routine needs to create a query plan for each invocation 56 | // and compute an estimated cost for that plan. 57 | // 58 | // In this implementation ID.Num is used to represent the query plan. 59 | // ID.String is unused. 60 | // 61 | // The query plan is represented by bits in idxNum: 62 | // 63 | // (1) start = $value -- constraint exists 64 | // (2) stop = $value -- constraint exists 65 | // (4) step = $value -- constraint exists 66 | // (8) output in descending order 67 | func (vt *vtab) BestIndex(inputs *sqlite.IndexInputs) (*sqlite.IndexOutputs, error) { 68 | var idxNum int32 69 | startSeen := false 70 | var unusableMask uint 71 | aIdx := [3]int{-1, -1, -1} 72 | for i, c := range inputs.Constraints { 73 | if c.Column < seriesColumnStart { 74 | continue 75 | } 76 | col := c.Column - seriesColumnStart // [0, 2] 77 | mask := uint(1 << col) 78 | if col == 0 { 79 | startSeen = true 80 | } 81 | if !c.Usable { 82 | unusableMask |= mask 83 | continue 84 | } 85 | if c.Op == sqlite.IndexConstraintEq { 86 | idxNum |= int32(mask) 87 | aIdx[col] = i 88 | } 89 | } 90 | outputs := &sqlite.IndexOutputs{ 91 | ID: sqlite.IndexID{Num: idxNum}, 92 | ConstraintUsage: make([]sqlite.IndexConstraintUsage, len(inputs.Constraints)), 93 | } 94 | nArg := 0 95 | for _, j := range aIdx { 96 | if j >= 0 { 97 | nArg++ 98 | outputs.ConstraintUsage[j] = sqlite.IndexConstraintUsage{ 99 | ArgvIndex: nArg, 100 | Omit: true, 101 | } 102 | } 103 | } 104 | if !startSeen { 105 | return nil, fmt.Errorf("first argument to \"generate_series()\" missing or unusable") 106 | } 107 | if unusableMask&^uint(idxNum) != 0 { 108 | // The start, stop, and step columns are inputs. 109 | // Therefore if there are unusable constraints on any of start, stop, or step then 110 | // this plan is unusable. 111 | return nil, sqlite.ResultConstraint.ToError() 112 | } 113 | if idxNum&3 == 3 { 114 | // Both start= and stop= boundaries are available. 115 | // This is the preferred case. 116 | if idxNum&4 != 0 { 117 | outputs.EstimatedCost = 1 118 | } else { 119 | outputs.EstimatedCost = 2 120 | } 121 | outputs.EstimatedRows = 1000 122 | if len(inputs.OrderBy) >= 1 && inputs.OrderBy[0].Column == 0 { 123 | if inputs.OrderBy[0].Desc { 124 | idxNum |= 8 125 | } else { 126 | idxNum |= 16 127 | } 128 | outputs.OrderByConsumed = true 129 | } 130 | } else { 131 | // If either boundary is missing, we have to generate a huge span of numbers. 132 | // Make this case very expensive so that the query planner will work hard to avoid it. 133 | outputs.EstimatedRows = 2147483647 134 | } 135 | return outputs, nil 136 | } 137 | 138 | func (vt *vtab) Open() (sqlite.VTableCursor, error) { 139 | return new(cursor), nil 140 | } 141 | 142 | func (vt *vtab) Disconnect() error { 143 | return nil 144 | } 145 | 146 | func (vt *vtab) Destroy() error { 147 | return nil 148 | } 149 | 150 | type cursor struct { 151 | isDesc bool 152 | rowid int64 153 | value int64 154 | mnValue int64 155 | mxValue int64 156 | step int64 157 | } 158 | 159 | // Filter is called to "rewind" the cursor object back to the first row of output. 160 | // This method is always called at least once 161 | // prior to any call to Column or RowID or EOF. 162 | // 163 | // The query plan selected by BestIndex is passed in the id parameter. 164 | // (id.String is not used in this implementation.) 165 | // id.Num is a bitmask showing which constraints are available: 166 | // 167 | // 1: start=VALUE 168 | // 2: stop=VALUE 169 | // 4: step=VALUE 170 | // 171 | // Also, if bit 8 is set, that means that the series should be output in descending order 172 | // rather than in ascending order. 173 | // If bit 16 is set, then output must appear in ascending order. 174 | // 175 | // This routine should initialize the cursor and position it 176 | // so that it is pointing at the first row, 177 | // or pointing off the end of the table (so that EOF will return true) 178 | // if the table is empty. 179 | func (cur *cursor) Filter(id sqlite.IndexID, argv []sqlite.Value) error { 180 | i := 0 181 | if id.Num&1 != 0 { 182 | cur.mnValue = argv[i].Int64() 183 | i++ 184 | } else { 185 | cur.mnValue = 0 186 | } 187 | if id.Num&2 != 0 { 188 | cur.mxValue = argv[i].Int64() 189 | i++ 190 | } else { 191 | cur.mxValue = 0xffffffff 192 | } 193 | if id.Num&4 != 0 { 194 | cur.step = argv[i].Int64() 195 | i++ 196 | if cur.step == 0 { 197 | cur.step = 1 198 | } else if cur.step < 0 { 199 | cur.step = -cur.step 200 | if id.Num&16 == 0 { 201 | id.Num |= 8 202 | } 203 | } 204 | } else { 205 | cur.step = 1 206 | } 207 | for _, arg := range argv { 208 | if arg.Type() == sqlite.TypeNull { 209 | // If any of the constraints have a NULL value, then return no rows. 210 | // See ticket https://www.sqlite.org/src/info/fac496b61722daf2 211 | cur.mnValue = 1 212 | cur.mxValue = 0 213 | break 214 | } 215 | } 216 | if id.Num&8 != 0 { 217 | cur.isDesc = true 218 | cur.value = cur.mxValue 219 | if cur.step > 0 { 220 | cur.value -= (cur.mxValue - cur.mnValue) % cur.step 221 | } 222 | } else { 223 | cur.isDesc = false 224 | cur.value = cur.mnValue 225 | } 226 | cur.rowid = 1 227 | return nil 228 | } 229 | 230 | func (cur *cursor) Next() error { 231 | if cur.isDesc { 232 | cur.value -= cur.step 233 | } else { 234 | cur.value += cur.step 235 | } 236 | cur.rowid++ 237 | return nil 238 | } 239 | 240 | func (cur *cursor) Column(i int, noChange bool) (sqlite.Value, error) { 241 | switch i { 242 | case seriesColumnValue: 243 | return sqlite.IntegerValue(cur.value), nil 244 | case seriesColumnStart: 245 | return sqlite.IntegerValue(cur.mnValue), nil 246 | case seriesColumnStop: 247 | return sqlite.IntegerValue(cur.mxValue), nil 248 | case seriesColumnStep: 249 | return sqlite.IntegerValue(cur.step), nil 250 | default: 251 | panic("unreachable") 252 | } 253 | } 254 | 255 | func (cur *cursor) RowID() (int64, error) { 256 | return cur.rowid, nil 257 | } 258 | 259 | func (cur *cursor) EOF() bool { 260 | if cur.isDesc { 261 | return cur.value < cur.mnValue 262 | } else { 263 | return cur.value > cur.mxValue 264 | } 265 | } 266 | 267 | func (cur *cursor) Close() error { 268 | return nil 269 | } 270 | -------------------------------------------------------------------------------- /ext/refunc/refunc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Package refunc provides an implementation of the [REGEXP operator] 5 | // that uses the Go [regexp] package. 6 | // 7 | // [REGEXP operator]: https://sqlite.org/lang_expr.html#the_like_glob_regexp_match_and_extract_operators 8 | package refunc 9 | 10 | import ( 11 | "fmt" 12 | "regexp" 13 | 14 | "zombiezen.com/go/sqlite" 15 | ) 16 | 17 | // Impl is the implementation of the REGEXP function. 18 | var Impl = &sqlite.FunctionImpl{ 19 | NArgs: 2, 20 | Deterministic: true, 21 | AllowIndirect: true, 22 | Scalar: regexpFunc, 23 | } 24 | 25 | // Register registers the "regexp" function on the given connection. 26 | func Register(c *sqlite.Conn) error { 27 | return c.CreateFunction("regexp", Impl) 28 | } 29 | 30 | func regexpFunc(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { 31 | // First: attempt to retrieve the compiled regexp from a previous call. 32 | re, ok := ctx.AuxData(0).(*regexp.Regexp) 33 | if !ok { 34 | // Auxiliary data not present. Either this is the first call with this 35 | // argument, or SQLite has discarded the auxiliary data. 36 | var err error 37 | re, err = regexp.Compile(args[0].Text()) 38 | if err != nil { 39 | return sqlite.Value{}, fmt.Errorf("regexp: %w", err) 40 | } 41 | // Store the auxiliary data for future calls. 42 | ctx.SetAuxData(0, re) 43 | } 44 | 45 | found := 0 46 | if re.MatchString(args[1].Text()) { 47 | found = 1 48 | } 49 | return sqlite.IntegerValue(int64(found)), nil 50 | } 51 | -------------------------------------------------------------------------------- /ext/refunc/refunc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package refunc 5 | 6 | import ( 7 | "testing" 8 | 9 | "zombiezen.com/go/sqlite" 10 | ) 11 | 12 | func TestImpl(t *testing.T) { 13 | c, err := sqlite.OpenConn("", sqlite.OpenMemory|sqlite.OpenReadWrite) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer func() { 18 | if err := c.Close(); err != nil { 19 | t.Error(err) 20 | } 21 | }() 22 | 23 | if err := Register(c); err != nil { 24 | t.Error("Register:", err) 25 | } 26 | 27 | tests := []struct { 28 | x, y string 29 | want bool 30 | }{ 31 | {"", "foo", false}, 32 | {"foo", "", true}, 33 | {"foo", "^fo*$", true}, 34 | {"bar", "^fo*$", false}, 35 | } 36 | stmt := c.Prep("VALUES (:x REGEXP :y);") 37 | for _, test := range tests { 38 | stmt.SetText(":x", test.x) 39 | stmt.SetText(":y", test.y) 40 | rowReturned, err := stmt.Step() 41 | if err != nil { 42 | t.Errorf("%q REGEXP %q: %v", test.x, test.y, err) 43 | stmt.Reset() 44 | continue 45 | } 46 | if !rowReturned { 47 | t.Errorf("%q REGEXP %q: no row returned", test.x, test.y) 48 | stmt.Reset() 49 | continue 50 | } 51 | if got := stmt.ColumnBool(0); got != test.want { 52 | t.Errorf("%q REGEXP %q = %t; want %t", test.x, test.y, got, test.want) 53 | } 54 | if err := stmt.Reset(); err != nil { 55 | t.Errorf("%q REGEXP %q: %v", test.x, test.y, err) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "id": "flake-utils", 17 | "type": "indirect" 18 | } 19 | }, 20 | "nixpkgs": { 21 | "locked": { 22 | "lastModified": 1746152631, 23 | "narHash": "sha256-zBuvmL6+CUsk2J8GINpyy8Hs1Zp4PP6iBWSmZ4SCQ/s=", 24 | "owner": "NixOS", 25 | "repo": "nixpkgs", 26 | "rev": "032bc6539bd5f14e9d0c51bd79cfe9a055b094c3", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "id": "nixpkgs", 31 | "type": "indirect" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | }, 40 | "systems": { 41 | "locked": { 42 | "lastModified": 1681028828, 43 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 44 | "owner": "nix-systems", 45 | "repo": "default", 46 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nix-systems", 51 | "repo": "default", 52 | "type": "github" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "zombiezen.com/go/sqlite"; 3 | 4 | inputs = { 5 | nixpkgs.url = "nixpkgs"; 6 | flake-utils.url = "flake-utils"; 7 | }; 8 | 9 | outputs = { nixpkgs, flake-utils, ... }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = import nixpkgs { inherit system; }; 13 | in 14 | { 15 | devShells.default = pkgs.mkShell { 16 | packages = [ 17 | pkgs.go-tools # staticcheck 18 | pkgs.go_1_24 19 | pkgs.gotools # godoc, etc. 20 | ]; 21 | 22 | hardeningDisable = [ "fortify" ]; 23 | }; 24 | } 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module zombiezen.com/go/sqlite 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 9 | github.com/chzyer/readline v1.5.0 10 | github.com/google/go-cmp v0.6.0 11 | golang.org/x/text v0.14.0 12 | modernc.org/libc v1.65.7 13 | modernc.org/sqlite v1.37.1 14 | ) 15 | 16 | require ( 17 | github.com/dustin/go-humanize v1.0.1 // indirect 18 | github.com/google/uuid v1.6.0 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | github.com/ncruces/go-strftime v0.1.9 // indirect 21 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 22 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 23 | golang.org/x/sys v0.33.0 // indirect 24 | modernc.org/mathutil v1.7.1 // indirect 25 | modernc.org/memory v1.11.0 // indirect 26 | ) 27 | 28 | retract ( 29 | v0.9.1 // Contains retractions only. 30 | v0.9.0 // Had libc memgrind issues. 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw= 2 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= 3 | github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= 4 | github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= 5 | github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= 6 | github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= 7 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= 8 | github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 10 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 11 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 12 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 18 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 19 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 20 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 21 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 22 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 23 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 24 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 25 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 26 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 27 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 30 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 32 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 33 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 34 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 35 | modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= 36 | modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 37 | modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= 38 | modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= 39 | modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= 40 | modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 41 | modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 42 | modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 43 | modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= 44 | modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= 45 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 46 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 47 | modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= 48 | modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 49 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 50 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 51 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 52 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 53 | modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= 54 | modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= 55 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 56 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 57 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 58 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 59 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.0 2 | 3 | toolchain go1.24.2 4 | 5 | use ( 6 | . 7 | ./cmd/zombiezen-sqlite-migrate 8 | ) 9 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 3 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 4 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 5 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 6 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 9 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 10 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 11 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 12 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 13 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 14 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 15 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 16 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 17 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 18 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 19 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 20 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 21 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 22 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 23 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 24 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 25 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 26 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 27 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 28 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 32 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 33 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 34 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 35 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 36 | golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= 37 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 38 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 39 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 40 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 41 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 42 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 43 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 44 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 45 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 46 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 47 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 48 | golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= 49 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 50 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 51 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 52 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 53 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 54 | modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= 55 | modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= 56 | modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= 57 | modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= 58 | modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= 59 | modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= 60 | modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= 61 | -------------------------------------------------------------------------------- /http_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net/http" 10 | 11 | "zombiezen.com/go/sqlite/sqlitex" 12 | ) 13 | 14 | var dbpool *sqlitex.Pool 15 | 16 | // Using a Pool to execute SQL in a concurrent HTTP handler. 17 | func Example_http() { 18 | var err error 19 | dbpool, err = sqlitex.NewPool("file:memory:?mode=memory", sqlitex.PoolOptions{ 20 | PoolSize: 10, 21 | }) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | http.HandleFunc("/", handle) 26 | log.Fatal(http.ListenAndServe(":8080", nil)) 27 | } 28 | 29 | func handle(w http.ResponseWriter, r *http.Request) { 30 | conn, err := dbpool.Take(r.Context()) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 33 | return 34 | } 35 | defer dbpool.Put(conn) 36 | 37 | stmt := conn.Prep("SELECT foo FROM footable WHERE id = $id;") 38 | stmt.SetText("$id", "_user_id_") 39 | for { 40 | if hasRow, err := stmt.Step(); err != nil { 41 | // ... handle error 42 | } else if !hasRow { 43 | break 44 | } 45 | foo := stmt.GetText("foo") 46 | // ... use foo 47 | fmt.Fprintln(w, foo) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /index_constraint.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "fmt" 8 | "unsafe" 9 | 10 | "modernc.org/libc" 11 | lib "modernc.org/sqlite/lib" 12 | ) 13 | 14 | // IndexConstraint is a constraint term in the WHERE clause 15 | // of a query that uses a virtual table. 16 | type IndexConstraint struct { 17 | // Column is the left-hand operand. 18 | // Column indices start at 0. 19 | // -1 indicates the left-hand operand is the rowid. 20 | // Column should be ignored when Op is [IndexConstraintLimit] or [IndexConstraintOffset]. 21 | Column int 22 | // Op is the constraint's operator. 23 | Op IndexConstraintOp 24 | // Usable indicates whether BestIndex should consider the constraint. 25 | // Usable may false depending on how tables are ordered in a join. 26 | Usable bool 27 | // Collation is the name of the collating sequence 28 | // that should be used when evaluating the constraint. 29 | Collation string 30 | // RValue is the right-hand operand, if known during statement preparation. 31 | // It's only valid until the end of BestIndex. 32 | RValue Value 33 | // RValueKnown indicates whether RValue is set. 34 | RValueKnown bool 35 | } 36 | 37 | func (c *IndexConstraint) copyFromC(tls *libc.TLS, infoPtr uintptr, i int32, ppVal uintptr) { 38 | info := (*lib.Sqlite3_index_info)(unsafe.Pointer(infoPtr)) 39 | src := (*lib.Sqlite3_index_constraint)(unsafe.Pointer(info.FaConstraint + uintptr(i)*unsafe.Sizeof(lib.Sqlite3_index_constraint{}))) 40 | *c = IndexConstraint{ 41 | Column: int(src.FiColumn), 42 | Op: IndexConstraintOp(src.Fop), 43 | Usable: src.Fusable != 0, 44 | } 45 | 46 | const binaryCollation = "BINARY" 47 | cCollation := lib.Xsqlite3_vtab_collation(tls, infoPtr, int32(i)) 48 | if isCStringEqual(cCollation, binaryCollation) { 49 | // BINARY is the most common, so avoid allocations in this case. 50 | c.Collation = binaryCollation 51 | } else { 52 | c.Collation = libc.GoString(cCollation) 53 | } 54 | 55 | if ppVal != 0 { 56 | res := ResultCode(lib.Xsqlite3_vtab_rhs_value(tls, infoPtr, int32(i), ppVal)) 57 | if res == ResultOK { 58 | c.RValue = Value{ 59 | tls: tls, 60 | ptrOrType: *(*uintptr)(unsafe.Pointer(ppVal)), 61 | } 62 | c.RValueKnown = true 63 | } 64 | } 65 | } 66 | 67 | // IndexConstraintOp is an enumeration of virtual table constraint operators 68 | // used in [IndexConstraint]. 69 | type IndexConstraintOp uint8 70 | 71 | const ( 72 | IndexConstraintEq IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_EQ 73 | IndexConstraintGT IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GT 74 | IndexConstraintLE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LE 75 | IndexConstraintLT IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LT 76 | IndexConstraintGE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GE 77 | IndexConstraintMatch IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_MATCH 78 | IndexConstraintLike IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LIKE 79 | IndexConstraintGlob IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GLOB 80 | IndexConstraintRegexp IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_REGEXP 81 | IndexConstraintNE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_NE 82 | IndexConstraintIsNot IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNOT 83 | IndexConstraintIsNotNull IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNOTNULL 84 | IndexConstraintIsNull IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNULL 85 | IndexConstraintIs IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_IS 86 | IndexConstraintLimit IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LIMIT 87 | IndexConstraintOffset IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_OFFSET 88 | ) 89 | 90 | const indexConstraintFunction IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_FUNCTION 91 | 92 | // String returns the operator symbol or keyword. 93 | func (op IndexConstraintOp) String() string { 94 | switch op { 95 | case IndexConstraintEq: 96 | return "=" 97 | case IndexConstraintGT: 98 | return ">" 99 | case IndexConstraintLE: 100 | return "<=" 101 | case IndexConstraintLT: 102 | return "<" 103 | case IndexConstraintGE: 104 | return ">=" 105 | case IndexConstraintMatch: 106 | return "MATCH" 107 | case IndexConstraintLike: 108 | return "LIKE" 109 | case IndexConstraintGlob: 110 | return "GLOB" 111 | case IndexConstraintRegexp: 112 | return "REGEXP" 113 | case IndexConstraintNE: 114 | return "<>" 115 | case IndexConstraintIsNot: 116 | return "IS NOT" 117 | case IndexConstraintIsNotNull: 118 | return "IS NOT NULL" 119 | case IndexConstraintIsNull: 120 | return "IS NULL" 121 | case IndexConstraintIs: 122 | return "IS" 123 | case IndexConstraintLimit: 124 | return "LIMIT" 125 | case IndexConstraintOffset: 126 | return "OFFSET" 127 | default: 128 | if op < indexConstraintFunction { 129 | return fmt.Sprintf("IndexConstraintOp(%d)", uint8(op)) 130 | } 131 | return fmt.Sprintf("", uint8(op)) 132 | } 133 | } 134 | 135 | func isCStringEqual(c uintptr, s string) bool { 136 | if c == 0 { 137 | return s == "" 138 | } 139 | for { 140 | cc := *(*byte)(unsafe.Pointer(c)) 141 | if cc == 0 { 142 | return len(s) == 0 143 | } 144 | if len(s) == 0 || cc != s[0] { 145 | return false 146 | } 147 | c++ 148 | s = s[1:] 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "testing" 8 | "unsafe" 9 | 10 | "modernc.org/libc" 11 | "modernc.org/libc/sys/types" 12 | ) 13 | 14 | func BenchmarkGoStringN(b *testing.B) { 15 | tls := libc.NewTLS() 16 | const want = "Hello, World!\n" 17 | ptr, err := malloc(tls, types.Size_t(len(want)+1)) 18 | if err != nil { 19 | b.Fatal(err) 20 | } 21 | defer libc.Xfree(tls, ptr) 22 | for i := 0; i < len(want); i++ { 23 | *(*byte)(unsafe.Pointer(ptr + uintptr(i))) = want[i] 24 | } 25 | b.ResetTimer() 26 | b.ReportAllocs() 27 | 28 | for i := 0; i < b.N; i++ { 29 | got := goStringN(ptr, len(want)) 30 | if got != want { 31 | b.Errorf("goStringN(%#x, %d) = %q; want %q", ptr, len(want), got, want) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /op_type.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "fmt" 8 | 9 | lib "modernc.org/sqlite/lib" 10 | ) 11 | 12 | // OpType is an enumeration of SQLite statements and authorizable actions. 13 | type OpType int32 14 | 15 | // Operation types 16 | const ( 17 | OpCreateIndex OpType = lib.SQLITE_CREATE_INDEX 18 | OpCreateTable OpType = lib.SQLITE_CREATE_TABLE 19 | OpCreateTempIndex OpType = lib.SQLITE_CREATE_TEMP_INDEX 20 | OpCreateTempTable OpType = lib.SQLITE_CREATE_TEMP_TABLE 21 | OpCreateTempTrigger OpType = lib.SQLITE_CREATE_TEMP_TRIGGER 22 | OpCreateTempView OpType = lib.SQLITE_CREATE_TEMP_VIEW 23 | OpCreateTrigger OpType = lib.SQLITE_CREATE_TRIGGER 24 | OpCreateView OpType = lib.SQLITE_CREATE_VIEW 25 | OpDelete OpType = lib.SQLITE_DELETE 26 | OpDropIndex OpType = lib.SQLITE_DROP_INDEX 27 | OpDropTable OpType = lib.SQLITE_DROP_TABLE 28 | OpDropTempIndex OpType = lib.SQLITE_DROP_TEMP_INDEX 29 | OpDropTempTable OpType = lib.SQLITE_DROP_TEMP_TABLE 30 | OpDropTempTrigger OpType = lib.SQLITE_DROP_TEMP_TRIGGER 31 | OpDropTempView OpType = lib.SQLITE_DROP_TEMP_VIEW 32 | OpDropTrigger OpType = lib.SQLITE_DROP_TRIGGER 33 | OpDropView OpType = lib.SQLITE_DROP_VIEW 34 | OpInsert OpType = lib.SQLITE_INSERT 35 | OpPragma OpType = lib.SQLITE_PRAGMA 36 | OpRead OpType = lib.SQLITE_READ 37 | OpSelect OpType = lib.SQLITE_SELECT 38 | OpTransaction OpType = lib.SQLITE_TRANSACTION 39 | OpUpdate OpType = lib.SQLITE_UPDATE 40 | OpAttach OpType = lib.SQLITE_ATTACH 41 | OpDetach OpType = lib.SQLITE_DETACH 42 | OpAlterTable OpType = lib.SQLITE_ALTER_TABLE 43 | OpReindex OpType = lib.SQLITE_REINDEX 44 | OpAnalyze OpType = lib.SQLITE_ANALYZE 45 | OpCreateVTable OpType = lib.SQLITE_CREATE_VTABLE 46 | OpDropVTable OpType = lib.SQLITE_DROP_VTABLE 47 | OpFunction OpType = lib.SQLITE_FUNCTION 48 | OpSavepoint OpType = lib.SQLITE_SAVEPOINT 49 | OpCopy OpType = lib.SQLITE_COPY 50 | OpRecursive OpType = lib.SQLITE_RECURSIVE 51 | ) 52 | 53 | // String returns the C constant name of the operation type. 54 | func (op OpType) String() string { 55 | switch op { 56 | case OpCreateIndex: 57 | return "SQLITE_CREATE_INDEX" 58 | case OpCreateTable: 59 | return "SQLITE_CREATE_TABLE" 60 | case OpCreateTempIndex: 61 | return "SQLITE_CREATE_TEMP_INDEX" 62 | case OpCreateTempTable: 63 | return "SQLITE_CREATE_TEMP_TABLE" 64 | case OpCreateTempTrigger: 65 | return "SQLITE_CREATE_TEMP_TRIGGER" 66 | case OpCreateTempView: 67 | return "SQLITE_CREATE_TEMP_VIEW" 68 | case OpCreateTrigger: 69 | return "SQLITE_CREATE_TRIGGER" 70 | case OpCreateView: 71 | return "SQLITE_CREATE_VIEW" 72 | case OpDelete: 73 | return "SQLITE_DELETE" 74 | case OpDropIndex: 75 | return "SQLITE_DROP_INDEX" 76 | case OpDropTable: 77 | return "SQLITE_DROP_TABLE" 78 | case OpDropTempIndex: 79 | return "SQLITE_DROP_TEMP_INDEX" 80 | case OpDropTempTable: 81 | return "SQLITE_DROP_TEMP_TABLE" 82 | case OpDropTempTrigger: 83 | return "SQLITE_DROP_TEMP_TRIGGER" 84 | case OpDropTempView: 85 | return "SQLITE_DROP_TEMP_VIEW" 86 | case OpDropTrigger: 87 | return "SQLITE_DROP_TRIGGER" 88 | case OpDropView: 89 | return "SQLITE_DROP_VIEW" 90 | case OpInsert: 91 | return "SQLITE_INSERT" 92 | case OpPragma: 93 | return "SQLITE_PRAGMA" 94 | case OpRead: 95 | return "SQLITE_READ" 96 | case OpSelect: 97 | return "SQLITE_SELECT" 98 | case OpTransaction: 99 | return "SQLITE_TRANSACTION" 100 | case OpUpdate: 101 | return "SQLITE_UPDATE" 102 | case OpAttach: 103 | return "SQLITE_ATTACH" 104 | case OpDetach: 105 | return "SQLITE_DETACH" 106 | case OpAlterTable: 107 | return "SQLITE_ALTER_TABLE" 108 | case OpReindex: 109 | return "SQLITE_REINDEX" 110 | case OpAnalyze: 111 | return "SQLITE_ANALYZE" 112 | case OpCreateVTable: 113 | return "SQLITE_CREATE_VTABLE" 114 | case OpDropVTable: 115 | return "SQLITE_DROP_VTABLE" 116 | case OpFunction: 117 | return "SQLITE_FUNCTION" 118 | case OpSavepoint: 119 | return "SQLITE_SAVEPOINT" 120 | case OpCopy: 121 | return "SQLITE_COPY" 122 | case OpRecursive: 123 | return "SQLITE_RECURSIVE" 124 | default: 125 | return fmt.Sprintf("OpType(%d)", int32(op)) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /openflags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | lib "modernc.org/sqlite/lib" 11 | ) 12 | 13 | // OpenFlags are [flags] used when opening a [Conn] via [OpenConn]. 14 | // Either [OpenReadOnly] or [OpenReadWrite] must always be present. 15 | // 16 | // [flags]: https://www.sqlite.org/c3ref/c_open_autoproxy.html 17 | type OpenFlags uint 18 | 19 | // Required flags, one of which must be passed to [OpenConn]. 20 | const ( 21 | // OpenReadOnly opens the database in read-only mode. 22 | // If the database does not already exist, an error is returned. 23 | OpenReadOnly OpenFlags = lib.SQLITE_OPEN_READONLY 24 | // OpenReadWrite opens the database for reading and writing if possible, 25 | // or reading only if the file is write protected by the operating system. 26 | // If the database does not already exist, 27 | // an error is returned unless OpenCreate is also passed. 28 | OpenReadWrite OpenFlags = lib.SQLITE_OPEN_READWRITE 29 | ) 30 | 31 | // Optional flags to pass to [OpenConn]. 32 | const ( 33 | // OpenCreate will create the file if it does not already exist. 34 | // It is only valid with [OpenReadWrite]. 35 | OpenCreate OpenFlags = lib.SQLITE_OPEN_CREATE 36 | // OpenURI allows the path to be interpreted as a URI. 37 | OpenURI OpenFlags = lib.SQLITE_OPEN_URI 38 | // OpenMemory will be opened as an in-memory database. 39 | // The path is ignored unless [OpenSharedCache] is used. 40 | OpenMemory OpenFlags = lib.SQLITE_OPEN_MEMORY 41 | // OpenSharedCache opens the database with [shared-cache]. 42 | // This is mostly only useful for sharing in-memory databases: 43 | // it's [not recommended] for other purposes. 44 | // 45 | // [shared-cache]: https://www.sqlite.org/sharedcache.html 46 | // [not recommended]: https://www.sqlite.org/sharedcache.html#dontuse 47 | OpenSharedCache OpenFlags = lib.SQLITE_OPEN_SHAREDCACHE 48 | // OpenPrivateCache forces the database to not use shared-cache. 49 | OpenPrivateCache OpenFlags = lib.SQLITE_OPEN_PRIVATECACHE 50 | // OpenWAL enables the [write-ahead log] for the database. 51 | // 52 | // [write-ahead log]: https://www.sqlite.org/wal.html 53 | OpenWAL OpenFlags = lib.SQLITE_OPEN_WAL 54 | 55 | // OpenNoMutex has no effect. 56 | // 57 | // Deprecated: This flag is now implied. 58 | OpenNoMutex OpenFlags = lib.SQLITE_OPEN_NOMUTEX 59 | // OpenFullMutex has no effect. 60 | // 61 | // Deprecated: This flag has no equivalent and is ignored. 62 | OpenFullMutex OpenFlags = lib.SQLITE_OPEN_FULLMUTEX 63 | ) 64 | 65 | // String returns a pipe-separated list of the C constant names set in flags. 66 | func (flags OpenFlags) String() string { 67 | var parts []string 68 | if flags&OpenReadOnly != 0 { 69 | parts = append(parts, "SQLITE_OPEN_READONLY") 70 | flags &^= OpenReadOnly 71 | } 72 | if flags&OpenReadWrite != 0 { 73 | parts = append(parts, "SQLITE_OPEN_READWRITE") 74 | flags &^= OpenReadWrite 75 | } 76 | if flags&OpenCreate != 0 { 77 | parts = append(parts, "SQLITE_OPEN_CREATE") 78 | flags &^= OpenCreate 79 | } 80 | if flags&OpenURI != 0 { 81 | parts = append(parts, "SQLITE_OPEN_URI") 82 | flags &^= OpenURI 83 | } 84 | if flags&OpenMemory != 0 { 85 | parts = append(parts, "SQLITE_OPEN_MEMORY") 86 | flags &^= OpenMemory 87 | } 88 | if flags&OpenNoMutex != 0 { 89 | parts = append(parts, "SQLITE_OPEN_NOMUTEX") 90 | flags &^= OpenNoMutex 91 | } 92 | if flags&OpenFullMutex != 0 { 93 | parts = append(parts, "SQLITE_OPEN_FULLMUTEX") 94 | flags &^= OpenFullMutex 95 | } 96 | if flags&OpenSharedCache != 0 { 97 | parts = append(parts, "SQLITE_OPEN_SHAREDCACHE") 98 | flags &^= OpenSharedCache 99 | } 100 | if flags&OpenPrivateCache != 0 { 101 | parts = append(parts, "SQLITE_OPEN_PRIVATECACHE") 102 | flags &^= OpenPrivateCache 103 | } 104 | if flags&OpenWAL != 0 { 105 | parts = append(parts, "SQLITE_OPEN_WAL") 106 | flags &^= OpenWAL 107 | } 108 | if flags != 0 || len(parts) == 0 { 109 | parts = append(parts, fmt.Sprintf("%#x", uint(flags))) 110 | } 111 | return strings.Join(parts, "|") 112 | } 113 | -------------------------------------------------------------------------------- /result_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite 5 | 6 | import "testing" 7 | 8 | func TestResultCodeMessage(t *testing.T) { 9 | t.Log(ResultOK.Message()) 10 | t.Log(ResultNoMem.Message()) 11 | } 12 | 13 | func TestErrCode(t *testing.T) { 14 | rawErr := ResultInterrupt.ToError() 15 | if got, want := ErrCode(rawErr), ResultInterrupt; got != want { 16 | t.Errorf("got err=%s, want %s", got, want) 17 | } 18 | 19 | wrappedErr := errWithMessage{err: rawErr, msg: "Doing something"} 20 | if got, want := ErrCode(wrappedErr), ResultInterrupt; got != want { 21 | t.Errorf("got err=%s, want %s", got, want) 22 | } 23 | } 24 | 25 | type errWithMessage struct { 26 | err error 27 | msg string 28 | } 29 | 30 | func (e errWithMessage) Unwrap() error { 31 | return e.err 32 | } 33 | 34 | func (e errWithMessage) Error() string { 35 | return e.msg + ": " + e.err.Error() 36 | } 37 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // 3 | // Permission to use, copy, modify, and distribute this software for any 4 | // purpose with or without fee is hereby granted, provided that the above 5 | // copyright notice and this permission notice appear in all copies. 6 | // 7 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | package sqlite_test 16 | 17 | import ( 18 | "bytes" 19 | "reflect" 20 | "testing" 21 | 22 | "zombiezen.com/go/sqlite" 23 | "zombiezen.com/go/sqlite/sqlitex" 24 | ) 25 | 26 | func initT(t *testing.T, conn *sqlite.Conn) { 27 | if _, err := conn.Prep(`INSERT INTO t (c1, c2, c3) VALUES ('1', '2', '3');`).Step(); err != nil { 28 | t.Fatal(err) 29 | } 30 | if _, err := conn.Prep(`INSERT INTO t (c1, c2, c3) VALUES ('4', '5', '6');`).Step(); err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func fillSession(t *testing.T) (*sqlite.Conn, *sqlite.Session) { 36 | conn, err := sqlite.OpenConn(":memory:", 0) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | if _, err := conn.Prep("CREATE TABLE t (c1 PRIMARY KEY, c2, c3);").Step(); err != nil { 42 | t.Fatal(err) 43 | } 44 | initT(t, conn) // two rows that predate the session 45 | 46 | s, err := conn.CreateSession("") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | if err := s.Attach(""); err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | stmts := []string{ 55 | `UPDATE t SET c1='one' WHERE c1='1';`, 56 | `UPDATE t SET c2='two', c3='three' WHERE c1='one';`, 57 | `UPDATE t SET c1='noop' WHERE c2='2';`, 58 | `DELETE FROM t WHERE c1='4';`, 59 | `INSERT INTO t (c1, c2, c3) VALUES ('four', 'five', 'six');`, 60 | } 61 | 62 | for _, stmt := range stmts { 63 | if _, err := conn.Prep(stmt).Step(); err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | if _, err := conn.Prep("BEGIN;").Step(); err != nil { 69 | t.Fatal(err) 70 | } 71 | stmt, err := conn.Prepare("INSERT INTO t (c1, c2, c3) VALUES (?,?,?);") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | for i := int64(2); i < 100; i++ { 76 | stmt.Reset() 77 | stmt.BindInt64(1, i) 78 | stmt.BindText(2, "column2") 79 | stmt.BindText(3, "column3") 80 | if _, err := stmt.Step(); err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | if _, err := conn.Prep("COMMIT;").Step(); err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | return conn, s 89 | } 90 | 91 | func TestFillSession(t *testing.T) { 92 | conn, s := fillSession(t) 93 | s.Delete() 94 | conn.Close() 95 | } 96 | 97 | func TestChangeset(t *testing.T) { 98 | conn, s := fillSession(t) 99 | defer func() { 100 | s.Delete() 101 | if err := conn.Close(); err != nil { 102 | t.Error(err) 103 | } 104 | }() 105 | 106 | buf := new(bytes.Buffer) 107 | if err := s.WriteChangeset(buf); err != nil { 108 | t.Fatal(err) 109 | } 110 | b := buf.Bytes() 111 | if len(b) == 0 { 112 | t.Errorf("changeset has no length") 113 | } 114 | 115 | iter, err := sqlite.NewChangesetIterator(bytes.NewReader(b)) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | numChanges := 0 120 | num3Cols := 0 121 | opTypes := make(map[sqlite.OpType]int) 122 | for { 123 | hasRow, err := iter.Next() 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | if !hasRow { 128 | break 129 | } 130 | op, err := iter.Operation() 131 | if err != nil { 132 | t.Fatalf("numChanges=%d, Op err: %v", numChanges, err) 133 | } 134 | if op.TableName != "t" { 135 | t.Errorf("table=%q, want t", op.TableName) 136 | } 137 | opTypes[op.Type]++ 138 | if op.NumColumns == 3 { 139 | num3Cols++ 140 | } 141 | numChanges++ 142 | } 143 | if numChanges != 102 { 144 | t.Errorf("numChanges=%d, want 102", numChanges) 145 | } 146 | if num3Cols != 102 { 147 | t.Errorf("num3Cols=%d, want 102", num3Cols) 148 | } 149 | if got := opTypes[sqlite.OpInsert]; got != 100 { 150 | t.Errorf("num inserts=%d, want 100", got) 151 | } 152 | if err := iter.Close(); err != nil { 153 | t.Fatal(err) 154 | } 155 | } 156 | 157 | func TestChangesetInvert(t *testing.T) { 158 | conn, s := fillSession(t) 159 | defer func() { 160 | s.Delete() 161 | if err := conn.Close(); err != nil { 162 | t.Error(err) 163 | } 164 | }() 165 | 166 | buf := new(bytes.Buffer) 167 | if err := s.WriteChangeset(buf); err != nil { 168 | t.Fatal(err) 169 | } 170 | b := buf.Bytes() 171 | 172 | buf = new(bytes.Buffer) 173 | if err := sqlite.InvertChangeset(buf, bytes.NewReader(b)); err != nil { 174 | t.Fatal(err) 175 | } 176 | invB := buf.Bytes() 177 | if len(invB) == 0 { 178 | t.Error("no inverted changeset") 179 | } 180 | if bytes.Equal(b, invB) { 181 | t.Error("inverted changeset is unchanged") 182 | } 183 | 184 | buf = new(bytes.Buffer) 185 | if err := sqlite.InvertChangeset(buf, bytes.NewReader(invB)); err != nil { 186 | t.Fatal(err) 187 | } 188 | invinvB := buf.Bytes() 189 | if !bytes.Equal(b, invinvB) { 190 | t.Error("inv(inv(b)) != b") 191 | } 192 | } 193 | 194 | func TestChangesetApply(t *testing.T) { 195 | conn, s := fillSession(t) 196 | defer func() { 197 | s.Delete() 198 | if err := conn.Close(); err != nil { 199 | t.Error(err) 200 | } 201 | }() 202 | 203 | buf := new(bytes.Buffer) 204 | if err := s.WriteChangeset(buf); err != nil { 205 | t.Fatal(err) 206 | } 207 | b := buf.Bytes() 208 | 209 | invBuf := new(bytes.Buffer) 210 | if err := sqlite.InvertChangeset(invBuf, bytes.NewReader(b)); err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | // Undo the entire session. 215 | conflictHandler := sqlite.ConflictHandler(func(typ sqlite.ConflictType, iter *sqlite.ChangesetIterator) sqlite.ConflictAction { 216 | return sqlite.ChangesetOmit 217 | }) 218 | if err := conn.ApplyChangeset(invBuf, nil, conflictHandler); err != nil { 219 | t.Fatal(err) 220 | } 221 | 222 | // Table t should now be equivalent to the first two statements: 223 | // INSERT INTO t (c1, c2, c3) VALUES ("1", "2", "3"); 224 | // INSERT INTO t (c1, c2, c3) VALUES ("4", "5", "6"); 225 | want := []string{"1,2,3", "4,5,6"} 226 | var got []string 227 | fn := func(stmt *sqlite.Stmt) error { 228 | got = append(got, stmt.ColumnText(0)+","+stmt.ColumnText(1)+","+stmt.ColumnText(2)) 229 | return nil 230 | } 231 | if err := sqlitex.Exec(conn, "SELECT c1, c2, c3 FROM t ORDER BY c1;", fn); err != nil { 232 | t.Fatal(err) 233 | } 234 | if !reflect.DeepEqual(got, want) { 235 | t.Errorf("got=%v, want=%v", got, want) 236 | } 237 | } 238 | 239 | func TestPatchsetApply(t *testing.T) { 240 | conn, s := fillSession(t) 241 | defer func() { 242 | if s != nil { 243 | s.Delete() 244 | } 245 | if err := conn.Close(); err != nil { 246 | t.Error(err) 247 | } 248 | }() 249 | 250 | var rowCountBefore int 251 | fn := func(stmt *sqlite.Stmt) error { 252 | rowCountBefore = stmt.ColumnInt(0) 253 | return nil 254 | } 255 | if err := sqlitex.Exec(conn, "SELECT COUNT(*) FROM t;", fn); err != nil { 256 | t.Fatal(err) 257 | } 258 | 259 | buf := new(bytes.Buffer) 260 | if err := s.WritePatchset(buf); err != nil { 261 | t.Fatal(err) 262 | } 263 | b := buf.Bytes() 264 | 265 | s.Delete() 266 | s = nil 267 | 268 | if _, err := conn.Prep("DELETE FROM t;").Step(); err != nil { 269 | t.Fatal(err) 270 | } 271 | initT(t, conn) 272 | 273 | filterFnCalled := false 274 | filterFn := func(tableName string) bool { 275 | if tableName == "t" { 276 | filterFnCalled = true 277 | return true 278 | } else { 279 | t.Errorf("unexpected table in filter fn: %q", tableName) 280 | return false 281 | } 282 | } 283 | conflictFn := func(sqlite.ConflictType, *sqlite.ChangesetIterator) sqlite.ConflictAction { 284 | t.Error("conflict applying patchset") 285 | return sqlite.ChangesetAbort 286 | } 287 | if err := conn.ApplyChangeset(bytes.NewReader(b), filterFn, conflictFn); err != nil { 288 | t.Fatal(err) 289 | } 290 | if !filterFnCalled { 291 | t.Error("filter function not called") 292 | } 293 | 294 | var rowCountAfter int 295 | fn = func(stmt *sqlite.Stmt) error { 296 | rowCountAfter = stmt.ColumnInt(0) 297 | return nil 298 | } 299 | if err := sqlitex.Exec(conn, "SELECT COUNT(*) FROM t;", fn); err != nil { 300 | t.Fatal(err) 301 | } 302 | 303 | if rowCountBefore != rowCountAfter { 304 | t.Errorf("row count is %d, want %d", rowCountAfter, rowCountBefore) 305 | } 306 | 307 | // Second application of patchset should fail. 308 | haveConflict := false 309 | conflictFn = func(ct sqlite.ConflictType, iter *sqlite.ChangesetIterator) sqlite.ConflictAction { 310 | if ct == sqlite.ChangesetConflict { 311 | haveConflict = true 312 | } else { 313 | t.Errorf("unexpected conflict type: %v", ct) 314 | } 315 | op, err := iter.Operation() 316 | if err != nil { 317 | t.Errorf("conflict iter.Op() error: %v", err) 318 | return sqlite.ChangesetAbort 319 | } 320 | if op.Type != sqlite.OpInsert { 321 | t.Errorf("unexpected conflict op type: %v", op.Type) 322 | } 323 | return sqlite.ChangesetAbort 324 | } 325 | err := conn.ApplyChangeset(bytes.NewReader(b), nil, conflictFn) 326 | if code := sqlite.ErrCode(err); code != sqlite.ResultAbort { 327 | t.Errorf("conflicting changeset Apply error is %v, want %v", err, sqlite.ResultAbort) 328 | } 329 | if !haveConflict { 330 | t.Error("no conflict found") 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /shell/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package shell_test 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "zombiezen.com/go/sqlite" 11 | "zombiezen.com/go/sqlite/shell" 12 | ) 13 | 14 | // This is a small program that emulates the behavior of the sqlite3 CLI. 15 | // A path to a database can be passed on the command-line. 16 | func Example() { 17 | dbName := ":memory:" 18 | if len(os.Args) > 1 { 19 | dbName = os.Args[1] 20 | } 21 | conn, err := sqlite.OpenConn(dbName) 22 | if err != nil { 23 | fmt.Fprintln(os.Stderr, err) 24 | os.Exit(1) 25 | } 26 | shell.Run(conn) 27 | conn.Close() 28 | } 29 | -------------------------------------------------------------------------------- /shell/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | // Package shell provides a minimal SQLite REPL, similar to the built-in one. 5 | // This is useful for providing a REPL with custom functions. 6 | package shell 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "os" 12 | "strings" 13 | "unicode" 14 | 15 | "github.com/chzyer/readline" 16 | "modernc.org/libc" 17 | lib "modernc.org/sqlite/lib" 18 | "zombiezen.com/go/sqlite" 19 | "zombiezen.com/go/sqlite/sqlitex" 20 | ) 21 | 22 | const ( 23 | prompt = "sqlite> " 24 | continuationPrompt = " ...> " 25 | ) 26 | 27 | // Run runs an interactive shell on the process's standard I/O. 28 | func Run(conn *sqlite.Conn) { 29 | tls := libc.NewTLS() 30 | defer tls.Close() 31 | 32 | rl, err := readline.NewEx(&readline.Config{ 33 | Prompt: prompt, 34 | }) 35 | if err != nil { 36 | fmt.Fprintln(os.Stderr, err) 37 | return 38 | } 39 | defer rl.Close() 40 | 41 | if readline.DefaultIsTerminal() { 42 | fmt.Printf("SQLite version %s\n", sqlite.Version) 43 | } 44 | var sql string 45 | for { 46 | if len(sql) > 0 { 47 | rl.SetPrompt(continuationPrompt) 48 | } else { 49 | rl.SetPrompt(prompt) 50 | } 51 | line, err := rl.Readline() 52 | if err == io.EOF { 53 | break 54 | } 55 | if err != nil { 56 | fmt.Fprintln(os.Stderr, err) 57 | return 58 | } 59 | if len(sql) == 0 && strings.HasPrefix(line, ".") { 60 | wordEnd := strings.IndexFunc(line, unicode.IsSpace) 61 | if wordEnd == -1 { 62 | wordEnd = len(line) 63 | } 64 | switch word := line[1:wordEnd]; word { 65 | case "schema": 66 | err := sqlitex.ExecuteTransient(conn, `SELECT sql FROM sqlite_master;`, &sqlitex.ExecOptions{ 67 | ResultFunc: func(stmt *sqlite.Stmt) error { 68 | fmt.Println(stmt.ColumnText(0) + ";") 69 | return nil 70 | }, 71 | }) 72 | if err != nil { 73 | fmt.Fprintln(os.Stderr, err) 74 | } 75 | case "quit": 76 | return 77 | default: 78 | fmt.Fprintf(os.Stderr, "unknown command .%s\n", word) 79 | } 80 | continue 81 | } 82 | line = strings.TrimLeftFunc(line, unicode.IsSpace) 83 | if line == "" { 84 | continue 85 | } 86 | sql += line + "\n" 87 | if !strings.Contains(line, ";") { 88 | continue 89 | } 90 | for isCompleteStmt(tls, sql) { 91 | sql = strings.TrimLeftFunc(sql, unicode.IsSpace) 92 | if sql == "" { 93 | break 94 | } 95 | stmt, trailingBytes, err := conn.PrepareTransient(sql) 96 | sql = sql[len(sql)-trailingBytes:] 97 | if err != nil { 98 | fmt.Fprintln(os.Stderr, err) 99 | continue 100 | } 101 | for { 102 | hasData, err := stmt.Step() 103 | if err != nil { 104 | fmt.Fprintln(os.Stderr, err) 105 | break 106 | } 107 | if !hasData { 108 | break 109 | } 110 | row := new(strings.Builder) 111 | for i, n := 0, stmt.ColumnCount(); i < n; i++ { 112 | if i > 0 { 113 | row.WriteString("|") 114 | } 115 | switch stmt.ColumnType(i) { 116 | case sqlite.TypeInteger: 117 | fmt.Fprint(row, stmt.ColumnInt64(i)) 118 | case sqlite.TypeFloat: 119 | fmt.Fprint(row, stmt.ColumnFloat(i)) 120 | case sqlite.TypeBlob: 121 | buf := make([]byte, stmt.ColumnLen(i)) 122 | stmt.ColumnBytes(i, buf) 123 | row.Write(buf) 124 | case sqlite.TypeText: 125 | row.WriteString(stmt.ColumnText(i)) 126 | } 127 | } 128 | fmt.Println(row) 129 | } 130 | stmt.Finalize() 131 | } 132 | } 133 | } 134 | 135 | func isCompleteStmt(tls *libc.TLS, s string) bool { 136 | if s == "" { 137 | return true 138 | } 139 | c, err := libc.CString(s + ";") 140 | if err != nil { 141 | panic(err) 142 | } 143 | defer libc.Xfree(tls, c) 144 | return lib.Xsqlite3_complete(tls, c) != 0 145 | } 146 | -------------------------------------------------------------------------------- /sqlitefile/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitefile 19 | 20 | import ( 21 | "errors" 22 | "io" 23 | 24 | "zombiezen.com/go/sqlite" 25 | ) 26 | 27 | // A Buffer is a variable-sized bytes buffer backed by SQLite blobs. 28 | // 29 | // The bytes are broken into pages, with the first and last pages 30 | // stored in memory, and intermediate pages loaded into blobs. 31 | // Unlike a single SQLite blob, a Buffer can grow beyond its initial size. 32 | // The blobs are allocated in a temporary table. 33 | // 34 | // A Buffer is very similar to a [bytes.Buffer]. 35 | type Buffer struct { 36 | err error 37 | conn *sqlite.Conn 38 | 39 | // cap(rbuf) == cap(wbuf) == blobs[N].Size() 40 | 41 | rbuf []byte // read buffer 42 | roff int // read head position in roff 43 | blobs []tblob // blobs storing data between rbuf and wbuf 44 | wbuf []byte // write buffer 45 | 46 | freelist []tblob 47 | } 48 | 49 | type tblob struct { 50 | blob *sqlite.Blob 51 | rowid int64 52 | } 53 | 54 | // NewBuffer creates a Buffer with 16KB pages. 55 | func NewBuffer(conn *sqlite.Conn) (*Buffer, error) { 56 | return NewBufferSize(conn, 16*1024) 57 | } 58 | 59 | // NewBufferSize creates a Buffer with a specified page size. 60 | func NewBufferSize(conn *sqlite.Conn, pageSize int) (*Buffer, error) { 61 | bb := &Buffer{ 62 | conn: conn, 63 | rbuf: make([]byte, 0, pageSize), 64 | wbuf: make([]byte, 0, pageSize), 65 | } 66 | stmt := conn.Prep("CREATE TEMP TABLE IF NOT EXISTS BlobBuffer (blob BLOB);") 67 | if _, err := stmt.Step(); err != nil { 68 | return nil, err 69 | } 70 | return bb, nil 71 | } 72 | 73 | func (bb *Buffer) alloc() (tblob, error) { 74 | if len(bb.freelist) > 0 { 75 | b := bb.freelist[len(bb.freelist)-1] 76 | bb.freelist = bb.freelist[:len(bb.freelist)-1] 77 | return b, nil 78 | } 79 | 80 | stmt := bb.conn.Prep("INSERT INTO BlobBuffer (blob) VALUES ($blob);") 81 | stmt.SetZeroBlob("$blob", int64(len(bb.rbuf))) 82 | if _, err := stmt.Step(); err != nil { 83 | return tblob{}, err 84 | } 85 | rowid := bb.conn.LastInsertRowID() 86 | blob, err := bb.conn.OpenBlob("temp", "BlobBuffer", "blob", rowid, true) 87 | if err != nil { 88 | return tblob{}, err 89 | } 90 | return tblob{ 91 | blob: blob, 92 | rowid: rowid, 93 | }, nil 94 | } 95 | 96 | func (bb *Buffer) free(b tblob) { 97 | bb.freelist = append(bb.freelist, b) 98 | } 99 | 100 | func (bb *Buffer) wbufEnsureSpace() error { 101 | if len(bb.wbuf) < cap(bb.wbuf) { 102 | return nil 103 | } 104 | 105 | // Flush the write buffer. 106 | if len(bb.blobs) == 0 && bb.roff == len(bb.rbuf) { 107 | // Short cut. The write buffer is full, but 108 | // there are no on-disk blobs and the read 109 | // buffer is empty. So push these bytes 110 | // directly to the front of the Buffer. 111 | bb.rbuf, bb.wbuf = bb.wbuf, bb.rbuf[:0] 112 | bb.roff = 0 113 | } else { 114 | tblob, err := bb.alloc() 115 | if err != nil { 116 | bb.err = err 117 | return err 118 | } 119 | if _, err := tblob.blob.Seek(0, io.SeekStart); err != nil { 120 | bb.err = err 121 | return err 122 | } 123 | if _, err := tblob.blob.Write(bb.wbuf); err != nil { 124 | bb.err = err 125 | return err 126 | } 127 | bb.blobs = append(bb.blobs, tblob) 128 | bb.wbuf = bb.wbuf[:0] 129 | } 130 | 131 | return nil 132 | } 133 | 134 | // WriteByte appends a byte to the buffer, growing it as needed. 135 | func (bb *Buffer) WriteByte(c byte) error { 136 | if bb.err != nil { 137 | return bb.err 138 | } 139 | if err := bb.wbufEnsureSpace(); err != nil { 140 | return err 141 | } 142 | bb.wbuf = append(bb.wbuf, c) 143 | return nil 144 | } 145 | 146 | func (bb *Buffer) UnreadByte() error { 147 | if bb.err != nil { 148 | return bb.err 149 | } 150 | if bb.roff == 0 { 151 | return errors.New("sqlitex.Buffer: UnreadByte: no byte to unread") 152 | } 153 | bb.roff-- 154 | return nil 155 | } 156 | 157 | // Write appends bytes to the buffer, growing it as needed. 158 | func (bb *Buffer) Write(p []byte) (n int, err error) { 159 | if bb.err != nil { 160 | return 0, bb.err 161 | } 162 | 163 | for len(p) > 0 { 164 | if err := bb.wbufEnsureSpace(); err != nil { 165 | return n, err 166 | } 167 | 168 | // TODO: shortcut for writing large p directly into a new blob 169 | 170 | nn := len(p) 171 | if rem := cap(bb.wbuf) - len(bb.wbuf); nn > rem { 172 | nn = rem 173 | } 174 | bb.wbuf = append(bb.wbuf, p[:nn]...) // never grows wbuf 175 | n += nn 176 | p = p[nn:] 177 | } 178 | 179 | return n, nil 180 | } 181 | 182 | // WriteString appends a string to the buffer, growing it as needed. 183 | func (bb *Buffer) WriteString(p string) (n int, err error) { 184 | if bb.err != nil { 185 | return 0, bb.err 186 | } 187 | 188 | for len(p) > 0 { 189 | if err := bb.wbufEnsureSpace(); err != nil { 190 | return n, err 191 | } 192 | 193 | // TODO: shortcut for writing large p directly into a new blob 194 | 195 | nn := len(p) 196 | if rem := cap(bb.wbuf) - len(bb.wbuf); nn > rem { 197 | nn = rem 198 | } 199 | bb.wbuf = append(bb.wbuf, p[:nn]...) // never grows wbuf 200 | n += nn 201 | p = p[nn:] 202 | } 203 | 204 | return n, nil 205 | } 206 | 207 | func (bb *Buffer) rbufFill() error { 208 | if bb.roff < len(bb.rbuf) { 209 | return nil 210 | } 211 | 212 | // Read buffer is empty. Fill it. 213 | if len(bb.blobs) > 0 { 214 | // Read the first blob entirely into the read buffer. 215 | // TODO: shortcut for if len(p) >= blob.Size() 216 | bb.roff = 0 217 | bb.rbuf = bb.rbuf[:cap(bb.rbuf)] 218 | 219 | tblob := bb.blobs[0] 220 | bb.blobs = bb.blobs[1:] 221 | if _, err := tblob.blob.Seek(0, io.SeekStart); err != nil { 222 | bb.err = err 223 | return err 224 | } 225 | if _, err := io.ReadFull(tblob.blob, bb.rbuf); err != nil { 226 | bb.err = err 227 | return err 228 | } 229 | bb.free(tblob) 230 | return nil 231 | } 232 | if len(bb.wbuf) > 0 { 233 | // No blobs. Swap the write buffer bytes here directly. 234 | bb.rbuf, bb.wbuf = bb.wbuf, bb.rbuf[:0] 235 | bb.roff = 0 236 | } 237 | 238 | if bb.roff == len(bb.rbuf) { 239 | return io.EOF 240 | } 241 | return nil 242 | } 243 | 244 | // ReadByte reads a byte from the beginning of the buffer, 245 | // or returns io.EOF if the buffer is empty. 246 | func (bb *Buffer) ReadByte() (byte, error) { 247 | if bb.err != nil { 248 | return 0, bb.err 249 | } 250 | if err := bb.rbufFill(); err != nil { 251 | return 0, err 252 | } 253 | c := bb.rbuf[bb.roff] 254 | bb.roff++ 255 | return c, nil 256 | } 257 | 258 | // Read reads data from the beginning of the buffer. 259 | func (bb *Buffer) Read(p []byte) (n int, err error) { 260 | if bb.err != nil { 261 | return 0, bb.err 262 | } 263 | if err := bb.rbufFill(); err != nil { 264 | return 0, err 265 | } 266 | if bb.roff == len(bb.rbuf) { 267 | return 0, io.EOF 268 | } 269 | 270 | n = copy(p, bb.rbuf[bb.roff:]) 271 | bb.roff += n 272 | return n, nil 273 | } 274 | 275 | // Len returns the number of unread bytes written to the buffer. 276 | func (bb *Buffer) Len() (n int64) { 277 | n = int64(len(bb.rbuf) - bb.roff) 278 | n += int64(cap(bb.rbuf) * len(bb.blobs)) 279 | n += int64(len(bb.wbuf)) 280 | return n 281 | } 282 | 283 | // Cap returns the number of bytes that have been allocated for this buffer, 284 | // both in memory as well as in the database. 285 | func (bb *Buffer) Cap() (n int64) { 286 | pageSize := int64(cap(bb.rbuf)) 287 | return (2 + int64(len(bb.blobs)+len(bb.freelist))) * pageSize 288 | } 289 | 290 | // Reset empties the buffer, 291 | // but retains the blobs written to the database for future writes. 292 | func (bb *Buffer) Reset() { 293 | bb.rbuf = bb.rbuf[:0] 294 | bb.wbuf = bb.wbuf[:0] 295 | bb.roff = 0 296 | bb.freelist = append(bb.freelist, bb.blobs...) 297 | bb.blobs = nil 298 | } 299 | 300 | // Close releases all resources associated with the file, 301 | // removing the storage from the database. 302 | func (bb *Buffer) Close() error { 303 | close := func(tblob tblob) { 304 | err := tblob.blob.Close() 305 | if bb.err == nil { 306 | bb.err = err 307 | } 308 | } 309 | for _, tblob := range bb.blobs { 310 | close(tblob) 311 | } 312 | for _, tblob := range bb.freelist { 313 | close(tblob) 314 | } 315 | 316 | stmt := bb.conn.Prep("DELETE FROM BlobBuffer WHERE rowid = $rowid;") 317 | del := func(tblob tblob) { 318 | stmt.Reset() 319 | stmt.SetInt64("$rowid", tblob.rowid) 320 | if _, err := stmt.Step(); err != nil && bb.err == nil { 321 | bb.err = err 322 | } 323 | } 324 | 325 | for _, tblob := range bb.blobs { 326 | del(tblob) 327 | } 328 | for _, tblob := range bb.freelist { 329 | del(tblob) 330 | } 331 | bb.blobs = nil 332 | bb.freelist = nil 333 | 334 | return bb.err 335 | } 336 | -------------------------------------------------------------------------------- /sqlitefile/buffer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitefile 19 | 20 | import ( 21 | "bytes" 22 | "context" 23 | "io" 24 | "testing" 25 | 26 | "crawshaw.io/iox/ioxtest" 27 | "zombiezen.com/go/sqlite" 28 | "zombiezen.com/go/sqlite/sqlitex" 29 | ) 30 | 31 | var _ interface { 32 | io.Reader 33 | io.Writer 34 | io.StringWriter 35 | io.ByteScanner 36 | io.ByteWriter 37 | io.Closer 38 | } = (*Buffer)(nil) 39 | 40 | func TestBuffer(t *testing.T) { 41 | conn, err := sqlite.OpenConn(":memory:", 0) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | defer conn.Close() 46 | 47 | data := make([]byte, 64*1024+1) 48 | for i := 0; i < 64; i++ { 49 | data[i*1024] = byte(i) 50 | data[i*1024+1023] = byte(i) 51 | } 52 | 53 | buf, err := NewBufferSize(conn, 1024) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | if n, err := buf.Write(data); err != nil { 59 | t.Fatal(err) 60 | } else if n != len(data) { 61 | t.Errorf("buf.Write(data) n=%d, want len(data)=%d", n, len(data)) 62 | } 63 | 64 | if l := int(buf.Len()); l != len(data) { 65 | t.Errorf("buf.Len()=%d, want %d", l, len(data)) 66 | } 67 | 68 | got1 := make([]byte, 1024) 69 | if n, err := buf.Read(got1); err != nil { 70 | t.Fatal(err) 71 | } else if n != len(got1) { 72 | t.Errorf("buf.Read(got1) n=%d, want len(got1)=%d", n, len(got1)) 73 | } 74 | if !bytes.Equal(got1, data[:len(got1)]) { 75 | t.Errorf("got1 does not match, [0]=%d, [1]=%d, [1023]=%d", got1[0], got1[1], got1[1023]) 76 | } 77 | 78 | if l := int(buf.Len()); l != len(data)-len(got1) { 79 | t.Errorf("buf.Len()=%d, want %d", l, len(data)-len(got1)) 80 | } 81 | 82 | b := new(bytes.Buffer) 83 | b.Write(got1) 84 | if _, err := io.Copy(b, buf); err != nil { 85 | t.Errorf("io.Copy err=%v", err) 86 | } 87 | if !bytes.Equal(b.Bytes(), data) { 88 | t.Errorf("b.Bytes and data do not match") 89 | } 90 | 91 | buf.Reset() 92 | if _, err := buf.Write(got1); err != nil { 93 | t.Fatal(err) 94 | } 95 | b.Reset() 96 | if _, err := io.Copy(b, buf); err != nil { 97 | t.Fatal(err) 98 | } 99 | if !bytes.Equal(b.Bytes(), got1) { 100 | t.Errorf("b.Bytes and got1 do not match") 101 | } 102 | 103 | if err := buf.Close(); err != nil { 104 | t.Fatal(err) 105 | } 106 | } 107 | 108 | func TestConcurrentBuffer(t *testing.T) { 109 | // Make sure the shared cache table lock does not 110 | // apply to blob buffers (because we use temp tables). 111 | dbpool, err := sqlitex.NewPool("file::memory:?mode=memory", sqlitex.PoolOptions{ 112 | PoolSize: 2, 113 | }) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | defer dbpool.Close() 118 | conn1, err := dbpool.Take(context.Background()) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | defer dbpool.Put(conn1) 123 | conn2, err := dbpool.Take(context.Background()) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | defer dbpool.Put(conn2) 128 | 129 | b1a, err := NewBuffer(conn1) 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | defer b1a.Close() 134 | 135 | b1b, err := NewBuffer(conn1) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | defer b1b.Close() 140 | 141 | b2, err := NewBuffer(conn2) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | defer b2.Close() 146 | } 147 | 148 | func TestBufferRand(t *testing.T) { 149 | conn, err := sqlite.OpenConn(":memory:", 0) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | defer conn.Close() 154 | 155 | buf, err := NewBufferSize(conn, 1<<18) 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | ft := ioxtest.Tester{ 161 | F1: buf, 162 | F2: new(bytes.Buffer), 163 | T: t, 164 | } 165 | ft.Run() 166 | if err := buf.Close(); err != nil { 167 | t.Fatal(err) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /sqlitefile/file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | // Package sqlitefile provides bytes buffers backed by a temporary SQLite table. 19 | package sqlitefile 20 | 21 | import ( 22 | "fmt" 23 | "io" 24 | 25 | "zombiezen.com/go/sqlite" 26 | ) 27 | 28 | // File is a readable, writable, and seekable series of temporary SQLite blobs. 29 | type File struct { 30 | err error 31 | conn *sqlite.Conn 32 | blobs []*sqlite.Blob 33 | rowids []int64 34 | off bbpos 35 | len bbpos 36 | } 37 | 38 | // NewFile creates a new [File] with a reasonable initial capacity. 39 | func NewFile(conn *sqlite.Conn) (*File, error) { 40 | return NewFileSize(conn, 16*1024) 41 | } 42 | 43 | // NewFileSize creates a new [File] with the given number of bytes of capacity. 44 | func NewFileSize(conn *sqlite.Conn, initSize int) (*File, error) { 45 | bb := &File{conn: conn} 46 | stmt := conn.Prep("CREATE TEMP TABLE IF NOT EXISTS BlobBuffer (blob BLOB);") 47 | if _, err := stmt.Step(); err != nil { 48 | return nil, err 49 | } 50 | if err := bb.addblob(int64(initSize)); err != nil { 51 | return nil, err 52 | } 53 | return bb, nil 54 | } 55 | 56 | func (bb *File) addblob(size int64) error { 57 | stmt := bb.conn.Prep("INSERT INTO BlobBuffer (blob) VALUES ($blob);") 58 | stmt.SetZeroBlob("$blob", size) 59 | if _, err := stmt.Step(); err != nil { 60 | return err 61 | } 62 | rowid := bb.conn.LastInsertRowID() 63 | blob, err := bb.conn.OpenBlob("temp", "BlobBuffer", "blob", rowid, true) 64 | if err != nil { 65 | return err 66 | } 67 | bb.blobs = append(bb.blobs, blob) 68 | bb.rowids = append(bb.rowids, rowid) 69 | return nil 70 | } 71 | 72 | // grow adds an sqlite blob if the buffer is out of space. 73 | func (bb *File) grow() error { 74 | lastSize := bb.blobs[len(bb.blobs)-1].Size() 75 | size := lastSize * 2 76 | if err := bb.addblob(size); err != nil { 77 | return err 78 | } 79 | return nil 80 | } 81 | 82 | // rem reports the remaining available bytes in the pointed-to blob 83 | func (bb *File) rem(pos bbpos) int64 { 84 | return bb.blobs[pos.index].Size() - pos.pos 85 | } 86 | 87 | func (bb *File) eq(p1, p2 bbpos) bool { 88 | if p1 == p2 { 89 | return true 90 | } 91 | if p1.index == p2.index+1 && bb.rem(p1) == 0 && p2.pos == 0 { 92 | return true 93 | } 94 | if p2.index == p1.index+1 && bb.rem(p2) == 0 && p1.pos == 0 { 95 | return true 96 | } 97 | return false 98 | } 99 | 100 | func (bb *File) gt(p1, p2 bbpos) bool { 101 | if bb.eq(p1, p2) { 102 | return false 103 | } 104 | if p1.index != p2.index { 105 | return p1.index > p2.index 106 | } 107 | return p1.pos > p2.pos 108 | } 109 | 110 | func (bb *File) zero(p1, p2 bbpos) error { 111 | var zeros [4096]byte 112 | for bb.gt(p2, p1) { 113 | w := bb.rem(p1) 114 | if w == 0 { 115 | p1.index++ 116 | p1.pos = 0 117 | w = bb.rem(p1) 118 | } 119 | if p1.index == p2.index { 120 | w = p2.pos 121 | } 122 | if w > int64(len(zeros)) { 123 | w = int64(len(zeros)) 124 | } 125 | if _, err := bb.blobs[p1.index].Seek(p1.pos, io.SeekStart); err != nil { 126 | return err 127 | } 128 | nn, err := bb.blobs[p1.index].Write(zeros[:w]) 129 | if err != nil { 130 | return err 131 | } 132 | p1.pos += int64(nn) 133 | } 134 | return nil 135 | } 136 | 137 | // Write writes p to the file. 138 | // See [io.Writer] for details. 139 | func (bb *File) Write(p []byte) (n int, err error) { 140 | if bb.err != nil { 141 | return 0, bb.err 142 | } 143 | 144 | if bb.gt(bb.off, bb.len) { 145 | if err := bb.zero(bb.len, bb.off); err != nil { 146 | bb.err = err 147 | return 0, err 148 | } 149 | } 150 | 151 | for len(p) > 0 { 152 | w := bb.rem(bb.off) 153 | if w == 0 { 154 | if bb.off.index == len(bb.blobs)-1 { 155 | if bb.err = bb.grow(); bb.err != nil { 156 | return n, bb.err 157 | } 158 | } 159 | bb.off.index++ 160 | bb.off.pos = 0 161 | w = bb.rem(bb.off) 162 | } 163 | if int64(len(p)) < w { 164 | w = int64(len(p)) 165 | } 166 | if _, err := bb.blobs[bb.off.index].Seek(bb.off.pos, io.SeekStart); err != nil { 167 | bb.err = err 168 | break 169 | } 170 | nn, err := bb.blobs[bb.off.index].Write(p[:w]) 171 | n += nn 172 | p = p[nn:] 173 | bb.off.pos += int64(nn) 174 | if bb.gt(bb.off, bb.len) { 175 | bb.len = bb.off 176 | } 177 | if err != nil { 178 | bb.err = err 179 | break 180 | } 181 | } 182 | 183 | return n, bb.err 184 | } 185 | 186 | // Read reads data into p. 187 | // See [io.Reader] for details. 188 | func (bb *File) Read(p []byte) (n int, err error) { 189 | if bb.err != nil { 190 | return 0, bb.err 191 | } 192 | 193 | for len(p) > 0 && bb.gt(bb.len, bb.off) { 194 | if bb.rem(bb.off) == 0 { 195 | bb.off.index++ 196 | bb.off.pos = 0 197 | } 198 | 199 | var bsize int64 200 | if bb.len.index == bb.off.index { 201 | bsize = bb.len.pos 202 | } else { 203 | bsize = bb.blobs[bb.off.index].Size() 204 | } 205 | w := bsize - bb.off.pos 206 | if int64(len(p)) < w { 207 | w = int64(len(p)) 208 | } 209 | if _, err := bb.blobs[bb.off.index].Seek(bb.off.pos, io.SeekStart); err != nil { 210 | bb.err = err 211 | return n, err 212 | } 213 | nn, err := io.ReadFull(bb.blobs[bb.off.index], p[:w]) 214 | n += nn 215 | p = p[nn:] 216 | bb.off.pos += int64(nn) 217 | if err != nil { 218 | bb.err = err 219 | return n, err 220 | } 221 | } 222 | 223 | if n == 0 && (bb.eq(bb.off, bb.len) || bb.gt(bb.off, bb.len)) { 224 | return 0, io.EOF 225 | } 226 | 227 | return n, nil 228 | } 229 | 230 | // Seek changes the read/write position in the file. 231 | // See [io.Seeker] for details. 232 | func (bb *File) Seek(offset int64, whence int) (int64, error) { 233 | if bb.err != nil { 234 | return 0, bb.err 235 | } 236 | 237 | const ( 238 | SeekStart = 0 239 | SeekCurrent = 1 240 | SeekEnd = 2 241 | ) 242 | switch whence { 243 | case SeekStart: 244 | // use offset directly 245 | case SeekCurrent: 246 | for i := 0; i < bb.off.index; i++ { 247 | offset += bb.blobs[i].Size() 248 | } 249 | offset += bb.off.pos 250 | case SeekEnd: 251 | offset += bb.Len() 252 | } 253 | if offset < 0 { 254 | return -1, fmt.Errorf("sqlitex.File: attempting to seek before beginning of blob (%d)", offset) 255 | } 256 | 257 | rem := offset 258 | bb.off.index = 0 259 | for i := 0; rem > bb.blobs[i].Size(); i++ { 260 | bb.off.index = i + 1 261 | rem -= bb.blobs[i].Size() 262 | 263 | if i == len(bb.blobs)-1 { 264 | if err := bb.grow(); err != nil { 265 | return offset - rem, err 266 | } 267 | } 268 | } 269 | bb.off.pos = rem 270 | 271 | return offset, nil 272 | } 273 | 274 | // Truncate changes the size of the file. 275 | func (bb *File) Truncate(size int64) error { 276 | for { 277 | for i := 0; i < len(bb.blobs); i++ { 278 | bsize := bb.blobs[i].Size() 279 | if bsize > size { 280 | newlen := bbpos{index: i, pos: size} 281 | if err := bb.zero(bb.len, newlen); err != nil { 282 | return err 283 | } 284 | bb.len = newlen 285 | return nil 286 | } 287 | size -= bsize 288 | } 289 | if err := bb.grow(); err != nil { 290 | return err 291 | } 292 | } 293 | } 294 | 295 | // Len returns the size of the file in bytes. 296 | func (bb *File) Len() (n int64) { 297 | for i := 0; i < bb.len.index; i++ { 298 | n += bb.blobs[i].Size() 299 | } 300 | n += bb.len.pos 301 | return n 302 | } 303 | 304 | // Cap returns the allocated capacity of the file in bytes. 305 | func (bb *File) Cap() (n int64) { 306 | for i := 0; i < len(bb.blobs); i++ { 307 | n += bb.blobs[i].Size() 308 | } 309 | return n 310 | } 311 | 312 | // Close releases all resources associated with the file, 313 | // removing the storage from the database. 314 | func (bb *File) Close() error { 315 | if bb.err != nil { 316 | return bb.err 317 | } 318 | for _, blob := range bb.blobs { 319 | err := blob.Close() 320 | if bb.err == nil { 321 | bb.err = err 322 | } 323 | } 324 | stmt := bb.conn.Prep("DELETE FROM BlobBuffer WHERE rowid = $rowid;") 325 | for _, rowid := range bb.rowids { 326 | stmt.Reset() 327 | stmt.SetInt64("$rowid", rowid) 328 | if _, err := stmt.Step(); err != nil && bb.err == nil { 329 | bb.err = err 330 | } 331 | } 332 | bb.blobs = nil 333 | bb.rowids = nil 334 | return bb.err 335 | } 336 | 337 | type bbpos struct { 338 | index int // bb.blobs[index] 339 | pos int64 // point inside that blob 340 | } 341 | -------------------------------------------------------------------------------- /sqlitefile/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitefile 19 | 20 | import ( 21 | "io" 22 | "os" 23 | "testing" 24 | 25 | "crawshaw.io/iox/ioxtest" 26 | "zombiezen.com/go/sqlite" 27 | ) 28 | 29 | var _ interface { 30 | io.Reader 31 | io.Writer 32 | io.Seeker 33 | io.Closer 34 | } = (*File)(nil) 35 | 36 | func TestFileRand(t *testing.T) { 37 | conn, err := sqlite.OpenConn(":memory:", 0) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | defer conn.Close() 42 | 43 | f1, err := NewFile(conn) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | f2, err := os.CreateTemp("", "sqlitex") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | ft := &ioxtest.Tester{F1: f1, F2: f2, T: t} 52 | ft.Run() 53 | } 54 | -------------------------------------------------------------------------------- /sqlitemigration/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlitemigration_test 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | 14 | "zombiezen.com/go/sqlite" 15 | "zombiezen.com/go/sqlite/sqlitemigration" 16 | "zombiezen.com/go/sqlite/sqlitex" 17 | ) 18 | 19 | func Example() { 20 | schema := sqlitemigration.Schema{ 21 | // Each element of the Migrations slice is applied in sequence. When you 22 | // want to change the schema, add a new SQL script to this list. 23 | // 24 | // Existing databases will pick up at the same position in the Migrations 25 | // slice as they last left off. 26 | Migrations: []string{ 27 | "CREATE TABLE foo ( id INTEGER NOT NULL PRIMARY KEY );", 28 | 29 | "ALTER TABLE foo ADD COLUMN name TEXT;", 30 | }, 31 | 32 | // The RepeatableMigration is run after all other Migrations if any 33 | // migration was run. It is useful for creating triggers and views. 34 | RepeatableMigration: "DROP VIEW IF EXISTS bar;\n" + 35 | "CREATE VIEW bar ( id, name ) AS SELECT id, name FROM foo;\n", 36 | } 37 | 38 | // Set up a temporary directory to store the database. 39 | dir, err := os.MkdirTemp("", "sqlitemigration") 40 | if err != nil { 41 | // handle error 42 | log.Fatal(err) 43 | } 44 | defer os.RemoveAll(dir) 45 | 46 | // Open a pool. This does not block, and will start running any migrations 47 | // asynchronously. 48 | pool := sqlitemigration.NewPool(filepath.Join(dir, "foo.db"), schema, sqlitemigration.Options{ 49 | Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, 50 | PrepareConn: func(conn *sqlite.Conn) error { 51 | // Enable foreign keys. See https://sqlite.org/foreignkeys.html 52 | return sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = ON;", nil) 53 | }, 54 | OnError: func(e error) { 55 | log.Println(e) 56 | }, 57 | }) 58 | defer pool.Close() 59 | 60 | // Get a connection. This blocks until the migration completes. 61 | conn, err := pool.Get(context.TODO()) 62 | if err != nil { 63 | // handle error 64 | } 65 | defer pool.Put(conn) 66 | 67 | // Print the list of schema objects created. 68 | const listSchemaQuery = `SELECT "type", "name" FROM sqlite_master ORDER BY 1, 2;` 69 | err = sqlitex.ExecuteTransient(conn, listSchemaQuery, &sqlitex.ExecOptions{ 70 | ResultFunc: func(stmt *sqlite.Stmt) error { 71 | fmt.Printf("%-5s %s\n", stmt.ColumnText(0), stmt.ColumnText(1)) 72 | return nil 73 | }, 74 | }) 75 | if err != nil { 76 | // handle error 77 | } 78 | 79 | // Output: 80 | // table foo 81 | // view bar 82 | } 83 | 84 | // This example constructs a schema from a set of SQL files in a directory named 85 | // schema01.sql, schema02.sql, etc. 86 | func ExampleSchema() { 87 | var schema sqlitemigration.Schema 88 | for i := 1; ; i++ { 89 | migration, err := os.ReadFile(fmt.Sprintf("schema%02d.sql", i)) 90 | if errors.Is(err, os.ErrNotExist) { 91 | break 92 | } 93 | if err != nil { 94 | // handle error... 95 | } 96 | schema.Migrations = append(schema.Migrations, string(migration)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sqlitex/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | /* 5 | Package sqlitex provides utilities for working with SQLite. 6 | 7 | # Statements 8 | 9 | To execute a statement from a string, 10 | pass an [ExecOptions] struct to one of the following functions: 11 | 12 | - [Execute] 13 | - [ExecuteScript] 14 | - [ExecuteTransient] 15 | 16 | To execute a statement from a file (typically using [embed]), 17 | pass an [ExecOptions] struct to one of the following functions: 18 | 19 | - [ExecuteFS] 20 | - [ExecuteScriptFS] 21 | - [ExecuteTransientFS] 22 | - [PrepareTransientFS] 23 | 24 | # Transactions and Savepoints 25 | 26 | - [Save] 27 | - [Transaction] 28 | - [ExclusiveTransaction] 29 | - [ImmediateTransaction] 30 | */ 31 | package sqlitex 32 | -------------------------------------------------------------------------------- /sqlitex/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlitex_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | 10 | "zombiezen.com/go/sqlite" 11 | "zombiezen.com/go/sqlite/sqlitex" 12 | ) 13 | 14 | func ExampleExecute() { 15 | conn, err := sqlite.OpenConn(":memory:") 16 | if err != nil { 17 | // handle err 18 | } 19 | 20 | if err := sqlitex.Execute(conn, "CREATE TABLE t (a, b, c, d);", nil); err != nil { 21 | // handle err 22 | } 23 | 24 | err = sqlitex.Execute(conn, "INSERT INTO t (a, b, c, d) VALUES (?, ?, ?, ?);", &sqlitex.ExecOptions{ 25 | Args: []any{"a1", 1, 42, 1}, 26 | }) 27 | if err != nil { 28 | // handle err 29 | } 30 | 31 | var a []string 32 | var b []int64 33 | err = sqlitex.Execute(conn, "SELECT a, b FROM t WHERE c = ? AND d = ?;", &sqlitex.ExecOptions{ 34 | ResultFunc: func(stmt *sqlite.Stmt) error { 35 | a = append(a, stmt.ColumnText(0)) 36 | b = append(b, stmt.ColumnInt64(1)) 37 | return nil 38 | }, 39 | Args: []any{42, 1}, 40 | }) 41 | if err != nil { 42 | // handle err 43 | } 44 | 45 | fmt.Println(a, b) 46 | // Output: 47 | // [a1] [1] 48 | } 49 | 50 | func ExampleSave() { 51 | doWork := func(conn *sqlite.Conn) (err error) { 52 | defer sqlitex.Save(conn)(&err) 53 | 54 | // ... do work in the transaction 55 | return nil 56 | } 57 | _ = doWork 58 | } 59 | 60 | func ExamplePool() { 61 | // Open a pool. 62 | dbpool, err := sqlitex.NewPool("foo.db", sqlitex.PoolOptions{}) 63 | if err != nil { 64 | // handle err 65 | } 66 | defer func() { 67 | if err := dbpool.Close(); err != nil { 68 | // handle err 69 | } 70 | }() 71 | 72 | // While handling a request: 73 | ctx := context.TODO() 74 | conn, err := dbpool.Take(ctx) 75 | if err != nil { 76 | // handle err 77 | } 78 | defer dbpool.Put(conn) 79 | } 80 | -------------------------------------------------------------------------------- /sqlitex/exec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex 19 | 20 | import ( 21 | "fmt" 22 | "reflect" 23 | "testing" 24 | 25 | "zombiezen.com/go/sqlite" 26 | ) 27 | 28 | func TestExec(t *testing.T) { 29 | conn, err := sqlite.OpenConn(":memory:", 0) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer conn.Close() 34 | 35 | if err := ExecTransient(conn, "CREATE TABLE t (a TEXT, b INTEGER);", nil); err != nil { 36 | t.Fatal(err) 37 | } 38 | if err := Exec(conn, "INSERT INTO t (a, b) VALUES (?, ?);", nil, "a1", 1); err != nil { 39 | t.Error(err) 40 | } 41 | if err := Exec(conn, "INSERT INTO t (a, b) VALUES (?, ?);", nil, "a2", 2); err != nil { 42 | t.Error(err) 43 | } 44 | 45 | var a []string 46 | var b []int64 47 | fn := func(stmt *sqlite.Stmt) error { 48 | a = append(a, stmt.ColumnText(0)) 49 | b = append(b, stmt.ColumnInt64(1)) 50 | return nil 51 | } 52 | if err := ExecTransient(conn, "SELECT a, b FROM t;", fn); err != nil { 53 | t.Fatal(err) 54 | } 55 | if want := []string{"a1", "a2"}; !reflect.DeepEqual(a, want) { 56 | t.Errorf("a=%v, want %v", a, want) 57 | } 58 | if want := []int64{1, 2}; !reflect.DeepEqual(b, want) { 59 | t.Errorf("b=%v, want %v", b, want) 60 | } 61 | } 62 | 63 | func TestExecNil(t *testing.T) { 64 | err := Execute(nil, `SELECT 1;`, &ExecOptions{ 65 | ResultFunc: func(stmt *sqlite.Stmt) error { 66 | t.Error("ResultFunc called") 67 | return nil 68 | }, 69 | }) 70 | if err == nil { 71 | t.Error("No error returned") 72 | } else { 73 | t.Log("Execute message:", err) 74 | } 75 | 76 | err = ExecuteTransient(nil, `SELECT 1;`, &ExecOptions{ 77 | ResultFunc: func(stmt *sqlite.Stmt) error { 78 | t.Error("ResultFunc called") 79 | return nil 80 | }, 81 | }) 82 | if err == nil { 83 | t.Error("No error returned") 84 | } else { 85 | t.Log("ExecuteTransient message:", err) 86 | } 87 | } 88 | 89 | func TestExecBadSyntax(t *testing.T) { 90 | conn, err := sqlite.OpenConn(":memory:", 0) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | defer conn.Close() 95 | 96 | err = ExecuteTransient(conn, " \nSELECT );", &ExecOptions{ 97 | ResultFunc: func(stmt *sqlite.Stmt) error { 98 | t.Error("ResultFunc called") 99 | return nil 100 | }, 101 | }) 102 | if err == nil { 103 | t.Fatal("No error returned") 104 | } 105 | t.Log("Message:", err) 106 | got, ok := sqlite.ErrorOffset(err) 107 | if want := 9; got != want || ok == false { 108 | t.Errorf("sqlite.ErrorOffset(err) = %d, %t; want %d, true", got, ok, want) 109 | } 110 | } 111 | 112 | func TestExecErr(t *testing.T) { 113 | conn, err := sqlite.OpenConn(":memory:", 0) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | defer conn.Close() 118 | 119 | err = Exec(conn, "INVALID SQL STMT", nil) 120 | if err == nil { 121 | t.Error("invalid SQL did not return an error code") 122 | } 123 | if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { 124 | t.Errorf("INVALID err code=%s, want %s", got, want) 125 | } 126 | 127 | if err := Exec(conn, "CREATE TABLE t (c1, c2);", nil); err != nil { 128 | t.Error(err) 129 | } 130 | if err := Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 1, 1); err != nil { 131 | t.Error(err) 132 | } 133 | if err := Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 2, 2); err != nil { 134 | t.Error(err) 135 | } 136 | err = Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 1, 1, 1) 137 | if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { 138 | t.Errorf("INSERT err code=%s, want %s", got, want) 139 | } 140 | 141 | calls := 0 142 | customErr := fmt.Errorf("custom err") 143 | fn := func(stmt *sqlite.Stmt) error { 144 | calls++ 145 | return customErr 146 | } 147 | err = Exec(conn, "SELECT c1 FROM t;", fn) 148 | if err != customErr { 149 | t.Errorf("SELECT want err=customErr, got: %v", err) 150 | } 151 | if calls != 1 { 152 | t.Errorf("SELECT want truncated callback calls, got calls=%d", calls) 153 | } 154 | } 155 | 156 | func TestExecArgsErrors(t *testing.T) { 157 | conn, err := sqlite.OpenConn(":memory:", 0) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | defer conn.Close() 162 | 163 | t.Run("TooManyPositional", func(t *testing.T) { 164 | err := Exec(conn, `SELECT ?;`, nil, 1, 2) 165 | t.Log(err) 166 | if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { 167 | t.Errorf("code = %v; want %v", got, want) 168 | } 169 | }) 170 | 171 | t.Run("Missing", func(t *testing.T) { 172 | // Compatibility: crawshaw.io/sqlite does not check for this condition. 173 | err := Exec(conn, `SELECT ?;`, nil) 174 | t.Log(err) 175 | if got, want := sqlite.ErrCode(err), sqlite.ResultOK; got != want { 176 | t.Errorf("code = %v; want %v", got, want) 177 | } 178 | }) 179 | } 180 | 181 | func TestExecuteArgsErrors(t *testing.T) { 182 | conn, err := sqlite.OpenConn(":memory:", 0) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | defer conn.Close() 187 | 188 | t.Run("TooManyPositional", func(t *testing.T) { 189 | err := Execute(conn, `SELECT ?;`, &ExecOptions{ 190 | Args: []any{1, 2}, 191 | }) 192 | t.Log(err) 193 | if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { 194 | t.Errorf("code = %v; want %v", got, want) 195 | } 196 | }) 197 | 198 | t.Run("ExtraNamed", func(t *testing.T) { 199 | err := Execute(conn, `SELECT :foo;`, &ExecOptions{ 200 | Named: map[string]any{ 201 | ":foo": 42, 202 | ":bar": "hi", 203 | }, 204 | }) 205 | t.Log(err) 206 | if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { 207 | t.Errorf("code = %v; want %v", got, want) 208 | } 209 | }) 210 | 211 | t.Run("Missing", func(t *testing.T) { 212 | err := Execute(conn, `SELECT ?;`, &ExecOptions{ 213 | Args: []any{}, 214 | }) 215 | t.Log(err) 216 | if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { 217 | t.Errorf("code = %v; want %v", got, want) 218 | } 219 | }) 220 | 221 | t.Run("MissingNamed", func(t *testing.T) { 222 | err := Execute(conn, `SELECT :foo;`, nil) 223 | t.Log(err) 224 | if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { 225 | t.Errorf("code = %v; want %v", got, want) 226 | } 227 | }) 228 | } 229 | 230 | func TestExecScript(t *testing.T) { 231 | conn, err := sqlite.OpenConn(":memory:", 0) 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | defer conn.Close() 236 | 237 | script := ` 238 | CREATE TABLE t (a TEXT, b INTEGER); 239 | INSERT INTO t (a, b) VALUES ('a1', 1); 240 | INSERT INTO t (a, b) VALUES ('a2', 2); 241 | ` 242 | 243 | if err := ExecScript(conn, script); err != nil { 244 | t.Error(err) 245 | } 246 | 247 | sum := 0 248 | fn := func(stmt *sqlite.Stmt) error { 249 | sum = stmt.ColumnInt(0) 250 | return nil 251 | } 252 | if err := Exec(conn, "SELECT sum(b) FROM t;", fn); err != nil { 253 | t.Fatal(err) 254 | } 255 | 256 | if sum != 3 { 257 | t.Errorf("sum=%d, want 3", sum) 258 | } 259 | } 260 | 261 | func TestExecuteScript(t *testing.T) { 262 | conn, err := sqlite.OpenConn(":memory:", 0) 263 | if err != nil { 264 | t.Fatal(err) 265 | } 266 | defer conn.Close() 267 | 268 | script := ` 269 | CREATE TABLE t (a TEXT, b INTEGER); 270 | INSERT INTO t (a, b) VALUES ('a1', :a1); 271 | INSERT INTO t (a, b) VALUES ('a2', :a2); 272 | ` 273 | 274 | err = ExecuteScript(conn, script, &ExecOptions{ 275 | Named: map[string]any{ 276 | ":a1": 1, 277 | ":a2": 2, 278 | }, 279 | }) 280 | if err != nil { 281 | t.Error(err) 282 | } 283 | 284 | sum := 0 285 | fn := func(stmt *sqlite.Stmt) error { 286 | sum = stmt.ColumnInt(0) 287 | return nil 288 | } 289 | if err := Exec(conn, "SELECT sum(b) FROM t;", fn); err != nil { 290 | t.Fatal(err) 291 | } 292 | 293 | if sum != 3 { 294 | t.Errorf("sum=%d, want 3", sum) 295 | } 296 | 297 | t.Run("ExtraNamed", func(t *testing.T) { 298 | conn, err := sqlite.OpenConn(":memory:", 0) 299 | if err != nil { 300 | t.Fatal(err) 301 | } 302 | defer conn.Close() 303 | 304 | script := ` 305 | CREATE TABLE t (a TEXT, b INTEGER); 306 | INSERT INTO t (a, b) VALUES ('a1', :a1); 307 | INSERT INTO t (a, b) VALUES ('a2', :a2); 308 | ` 309 | 310 | err = ExecuteScript(conn, script, &ExecOptions{ 311 | Named: map[string]any{ 312 | ":a1": 1, 313 | ":a2": 2, 314 | ":a3": 3, 315 | }, 316 | }) 317 | t.Log(err) 318 | if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { 319 | t.Errorf("code = %v; want %v", got, want) 320 | } 321 | }) 322 | 323 | t.Run("MissingNamed", func(t *testing.T) { 324 | conn, err := sqlite.OpenConn(":memory:", 0) 325 | if err != nil { 326 | t.Fatal(err) 327 | } 328 | defer conn.Close() 329 | 330 | script := ` 331 | CREATE TABLE t (a TEXT, b INTEGER); 332 | INSERT INTO t (a, b) VALUES ('a1', :a1); 333 | INSERT INTO t (a, b) VALUES ('a2', :a2); 334 | ` 335 | 336 | err = ExecuteScript(conn, script, &ExecOptions{ 337 | Named: map[string]any{ 338 | ":a1": 1, 339 | }, 340 | }) 341 | t.Log(err) 342 | if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { 343 | t.Errorf("code = %v; want %v", got, want) 344 | } 345 | }) 346 | } 347 | 348 | func TestBitsetHasAll(t *testing.T) { 349 | tests := []struct { 350 | bs bitset 351 | n int 352 | want bool 353 | }{ 354 | { 355 | bs: bitset{}, 356 | n: 0, 357 | want: true, 358 | }, 359 | { 360 | bs: bitset{0}, 361 | n: 1, 362 | want: false, 363 | }, 364 | { 365 | bs: bitset{0x0000000000000001}, 366 | n: 1, 367 | want: true, 368 | }, 369 | { 370 | bs: bitset{0x8000000000000001}, 371 | n: 1, 372 | want: true, 373 | }, 374 | { 375 | bs: bitset{0x0000000000000001}, 376 | n: 2, 377 | want: false, 378 | }, 379 | { 380 | bs: bitset{0xffffffffffffffff}, 381 | n: 64, 382 | want: true, 383 | }, 384 | { 385 | bs: bitset{0xffffffffffffffff}, 386 | n: 65, 387 | want: false, 388 | }, 389 | { 390 | bs: bitset{0xffffffffffffffff, 0x0000000000000000}, 391 | n: 65, 392 | want: false, 393 | }, 394 | { 395 | bs: bitset{0xffffffffffffffff, 0x0000000000000001}, 396 | n: 65, 397 | want: true, 398 | }, 399 | { 400 | bs: bitset{0x7fffffffffffffff, 0x0000000000000001}, 401 | n: 65, 402 | want: false, 403 | }, 404 | } 405 | for _, test := range tests { 406 | if got := test.bs.hasAll(test.n); got != test.want { 407 | t.Errorf("%v.hasAll(%d) = %t; want %t", test.bs, test.n, got, test.want) 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /sqlitex/pool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex 19 | 20 | import ( 21 | "context" 22 | "fmt" 23 | "sync" 24 | 25 | "zombiezen.com/go/sqlite" 26 | ) 27 | 28 | // PoolOptions is the set of optional arguments to [NewPool]. 29 | type PoolOptions struct { 30 | // Flags is interpreted the same way as the argument to [sqlite.Open]. 31 | // A Flags value of 0 defaults to: 32 | // 33 | // - SQLITE_OPEN_READWRITE 34 | // - SQLITE_OPEN_CREATE 35 | // - SQLITE_OPEN_WAL 36 | // - SQLITE_OPEN_URI 37 | Flags sqlite.OpenFlags 38 | 39 | // PoolSize sets an explicit size to the pool. 40 | // If less than 1, a reasonable default is used. 41 | PoolSize int 42 | 43 | // PrepareConn is called for each connection in the pool to set up functions 44 | // and other connection-specific state. 45 | PrepareConn ConnPrepareFunc 46 | } 47 | 48 | // Pool is a pool of SQLite connections. 49 | // It is safe for use by multiple goroutines concurrently. 50 | type Pool struct { 51 | free chan *sqlite.Conn 52 | closed chan struct{} 53 | prepare ConnPrepareFunc 54 | 55 | mu sync.Mutex 56 | all map[*sqlite.Conn]context.CancelFunc 57 | inited map[*sqlite.Conn]struct{} 58 | } 59 | 60 | // Open opens a fixed-size pool of SQLite connections. 61 | // 62 | // Deprecated: [NewPool] supports more options. 63 | func Open(uri string, flags sqlite.OpenFlags, poolSize int) (pool *Pool, err error) { 64 | return NewPool(uri, PoolOptions{ 65 | Flags: flags, 66 | PoolSize: poolSize, 67 | }) 68 | } 69 | 70 | // NewPool opens a fixed-size pool of SQLite connections. 71 | func NewPool(uri string, opts PoolOptions) (pool *Pool, err error) { 72 | if uri == ":memory:" { 73 | return nil, strerror{msg: `sqlite: ":memory:" does not work with multiple connections, use "file::memory:?mode=memory&cache=shared"`} 74 | } 75 | 76 | poolSize := opts.PoolSize 77 | if poolSize < 1 { 78 | poolSize = 10 79 | } 80 | p := &Pool{ 81 | free: make(chan *sqlite.Conn, poolSize), 82 | closed: make(chan struct{}), 83 | prepare: opts.PrepareConn, 84 | } 85 | defer func() { 86 | // If an error occurred, call Close outside the lock so this doesn't deadlock. 87 | if err != nil { 88 | p.Close() 89 | } 90 | }() 91 | 92 | flags := opts.Flags 93 | if flags == 0 { 94 | flags = sqlite.OpenReadWrite | 95 | sqlite.OpenCreate | 96 | sqlite.OpenWAL | 97 | sqlite.OpenURI 98 | } 99 | 100 | // TODO(maybe) 101 | // sqlitex_pool is also defined in package sqlite 102 | // const sqlitex_pool = sqlite.OpenFlags(0x01000000) 103 | // flags |= sqlitex_pool 104 | 105 | p.all = make(map[*sqlite.Conn]context.CancelFunc) 106 | for i := 0; i < poolSize; i++ { 107 | conn, err := sqlite.OpenConn(uri, flags) 108 | if err != nil { 109 | return nil, err 110 | } 111 | p.free <- conn 112 | p.all[conn] = func() {} 113 | } 114 | 115 | return p, nil 116 | } 117 | 118 | // Get returns an SQLite connection from the Pool. 119 | // 120 | // Deprecated: Use [Pool.Take] instead. 121 | // Get is an alias for backward compatibility. 122 | func (p *Pool) Get(ctx context.Context) *sqlite.Conn { 123 | if ctx == nil { 124 | ctx = context.Background() 125 | } 126 | conn, _ := p.Take(ctx) 127 | return conn 128 | } 129 | 130 | // Take returns an SQLite connection from the Pool. 131 | // 132 | // If no connection is available, 133 | // Take will block until at least one connection is returned with [Pool.Put], 134 | // or until either the Pool is closed or the context is canceled. 135 | // If no connection can be obtained 136 | // or an error occurs while preparing the connection, 137 | // an error is returned. 138 | // 139 | // The provided context is also used to control the execution lifetime of the connection. 140 | // See [sqlite.Conn.SetInterrupt] for details. 141 | // 142 | // Applications must ensure that all non-nil Conns returned from Take 143 | // are returned to the same Pool with [Pool.Put]. 144 | func (p *Pool) Take(ctx context.Context) (*sqlite.Conn, error) { 145 | select { 146 | case conn := <-p.free: 147 | ctx, cancel := context.WithCancel(ctx) 148 | conn.SetInterrupt(ctx.Done()) 149 | 150 | p.mu.Lock() 151 | p.all[conn] = cancel 152 | inited := true 153 | if p.prepare != nil { 154 | _, inited = p.inited[conn] 155 | } 156 | p.mu.Unlock() 157 | 158 | if !inited { 159 | if err := p.prepare(conn); err != nil { 160 | p.put(conn) 161 | return nil, fmt.Errorf("get sqlite connection: %w", err) 162 | } 163 | 164 | p.mu.Lock() 165 | if p.inited == nil { 166 | p.inited = make(map[*sqlite.Conn]struct{}) 167 | } 168 | p.inited[conn] = struct{}{} 169 | p.mu.Unlock() 170 | } 171 | 172 | return conn, nil 173 | case <-ctx.Done(): 174 | return nil, fmt.Errorf("get sqlite connection: %w", ctx.Err()) 175 | case <-p.closed: 176 | return nil, fmt.Errorf("get sqlite connection: pool closed") 177 | } 178 | } 179 | 180 | // Put puts an SQLite connection back into the Pool. 181 | // 182 | // Put will panic if the conn was not originally created by p. 183 | // Put(nil) is a no-op. 184 | // 185 | // Applications must ensure that all non-nil Conns returned from [Pool.Get] 186 | // are returned to the same Pool with Put. 187 | func (p *Pool) Put(conn *sqlite.Conn) { 188 | if conn == nil { 189 | // See https://github.com/zombiezen/go-sqlite/issues/17 190 | return 191 | } 192 | query := conn.CheckReset() 193 | if query != "" { 194 | panic(fmt.Sprintf( 195 | "connection returned to pool has active statement: %q", 196 | query)) 197 | } 198 | p.put(conn) 199 | } 200 | 201 | func (p *Pool) put(conn *sqlite.Conn) { 202 | p.mu.Lock() 203 | cancel, found := p.all[conn] 204 | if found { 205 | p.all[conn] = func() {} 206 | } 207 | p.mu.Unlock() 208 | 209 | if !found { 210 | panic("sqlite.Pool.Put: connection not created by this pool") 211 | } 212 | 213 | conn.SetInterrupt(nil) 214 | cancel() 215 | p.free <- conn 216 | } 217 | 218 | // Close interrupts and closes all the connections in the Pool, 219 | // blocking until all connections are returned to the Pool. 220 | func (p *Pool) Close() (err error) { 221 | close(p.closed) 222 | 223 | p.mu.Lock() 224 | n := len(p.all) 225 | cancelList := make([]context.CancelFunc, 0, n) 226 | for conn, cancel := range p.all { 227 | cancelList = append(cancelList, cancel) 228 | p.all[conn] = func() {} 229 | } 230 | p.mu.Unlock() 231 | 232 | for _, cancel := range cancelList { 233 | cancel() 234 | } 235 | for closed := 0; closed < n; closed++ { 236 | conn := <-p.free 237 | if err2 := conn.Close(); err == nil { 238 | err = err2 239 | } 240 | } 241 | return 242 | } 243 | 244 | // A ConnPrepareFunc is called for each connection in a pool 245 | // to set up connection-specific state. 246 | // It must be safe to call from multiple goroutines. 247 | // 248 | // If the ConnPrepareFunc returns an error, 249 | // then it will be called the next time the connection is about to be used. 250 | // Once ConnPrepareFunc returns nil for a given connection, 251 | // it will not be called on that connection again. 252 | type ConnPrepareFunc func(conn *sqlite.Conn) error 253 | 254 | type strerror struct { 255 | msg string 256 | } 257 | 258 | func (err strerror) Error() string { return err.msg } 259 | -------------------------------------------------------------------------------- /sqlitex/pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex_test 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "fmt" 24 | "os" 25 | "path/filepath" 26 | "sync" 27 | "testing" 28 | "time" 29 | 30 | "zombiezen.com/go/sqlite" 31 | "zombiezen.com/go/sqlite/sqlitex" 32 | ) 33 | 34 | const poolSize = 20 35 | 36 | // newMemPool returns new sqlitex.Pool attached to new database opened in memory. 37 | // 38 | // the pool is initialized with size=poolSize. 39 | // any error is t.Fatal. 40 | func newMemPool(t *testing.T) *sqlitex.Pool { 41 | t.Helper() 42 | dbpool, err := sqlitex.NewPool("file::memory:?mode=memory&cache=shared", sqlitex.PoolOptions{ 43 | Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI | sqlite.OpenSharedCache, 44 | PoolSize: poolSize, 45 | }) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | return dbpool 50 | } 51 | 52 | func TestPool(t *testing.T) { 53 | dbpool := newMemPool(t) 54 | defer func() { 55 | if err := dbpool.Close(); err != nil { 56 | t.Error(err) 57 | } 58 | }() 59 | 60 | c := dbpool.Get(nil) 61 | c.Prep("DROP TABLE IF EXISTS footable;").Step() 62 | if hasRow, err := c.Prep("CREATE TABLE footable (col1 integer);").Step(); err != nil { 63 | t.Fatal(err) 64 | } else if hasRow { 65 | t.Errorf("CREATE TABLE reports having a row") 66 | } 67 | dbpool.Put(c) 68 | c = nil 69 | 70 | var wg sync.WaitGroup 71 | for i := 0; i < poolSize; i++ { 72 | wg.Add(1) 73 | go func(i int) { 74 | for j := 0; j < 10; j++ { 75 | testInsert(t, fmt.Sprintf("%d-%d", i, j), dbpool) 76 | } 77 | wg.Done() 78 | }(i) 79 | } 80 | wg.Wait() 81 | 82 | c = dbpool.Get(nil) 83 | defer dbpool.Put(c) 84 | stmt := c.Prep("SELECT COUNT(*) FROM footable;") 85 | if hasRow, err := stmt.Step(); err != nil { 86 | t.Fatal(err) 87 | } else if hasRow { 88 | count := int(stmt.ColumnInt64(0)) 89 | if want := poolSize * 10 * insertCount; count != want { 90 | t.Errorf("SELECT COUNT(*) = %d, want %d", count, want) 91 | } 92 | } else { 93 | t.Errorf("SELECT COUNT(*) reports not having a row") 94 | } 95 | stmt.Reset() 96 | } 97 | 98 | const insertCount = 120 99 | 100 | func testInsert(t *testing.T, id string, dbpool *sqlitex.Pool) { 101 | c := dbpool.Get(nil) 102 | defer dbpool.Put(c) 103 | 104 | begin := c.Prep("BEGIN;") 105 | commit := c.Prep("COMMIT;") 106 | stmt := c.Prep("INSERT INTO footable (col1) VALUES (?);") 107 | 108 | if _, err := begin.Step(); err != nil { 109 | t.Errorf("id=%s: BEGIN step: %v", id, err) 110 | } 111 | for i := int64(0); i < insertCount; i++ { 112 | if err := stmt.Reset(); err != nil { 113 | t.Errorf("id=%s: reset: %v", id, err) 114 | } 115 | stmt.BindInt64(1, i) 116 | if _, err := stmt.Step(); err != nil { 117 | t.Errorf("id=%s: step: %v", id, err) 118 | } 119 | } 120 | if _, err := commit.Step(); err != nil { 121 | t.Errorf("id=%s: COMMIT step: %v", id, err) 122 | } 123 | } 124 | 125 | func TestPoolAfterClose(t *testing.T) { 126 | // verify that Get after close never try to initialize a Conn and segfault 127 | dbpool := newMemPool(t) 128 | 129 | err := dbpool.Close() 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | for i := 0; i < 10*poolSize; i++ { 135 | conn := dbpool.Get(nil) 136 | if conn != nil { 137 | t.Fatal("dbpool: Get after Close -> !nil conn") 138 | } 139 | } 140 | } 141 | 142 | func TestSharedCacheLock(t *testing.T) { 143 | dir, err := os.MkdirTemp("", "sqlite-test-") 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | defer os.RemoveAll(dir) 148 | 149 | dbFile := filepath.Join(dir, "awal.db") 150 | 151 | c0, err := sqlite.OpenConn(dbFile, 0) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | defer func() { 156 | if err := c0.Close(); err != nil { 157 | t.Error(err) 158 | } 159 | }() 160 | // TODO(someday): CI fails without this old behavior. 161 | c0.SetBusyTimeout(10 * time.Second) 162 | 163 | err = sqlitex.ExecScript(c0, ` 164 | DROP TABLE IF EXISTS t; 165 | CREATE TABLE t (c, content BLOB); 166 | DROP TABLE IF EXISTS t2; 167 | CREATE TABLE t2 (c); 168 | INSERT INTO t2 (c) VALUES ('hello'); 169 | `) 170 | if err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | c1, err := sqlite.OpenConn(dbFile, 0) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | defer func() { 179 | if err := c1.Close(); err != nil { 180 | t.Error(err) 181 | } 182 | }() 183 | // TODO(someday): CI fails without this old behavior. 184 | c1.SetBusyTimeout(10 * time.Second) 185 | 186 | c0Lock := func() { 187 | if _, err := c0.Prep("BEGIN;").Step(); err != nil { 188 | t.Fatal(err) 189 | } 190 | if _, err := c0.Prep("INSERT INTO t (c, content) VALUES (0, 'hi');").Step(); err != nil { 191 | t.Fatal(err) 192 | } 193 | } 194 | c0Unlock := func() { 195 | if err := sqlitex.Execute(c0, "COMMIT;", nil); err != nil { 196 | t.Fatal(err) 197 | } 198 | } 199 | 200 | c0Lock() 201 | 202 | stmt := c1.Prep("INSERT INTO t (c) VALUES (1);") 203 | 204 | done := make(chan struct{}) 205 | go func() { 206 | if _, err := stmt.Step(); err != nil { 207 | t.Error(err) 208 | return 209 | } 210 | close(done) 211 | }() 212 | 213 | time.Sleep(10 * time.Millisecond) 214 | select { 215 | case <-done: 216 | t.Error("insert done while transaction was held") 217 | default: 218 | } 219 | 220 | c0Unlock() 221 | 222 | // End the initial transaction, allowing the goroutine to complete 223 | select { 224 | case <-done: 225 | case <-time.After(500 * time.Millisecond): 226 | t.Error("second connection insert not completing") 227 | } 228 | 229 | // TODO: It is possible for stmt.Reset to return SQLITE_LOCKED. 230 | // Work out why and find a way to test it. 231 | } 232 | 233 | func TestPoolPutMatch(t *testing.T) { 234 | dbpool0 := newMemPool(t) 235 | dbpool1 := newMemPool(t) 236 | defer func() { 237 | if err := dbpool0.Close(); err != nil { 238 | t.Error(err) 239 | } 240 | if err := dbpool1.Close(); err != nil { 241 | t.Error(err) 242 | } 243 | }() 244 | 245 | func() { 246 | c := dbpool0.Get(nil) 247 | defer func() { 248 | if r := recover(); r == nil { 249 | t.Error("expect put mismatch panic, got none") 250 | } 251 | dbpool0.Put(c) 252 | }() 253 | 254 | dbpool1.Put(c) 255 | }() 256 | } 257 | 258 | // See https://github.com/crawshaw/sqlite/issues/119 and 259 | // https://github.com/zombiezen/go-sqlite/issues/14 260 | func TestPoolWALClose(t *testing.T) { 261 | dbName := filepath.Join(t.TempDir(), "wal-close.db") 262 | pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ 263 | Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, 264 | PoolSize: 10, 265 | }) 266 | if err != nil { 267 | t.Fatal(err) 268 | } 269 | conn := pool.Get(context.Background()) 270 | if _, err := os.Stat(dbName + "-wal"); err != nil { 271 | t.Error(err) 272 | } 273 | err = sqlitex.ExecTransient(conn, `CREATE TABLE foo (id integer primary key);`, nil) 274 | if err != nil { 275 | t.Error(err) 276 | } 277 | pool.Put(conn) 278 | if err := pool.Close(); err != nil { 279 | t.Error(err) 280 | } 281 | if _, err := os.Stat(dbName + "-wal"); !errors.Is(err, os.ErrNotExist) { 282 | t.Errorf("After close: %v; want %v", err, os.ErrNotExist) 283 | } 284 | } 285 | 286 | func TestPoolPrepareConn(t *testing.T) { 287 | dbName := filepath.Join(t.TempDir(), "foo.db") 288 | pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ 289 | PrepareConn: func(conn *sqlite.Conn) error { 290 | err := sqlitex.ExecuteTransient(conn, `PRAGMA foreign_keys = on;`, nil) 291 | if err != nil { 292 | t.Error("Prepare internal error:", err) 293 | } 294 | return err 295 | }, 296 | }) 297 | if err != nil { 298 | t.Fatal(err) 299 | } 300 | defer func() { 301 | if err := pool.Close(); err != nil { 302 | t.Error("Close:", err) 303 | } 304 | }() 305 | 306 | conn := pool.Get(context.Background()) 307 | if conn == nil { 308 | t.Fatal("conn == nil") 309 | } 310 | defer pool.Put(conn) 311 | 312 | fkEnabled := false 313 | err = sqlitex.ExecuteTransient(conn, `PRAGMA foreign_keys;`, &sqlitex.ExecOptions{ 314 | ResultFunc: func(stmt *sqlite.Stmt) error { 315 | fkEnabled = stmt.ColumnBool(0) 316 | return nil 317 | }, 318 | }) 319 | if err != nil { 320 | t.Fatal(err) 321 | } 322 | if !fkEnabled { 323 | t.Error("foreign_keys not enabled") 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /sqlitex/query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlitex 5 | 6 | import ( 7 | "errors" 8 | 9 | "zombiezen.com/go/sqlite" 10 | ) 11 | 12 | var ( 13 | errNoResults = errors.New("sqlite: statement has no results") 14 | errMultipleResults = errors.New("sqlite: statement has multiple result rows") 15 | ) 16 | 17 | func resultSetup(stmt *sqlite.Stmt) error { 18 | hasRow, err := stmt.Step() 19 | if err != nil { 20 | stmt.Reset() 21 | return err 22 | } 23 | if !hasRow { 24 | stmt.Reset() 25 | return errNoResults 26 | } 27 | return nil 28 | } 29 | 30 | func resultTeardown(stmt *sqlite.Stmt) error { 31 | hasRow, err := stmt.Step() 32 | if err != nil { 33 | stmt.Reset() 34 | return err 35 | } 36 | if hasRow { 37 | stmt.Reset() 38 | return errMultipleResults 39 | } 40 | return stmt.Reset() 41 | } 42 | 43 | // ResultBool reports whether the first column of the first and only row 44 | // produced by running stmt 45 | // is non-zero. 46 | // It returns an error if there is not exactly one result row. 47 | func ResultBool(stmt *sqlite.Stmt) (bool, error) { 48 | res, err := ResultInt64(stmt) 49 | return res != 0, err 50 | } 51 | 52 | // ResultInt returns the first column of the first and only row 53 | // produced by running stmt 54 | // as an integer. 55 | // It returns an error if there is not exactly one result row. 56 | func ResultInt(stmt *sqlite.Stmt) (int, error) { 57 | res, err := ResultInt64(stmt) 58 | return int(res), err 59 | } 60 | 61 | // ResultInt64 returns the first column of the first and only row 62 | // produced by running stmt 63 | // as an integer. 64 | // It returns an error if there is not exactly one result row. 65 | func ResultInt64(stmt *sqlite.Stmt) (int64, error) { 66 | if err := resultSetup(stmt); err != nil { 67 | return 0, err 68 | } 69 | res := stmt.ColumnInt64(0) 70 | if err := resultTeardown(stmt); err != nil { 71 | return 0, err 72 | } 73 | return res, nil 74 | } 75 | 76 | // ResultText returns the first column of the first and only row 77 | // produced by running stmt 78 | // as text. 79 | // It returns an error if there is not exactly one result row. 80 | func ResultText(stmt *sqlite.Stmt) (string, error) { 81 | if err := resultSetup(stmt); err != nil { 82 | return "", err 83 | } 84 | res := stmt.ColumnText(0) 85 | if err := resultTeardown(stmt); err != nil { 86 | return "", err 87 | } 88 | return res, nil 89 | } 90 | 91 | // ResultFloat returns the first column of the first and only row 92 | // produced by running stmt 93 | // as a real number. 94 | // It returns an error if there is not exactly one result row. 95 | func ResultFloat(stmt *sqlite.Stmt) (float64, error) { 96 | if err := resultSetup(stmt); err != nil { 97 | return 0, err 98 | } 99 | res := stmt.ColumnFloat(0) 100 | if err := resultTeardown(stmt); err != nil { 101 | return 0, err 102 | } 103 | return res, nil 104 | } 105 | 106 | // ResultBytes reads the first column of the first and only row 107 | // produced by running stmt into buf, 108 | // returning the number of bytes read. 109 | // It returns an error if there is not exactly one result row. 110 | func ResultBytes(stmt *sqlite.Stmt, buf []byte) (int, error) { 111 | if err := resultSetup(stmt); err != nil { 112 | return 0, err 113 | } 114 | read := stmt.ColumnBytes(0, buf) 115 | if err := resultTeardown(stmt); err != nil { 116 | return 0, err 117 | } 118 | return read, nil 119 | } 120 | -------------------------------------------------------------------------------- /sqlitex/query_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlitex 5 | 6 | import ( 7 | "testing" 8 | 9 | "zombiezen.com/go/sqlite" 10 | ) 11 | 12 | func TestResultInt64(t *testing.T) { 13 | conn, err := sqlite.OpenConn(":memory:") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer func() { 18 | if err := conn.Close(); err != nil { 19 | t.Error(err) 20 | } 21 | }() 22 | 23 | err = ExecuteScript(conn, ` 24 | CREATE TABLE foo ( 25 | id integer not null primary key 26 | ); 27 | 28 | INSERT INTO foo VALUES (1), (2);`, nil) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | t.Run("Single", func(t *testing.T) { 34 | stmt, _, err := conn.PrepareTransient(`SELECT 42;`) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | defer stmt.Finalize() 39 | 40 | const want = 42 41 | got, err := ResultInt64(stmt) 42 | if got != want || err != nil { 43 | t.Errorf("ResultInt64(...) = %d, %v; want %d, ", got, err, want) 44 | } 45 | }) 46 | 47 | t.Run("Multiple", func(t *testing.T) { 48 | stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | defer stmt.Finalize() 53 | 54 | n, err := ResultInt64(stmt) 55 | if n != 0 || err == nil { 56 | t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) 57 | } else { 58 | t.Log("Returned (expected) error:", err) 59 | } 60 | }) 61 | 62 | t.Run("NoRows", func(t *testing.T) { 63 | stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | defer stmt.Finalize() 68 | 69 | n, err := ResultInt64(stmt) 70 | if n != 0 || err == nil { 71 | t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) 72 | } else { 73 | t.Log("Returned (expected) error:", err) 74 | } 75 | }) 76 | } 77 | 78 | func TestResultBool(t *testing.T) { 79 | conn, err := sqlite.OpenConn(":memory:") 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | defer func() { 84 | if err := conn.Close(); err != nil { 85 | t.Error(err) 86 | } 87 | }() 88 | 89 | err = ExecuteScript(conn, ` 90 | CREATE TABLE foo ( 91 | id integer not null primary key 92 | ); 93 | 94 | INSERT INTO foo VALUES (1), (2);`, nil) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | t.Run("False", func(t *testing.T) { 100 | stmt, _, err := conn.PrepareTransient(`SELECT false;`) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | defer stmt.Finalize() 105 | 106 | got, err := ResultBool(stmt) 107 | if got || err != nil { 108 | t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) 109 | } 110 | }) 111 | 112 | t.Run("True", func(t *testing.T) { 113 | stmt, _, err := conn.PrepareTransient(`SELECT true;`) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | defer stmt.Finalize() 118 | 119 | got, err := ResultBool(stmt) 120 | if !got || err != nil { 121 | t.Errorf("ResultBool(...) = %t, %v; want true, ", got, err) 122 | } 123 | }) 124 | 125 | t.Run("Multiple", func(t *testing.T) { 126 | stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo;`) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | defer stmt.Finalize() 131 | 132 | got, err := ResultBool(stmt) 133 | if got || err == nil { 134 | t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) 135 | } else { 136 | t.Log("Returned (expected) error:", err) 137 | } 138 | }) 139 | 140 | t.Run("NoRows", func(t *testing.T) { 141 | stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo WHERE id > 3;`) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | defer stmt.Finalize() 146 | 147 | got, err := ResultBool(stmt) 148 | if got || err == nil { 149 | t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) 150 | } else { 151 | t.Log("Returned (expected) error:", err) 152 | } 153 | }) 154 | } 155 | 156 | func TestResultText(t *testing.T) { 157 | conn, err := sqlite.OpenConn(":memory:") 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | defer func() { 162 | if err := conn.Close(); err != nil { 163 | t.Error(err) 164 | } 165 | }() 166 | 167 | err = ExecuteScript(conn, ` 168 | CREATE TABLE foo ( 169 | id integer not null primary key, 170 | my_blob blob 171 | ); 172 | 173 | INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) 174 | if err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | t.Run("Single", func(t *testing.T) { 179 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | defer stmt.Finalize() 184 | 185 | const want = "hi" 186 | got, err := ResultText(stmt) 187 | if got != want || err != nil { 188 | t.Errorf("ResultText(...) = %q, %v; want %q, ", got, err, want) 189 | } 190 | }) 191 | 192 | t.Run("Multiple", func(t *testing.T) { 193 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) 194 | if err != nil { 195 | t.Fatal(err) 196 | } 197 | defer stmt.Finalize() 198 | 199 | got, err := ResultText(stmt) 200 | if got != "" || err == nil { 201 | t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) 202 | } else { 203 | t.Log("Returned (expected) error:", err) 204 | } 205 | }) 206 | 207 | t.Run("NoRows", func(t *testing.T) { 208 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | defer stmt.Finalize() 213 | 214 | got, err := ResultText(stmt) 215 | if got != "" || err == nil { 216 | t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) 217 | } else { 218 | t.Log("Returned (expected) error:", err) 219 | } 220 | }) 221 | } 222 | 223 | func TestResultFloat(t *testing.T) { 224 | conn, err := sqlite.OpenConn(":memory:") 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | defer func() { 229 | if err := conn.Close(); err != nil { 230 | t.Error(err) 231 | } 232 | }() 233 | 234 | err = ExecuteScript(conn, ` 235 | CREATE TABLE foo ( 236 | id integer not null primary key 237 | ); 238 | 239 | INSERT INTO foo VALUES (1), (2);`, nil) 240 | if err != nil { 241 | t.Fatal(err) 242 | } 243 | 244 | t.Run("Single", func(t *testing.T) { 245 | stmt, _, err := conn.PrepareTransient(`SELECT 42;`) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | defer stmt.Finalize() 250 | 251 | const want = 42.0 252 | got, err := ResultFloat(stmt) 253 | if got != want || err != nil { 254 | t.Errorf("ResultFloat(...) = %g, %v; want %g, ", got, err, want) 255 | } 256 | }) 257 | 258 | t.Run("Multiple", func(t *testing.T) { 259 | stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | defer stmt.Finalize() 264 | 265 | n, err := ResultFloat(stmt) 266 | if n != 0 || err == nil { 267 | t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) 268 | } else { 269 | t.Log("Returned (expected) error:", err) 270 | } 271 | }) 272 | 273 | t.Run("NoRows", func(t *testing.T) { 274 | stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | defer stmt.Finalize() 279 | 280 | n, err := ResultFloat(stmt) 281 | if n != 0 || err == nil { 282 | t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) 283 | } else { 284 | t.Log("Returned (expected) error:", err) 285 | } 286 | }) 287 | } 288 | 289 | func TestResultBytes(t *testing.T) { 290 | conn, err := sqlite.OpenConn(":memory:") 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | defer func() { 295 | if err := conn.Close(); err != nil { 296 | t.Error(err) 297 | } 298 | }() 299 | 300 | err = ExecuteScript(conn, ` 301 | CREATE TABLE foo ( 302 | id integer not null primary key, 303 | my_blob blob 304 | ); 305 | 306 | INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) 307 | if err != nil { 308 | t.Fatal(err) 309 | } 310 | 311 | t.Run("Single", func(t *testing.T) { 312 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) 313 | if err != nil { 314 | t.Fatal(err) 315 | } 316 | defer stmt.Finalize() 317 | 318 | const want = "hi" 319 | buf := make([]byte, 4096) 320 | n, err := ResultBytes(stmt, buf) 321 | if n != len(want) || err != nil { 322 | t.Errorf("ResultBytes(...) = %d, %v; want %d, ", n, err, len(want)) 323 | } 324 | if got := string(buf[:n]); got != want { 325 | t.Errorf("result = %q; want %q", got, want) 326 | } 327 | }) 328 | 329 | t.Run("Multiple", func(t *testing.T) { 330 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) 331 | if err != nil { 332 | t.Fatal(err) 333 | } 334 | defer stmt.Finalize() 335 | 336 | buf := make([]byte, 4096) 337 | n, err := ResultBytes(stmt, buf) 338 | if n != 0 || err == nil { 339 | t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) 340 | } else { 341 | t.Log("Returned (expected) error:", err) 342 | } 343 | }) 344 | 345 | t.Run("NoRows", func(t *testing.T) { 346 | stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) 347 | if err != nil { 348 | t.Fatal(err) 349 | } 350 | defer stmt.Finalize() 351 | 352 | buf := make([]byte, 4096) 353 | n, err := ResultBytes(stmt, buf) 354 | if n != 0 || err == nil { 355 | t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) 356 | } else { 357 | t.Log("Returned (expected) error:", err) 358 | } 359 | }) 360 | } 361 | -------------------------------------------------------------------------------- /sqlitex/rand_id.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex 19 | 20 | import ( 21 | "crypto/rand" 22 | "fmt" 23 | "math/big" 24 | 25 | "zombiezen.com/go/sqlite" 26 | ) 27 | 28 | // InsertRandID executes stmt with a random value in the range [min, max) for $param. 29 | func InsertRandID(stmt *sqlite.Stmt, param string, min, max int64) (int64, error) { 30 | if min < 0 { 31 | return 0, fmt.Errorf("sqlitex.InsertRandID: min (%d) is negative", min) 32 | } 33 | 34 | for i := 0; ; i++ { 35 | v, err := rand.Int(rand.Reader, big.NewInt(max-min)) 36 | if err != nil { 37 | return 0, fmt.Errorf("sqlitex.InsertRandID: %w", err) 38 | } 39 | id := v.Int64() + min 40 | 41 | stmt.Reset() 42 | stmt.SetInt64(param, id) 43 | _, err = stmt.Step() 44 | if err == nil { 45 | return id, nil 46 | } 47 | if i >= 100 || sqlite.ErrCode(err) != sqlite.ResultConstraintPrimaryKey { 48 | return 0, fmt.Errorf("sqlitex.InsertRandID: %w", err) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sqlitex/rand_id_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex_test 19 | 20 | import ( 21 | "testing" 22 | 23 | "zombiezen.com/go/sqlite" 24 | "zombiezen.com/go/sqlite/sqlitex" 25 | ) 26 | 27 | func TestRandID(t *testing.T) { 28 | conn, err := sqlite.OpenConn(":memory:", 0) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | defer conn.Close() 33 | 34 | if err := sqlitex.ExecTransient(conn, "CREATE TABLE t (key PRIMARY KEY, val TEXT);", nil); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | stmt := conn.Prep(`INSERT INTO t (key, val) VALUES ($key, $val);`) 39 | stmt.SetText("$val", "value") 40 | 41 | for i := 0; i < 10; i++ { 42 | const min, max = 1000, 10000 43 | 44 | id, err := sqlitex.InsertRandID(stmt, "$key", min, max) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if id < min || id >= max { 49 | t.Errorf("id %d out of range [%d, %d)", id, min, max) 50 | } 51 | 52 | countStmt := conn.Prep("SELECT count(*) FROM t WHERE key = $key;") 53 | countStmt.SetInt64("$key", id) 54 | if count, err := sqlitex.ResultInt(countStmt); err != nil { 55 | t.Fatal(err) 56 | } else if count != 1 { 57 | t.Errorf("missing key %d", id) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sqlitex/savepoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 David Crawshaw 2 | // Copyright (c) 2021 Roxy Light 3 | // 4 | // Permission to use, copy, modify, and distribute this software for any 5 | // purpose with or without fee is hereby granted, provided that the above 6 | // copyright notice and this permission notice appear in all copies. 7 | // 8 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | // 16 | // SPDX-License-Identifier: ISC 17 | 18 | package sqlitex 19 | 20 | import ( 21 | "fmt" 22 | "runtime" 23 | "strings" 24 | 25 | "zombiezen.com/go/sqlite" 26 | ) 27 | 28 | // Save creates a named SQLite transaction using SAVEPOINT. 29 | // 30 | // On success Savepoint returns a releaseFn that will call either 31 | // RELEASE or ROLLBACK depending on whether the parameter *error 32 | // points to a nil or non-nil error. This is designed to be deferred. 33 | // 34 | // https://www.sqlite.org/lang_savepoint.html 35 | func Save(conn *sqlite.Conn) (releaseFn func(*error)) { 36 | name := "sqlitex.Save" // safe as names can be reused 37 | var pc [3]uintptr 38 | if n := runtime.Callers(0, pc[:]); n > 0 { 39 | frames := runtime.CallersFrames(pc[:n]) 40 | if _, more := frames.Next(); more { // runtime.Callers 41 | if _, more := frames.Next(); more { // savepoint.Save 42 | frame, _ := frames.Next() // caller we care about 43 | if frame.Function != "" { 44 | name = frame.Function 45 | } 46 | } 47 | } 48 | } 49 | 50 | releaseFn, err := savepoint(conn, name) 51 | if err != nil { 52 | if sqlite.ErrCode(err) == sqlite.ResultInterrupt { 53 | return func(errp *error) { 54 | if *errp == nil { 55 | *errp = err 56 | } 57 | } 58 | } 59 | panic(err) 60 | } 61 | return releaseFn 62 | } 63 | 64 | func savepoint(conn *sqlite.Conn, name string) (releaseFn func(*error), err error) { 65 | if strings.Contains(name, `"`) { 66 | return nil, fmt.Errorf("sqlitex.Savepoint: invalid name: %q", name) 67 | } 68 | if err := Execute(conn, fmt.Sprintf("SAVEPOINT %q;", name), nil); err != nil { 69 | return nil, err 70 | } 71 | releaseFn = func(errp *error) { 72 | recoverP := recover() 73 | 74 | // If a query was interrupted or if a user exec'd COMMIT or 75 | // ROLLBACK, then everything was already rolled back 76 | // automatically, thus returning the connection to autocommit 77 | // mode. 78 | if conn.AutocommitEnabled() { 79 | // There is nothing to rollback. 80 | if recoverP != nil { 81 | panic(recoverP) 82 | } 83 | return 84 | } 85 | 86 | if *errp == nil && recoverP == nil { 87 | // Success path. Release the savepoint successfully. 88 | *errp = Execute(conn, fmt.Sprintf("RELEASE %q;", name), nil) 89 | if *errp == nil { 90 | return 91 | } 92 | // Possible interrupt. Fall through to the error path. 93 | if conn.AutocommitEnabled() { 94 | // There is nothing to rollback. 95 | if recoverP != nil { 96 | panic(recoverP) 97 | } 98 | return 99 | } 100 | } 101 | 102 | orig := "" 103 | if *errp != nil { 104 | orig = (*errp).Error() + "\n\t" 105 | } 106 | 107 | // Error path. 108 | 109 | // Always run ROLLBACK even if the connection has been interrupted. 110 | oldDoneCh := conn.SetInterrupt(nil) 111 | defer conn.SetInterrupt(oldDoneCh) 112 | 113 | err := Execute(conn, fmt.Sprintf("ROLLBACK TO %q;", name), nil) 114 | if err != nil { 115 | panic(orig + err.Error()) 116 | } 117 | err = Execute(conn, fmt.Sprintf("RELEASE %q;", name), nil) 118 | if err != nil { 119 | panic(orig + err.Error()) 120 | } 121 | 122 | if recoverP != nil { 123 | panic(recoverP) 124 | } 125 | } 126 | return releaseFn, nil 127 | } 128 | 129 | // Transaction creates a DEFERRED SQLite transaction. 130 | // 131 | // On success Transaction returns an endFn that will call either 132 | // COMMIT or ROLLBACK depending on whether the parameter *error 133 | // points to a nil or non-nil error. This is designed to be deferred. 134 | // 135 | // https://www.sqlite.org/lang_transaction.html 136 | func Transaction(conn *sqlite.Conn) (endFn func(*error)) { 137 | endFn, err := transaction(conn, "DEFERRED") 138 | if err != nil { 139 | if sqlite.ErrCode(err) == sqlite.ResultInterrupt { 140 | return func(errp *error) { 141 | if *errp == nil { 142 | *errp = err 143 | } 144 | } 145 | } 146 | panic(err) 147 | } 148 | return endFn 149 | } 150 | 151 | // ImmediateTransaction creates an IMMEDIATE SQLite transaction. 152 | // 153 | // On success ImmediateTransaction returns an endFn that will call either 154 | // COMMIT or ROLLBACK depending on whether the parameter *error 155 | // points to a nil or non-nil error. This is designed to be deferred. 156 | // 157 | // https://www.sqlite.org/lang_transaction.html 158 | func ImmediateTransaction(conn *sqlite.Conn) (endFn func(*error), err error) { 159 | endFn, err = transaction(conn, "IMMEDIATE") 160 | if err != nil { 161 | return func(*error) {}, err 162 | } 163 | return endFn, nil 164 | } 165 | 166 | // ExclusiveTransaction creates an EXCLUSIVE SQLite transaction. 167 | // 168 | // On success ImmediateTransaction returns an endFn that will call either 169 | // COMMIT or ROLLBACK depending on whether the parameter *error 170 | // points to a nil or non-nil error. This is designed to be deferred. 171 | // 172 | // https://www.sqlite.org/lang_transaction.html 173 | func ExclusiveTransaction(conn *sqlite.Conn) (endFn func(*error), err error) { 174 | endFn, err = transaction(conn, "EXCLUSIVE") 175 | if err != nil { 176 | return func(*error) {}, err 177 | } 178 | return endFn, nil 179 | } 180 | 181 | func transaction(conn *sqlite.Conn, mode string) (endFn func(*error), err error) { 182 | if err := Execute(conn, "BEGIN "+mode+";", nil); err != nil { 183 | return nil, err 184 | } 185 | endFn = func(errp *error) { 186 | recoverP := recover() 187 | 188 | // If a query was interrupted or if a user exec'd COMMIT or 189 | // ROLLBACK, then everything was already rolled back 190 | // automatically, thus returning the connection to autocommit 191 | // mode. 192 | if conn.AutocommitEnabled() { 193 | // There is nothing to rollback. 194 | if recoverP != nil { 195 | panic(recoverP) 196 | } 197 | return 198 | } 199 | 200 | if *errp == nil && recoverP == nil { 201 | // Success path. Commit the transaction. 202 | *errp = Execute(conn, "COMMIT;", nil) 203 | if *errp == nil { 204 | return 205 | } 206 | // Possible interrupt. Fall through to the error path. 207 | if conn.AutocommitEnabled() { 208 | // There is nothing to rollback. 209 | if recoverP != nil { 210 | panic(recoverP) 211 | } 212 | return 213 | } 214 | } 215 | 216 | orig := "" 217 | if *errp != nil { 218 | orig = (*errp).Error() + "\n\t" 219 | } 220 | 221 | // Error path. 222 | 223 | // Always run ROLLBACK even if the connection has been interrupted. 224 | oldDoneCh := conn.SetInterrupt(nil) 225 | defer conn.SetInterrupt(oldDoneCh) 226 | 227 | err := Execute(conn, "ROLLBACK;", nil) 228 | if err != nil { 229 | panic(orig + err.Error()) 230 | } 231 | 232 | if recoverP != nil { 233 | panic(recoverP) 234 | } 235 | } 236 | return endFn, nil 237 | } 238 | -------------------------------------------------------------------------------- /vtable_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Roxy Light 2 | // SPDX-License-Identifier: ISC 3 | 4 | package sqlite_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "zombiezen.com/go/sqlite" 11 | "zombiezen.com/go/sqlite/sqlitex" 12 | ) 13 | 14 | func ExampleVTable() { 15 | conn, err := sqlite.OpenConn(":memory:") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer conn.Close() 20 | 21 | err = conn.SetModule("templatevtab", &sqlite.Module{ 22 | Connect: templatevtabConnect, 23 | }) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | err = sqlitex.ExecuteTransient( 28 | conn, 29 | `SELECT a, b FROM templatevtab ORDER BY rowid;`, 30 | &sqlitex.ExecOptions{ 31 | ResultFunc: func(stmt *sqlite.Stmt) error { 32 | fmt.Printf("%4d, %4d\n", stmt.ColumnInt(0), stmt.ColumnInt(1)) 33 | return nil 34 | }, 35 | }, 36 | ) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // Output: 42 | // 1001, 2001 43 | // 1002, 2002 44 | // 1003, 2003 45 | // 1004, 2004 46 | // 1005, 2005 47 | // 1006, 2006 48 | // 1007, 2007 49 | // 1008, 2008 50 | // 1009, 2009 51 | // 1010, 2010 52 | } 53 | 54 | type templatevtab struct{} 55 | 56 | const ( 57 | templatevarColumnA = iota 58 | templatevarColumnB 59 | ) 60 | 61 | func templatevtabConnect(c *sqlite.Conn, opts *sqlite.VTableConnectOptions) (sqlite.VTable, *sqlite.VTableConfig, error) { 62 | vtab := new(templatevtab) 63 | cfg := &sqlite.VTableConfig{ 64 | Declaration: "CREATE TABLE x(a,b)", 65 | } 66 | return vtab, cfg, nil 67 | } 68 | 69 | func (vt *templatevtab) BestIndex(*sqlite.IndexInputs) (*sqlite.IndexOutputs, error) { 70 | return &sqlite.IndexOutputs{ 71 | EstimatedCost: 10, 72 | EstimatedRows: 10, 73 | }, nil 74 | } 75 | 76 | func (vt *templatevtab) Open() (sqlite.VTableCursor, error) { 77 | return &templatevtabCursor{rowid: 1}, nil 78 | } 79 | 80 | func (vt *templatevtab) Disconnect() error { 81 | return nil 82 | } 83 | 84 | func (vt *templatevtab) Destroy() error { 85 | return nil 86 | } 87 | 88 | type templatevtabCursor struct { 89 | rowid int64 90 | } 91 | 92 | func (cur *templatevtabCursor) Filter(id sqlite.IndexID, argv []sqlite.Value) error { 93 | cur.rowid = 1 94 | return nil 95 | } 96 | 97 | func (cur *templatevtabCursor) Next() error { 98 | cur.rowid++ 99 | return nil 100 | } 101 | 102 | func (cur *templatevtabCursor) Column(i int, noChange bool) (sqlite.Value, error) { 103 | switch i { 104 | case templatevarColumnA: 105 | return sqlite.IntegerValue(1000 + cur.rowid), nil 106 | case templatevarColumnB: 107 | return sqlite.IntegerValue(2000 + cur.rowid), nil 108 | default: 109 | panic("unreachable") 110 | } 111 | } 112 | 113 | func (cur *templatevtabCursor) RowID() (int64, error) { 114 | return cur.rowid, nil 115 | } 116 | 117 | func (cur *templatevtabCursor) EOF() bool { 118 | return cur.rowid > 10 119 | } 120 | 121 | func (cur *templatevtabCursor) Close() error { 122 | return nil 123 | } 124 | --------------------------------------------------------------------------------