pax_global_header00006660000000000000000000000064150141711650014513gustar00rootroot0000000000000052 comment=e5fb83745cf1640f27b86f6560ca32f50b2442e9 zombiezen-go-sqlite-297af96/000077500000000000000000000000001501417116500157705ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/.envrc000066400000000000000000000000421501417116500171020ustar00rootroot00000000000000# shellcheck shell=bash use flake zombiezen-go-sqlite-297af96/.github/000077500000000000000000000000001501417116500173305ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/.github/FUNDING.yml000066400000000000000000000000221501417116500211370ustar00rootroot00000000000000github: zombiezen zombiezen-go-sqlite-297af96/.github/dependabot.yml000066400000000000000000000002741501417116500221630ustar00rootroot00000000000000# Copyright 2021 Roxy Light # SPDX-License-Identifier: ISC version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: daily reviewers: [zombiezen] zombiezen-go-sqlite-297af96/.github/workflows/000077500000000000000000000000001501417116500213655ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/.github/workflows/build.yml000066400000000000000000000024641501417116500232150ustar00rootroot00000000000000# Copyright 2021 Roxy Light # SPDX-License-Identifier: ISC name: Build on: - push - pull_request permissions: contents: read jobs: build: name: Build runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] go: ["1.24"] arch: [amd64] include: - os: ubuntu-latest go: "1.24" arch: "386" steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Run tests with race detector run: go test -mod=readonly -race -tags="libc.memgrind" -v ./... if: ${{ matrix.arch == 'amd64' }} env: GOARCH: ${{ matrix.arch }} - name: Run tests without race detector run: go test -mod=readonly -v -tags="libc.memgrind" ./... if: ${{ matrix.arch != 'amd64' }} env: GOARCH: ${{ matrix.arch }} migrate: name: Migration Tool runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.24" - name: Run tests run: go test -mod=readonly -race -v ./... working-directory: ./cmd/zombiezen-sqlite-migrate zombiezen-go-sqlite-297af96/.github/workflows/publish.yml000066400000000000000000000005701501417116500235600ustar00rootroot00000000000000# Copyright 2021 Roxy Light # SPDX-License-Identifier: ISC name: Publish on: release: types: [published] permissions: {} jobs: go-get: name: go get runs-on: ubuntu-latest steps: - name: Fetch release from proxy run: | curl -fsSL "https://proxy.golang.org/zombiezen.com/go/sqlite/@v/$(echo "$GITHUB_REF" | sed -e 's:^refs/tags/::').info" zombiezen-go-sqlite-297af96/.gitignore000066400000000000000000000003001501417116500177510ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out zombiezen-go-sqlite-297af96/.vscode/000077500000000000000000000000001501417116500173315ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/.vscode/copyright.code-snippets000066400000000000000000000003151501417116500240370ustar00rootroot00000000000000{ "Copyright": { "prefix": "copyright", "body": [ "$LINE_COMMENT Copyright $CURRENT_YEAR Roxy Light", "$LINE_COMMENT SPDX-License-Identifier: ISC", ], "description": "Copyright header" } } zombiezen-go-sqlite-297af96/.vscode/extensions.json000066400000000000000000000002141501417116500224200ustar00rootroot00000000000000{ // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. "recommendations": [ "golang.go" ] } zombiezen-go-sqlite-297af96/CHANGELOG.md000066400000000000000000000440221501417116500176030ustar00rootroot00000000000000# `zombiezen.com/go/sqlite` Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [Unreleased]: https://github.com/zombiezen/go-sqlite/compare/v1.4.2...main ## [1.4.2][] - 2025-05-23 Version 1.4.2 updates the `modernc.org/sqlite` version to 1.37.1. [1.4.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.4.2 ### Changed - The minimum `modernc.org/sqlite` version updated to 1.37.1. ## [1.4.1][] - 2025-05-23 Version 1.4.1 updates the `modernc.org/sqlite` version to 1.36.1 and includes a couple small improvements. [1.4.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.4.1 ### Changed - The minimum `modernc.org/sqlite` version updated to 1.36.1. ### Fixed - `*Stmt.ColumnName` no longer performs an allocation ([#101](https://github.com/zombiezen/go-sqlite/issues/118)). - The doc comment for `OpenFlags` has been rewritten for clarity ([#114](https://github.com/zombiezen/go-sqlite/pull/114)). ## [1.4.0][] - 2024-09-23 Version 1.4 adds the `sqlitex.ResultBytes` function and fixes several bugs. [1.4.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.4.0 ### Added - New function `sqlitex.ResultBytes`. ([#86](https://github.com/zombiezen/go-sqlite/pull/86)) ### Changed - `Conn.Close` returns an error if the connection has already been closed ([#101](https://github.com/zombiezen/go-sqlite/issues/101)). - The minimum `modernc.org/sqlite` version updated to 1.33.1. ### Fixed - `sqlite3_initialize` is now called from any top-level function to prevent race conditions during initialization. ([#18](https://github.com/zombiezen/go-sqlite/issues/18)). ## [1.3.0][] - 2024-05-04 Version 1.3 is largely a bug-fix release, but is a minor version change because of the new `sqlitemigration.Pool.Take` method. [1.3.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.3.0 ### Added - `sqlitemigration.Pool` now has a new method `Take` so that it implements a common interface with `sqlitex.Pool` ([#97](https://github.com/zombiezen/go-sqlite/pull/97)). - Documented `OpenWAL` behavior on `sqlite.OpenConn`. ### Fixed - Address low-frequency errors with concurrent use of `sqlitemigration` ([#99](https://github.com/zombiezen/go-sqlite/issues/99)). - The error returned from `sqlitex.NewPool` when trying to open an in-memory database now gives correct advice ([#92](https://github.com/zombiezen/go-sqlite/issues/92)). ## [1.2.0][] - 2024-03-27 Version 1.2.0 adds a `sqlitex.Pool.Take` method and improves error messages. [1.2.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.2.0 ### Added - `sqlitex.Pool` has a new method `Take` which returns an `error` along with a `Conn` ([#83](https://github.com/zombiezen/go-sqlite/issues/83)). - `sqlite.ErrorOffset` is a new function that returns the SQL byte offset that an error references. ### Changed - `sqlite.Conn.Prep`, `sqlite.Conn.Prepare`, and `sqlite.Conn.PrepareTransient` now include position information in error messages if available. - Many error messages around statement execution changed their format for better readability. Error messages are not stable API and should not be depended on. ### Deprecated - The `sqlitex.Pool.Get` method has been deprecated in favor of the new `Take` method. ### Fixed - Error messages no longer duplicate information from their error code (reported in [#84](https://github.com/zombiezen/go-sqlite/issues/84)). ## [1.1.2][] - 2024-02-14 Version 1.1.2 updates the `modernc.org/sqlite` version to 1.29.1 and makes further tweaks to busy-polling. [1.1.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.1.2 ### Changed - Set the maximum time between busy polls to 100 milliseconds (follow-on from [#75](https://github.com/zombiezen/go-sqlite/issues/75)). - The minimum `modernc.org/sqlite` version updated to 1.29.1 ([#77](https://github.com/zombiezen/go-sqlite/issues/77)). ## [1.1.1][] - 2024-02-02 Version 1.1.1 improves performance on write-contended workloads. [1.1.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.1.1 ### Fixed - Make busy-blocking more responsive ([#75](https://github.com/zombiezen/go-sqlite/issues/75)). ## [1.1.0][] - 2024-01-14 Version 1.1 introduces the ability to prepare connections on `sqlitex.Pool`, improves performance, and improves documentation. [1.1.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.1.0 ### Added - Added a `sqlitex.NewPool` function with support for a `ConnPrepareFunc` ([#65](https://github.com/zombiezen/go-sqlite/issues/65)). - Added a documentation example for `SetCollation` ([#64](https://github.com/zombiezen/go-sqlite/issues/64)). ### Deprecated - Deprecated `sqlitex.Open` in favor of `sqlitex.NewPool`. ### Fixed - Speed up internal string conversions ([#66](https://github.com/zombiezen/go-sqlite/pull/66)). Thank you [@ffmiruz](https://github.com/ffmiruz) for the profiling work! ## [1.0.0][] - 2023-12-07 Version 1.0 is the first officially stable release of `zombiezen.com/go/sqlite`. It includes improved documentation and is cleaned up for current versions of Go. There are no breaking changes to the API: this release is more a recognition that the API has been stable and a promise that it will continue to be stable. [1.0.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v1.0.0 ### Added - Added `*Stmt.ColumnIsNull` and `*Stmt.IsNull` methods ([#55](https://github.com/zombiezen/go-sqlite/issues/55)). - Added more documentation to `sqlitefile` and `sqlitex`. ### Changed - Replaced `interface{}` with `any`. This should be a compatible change. - The minimum supported Go version for this library is now Go 1.20. - The minimum `modernc.org/sqlite` version updated to 1.27.0. ### Removed - Removed the `io.*` interface fields on `sqlitefile.Buffer` and `sqlitefile.File`. These were unused. - Removed the `zombiezen.com/go/sqlite/fs` package. It existed to help transition around Go 1.16, but is no longer useful. ## [0.13.1][] - 2023-08-15 Version 0.13.1 fixed a bug with the `sqlitemigration` package. [0.13.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.13.1 ### Fixed - `sqlitemigration` will no longer disable foreign keys during operation ([#54](https://github.com/zombiezen/go-sqlite/issues/54)). ## [0.13.0][] - 2023-03-28 Version 0.13 added support for user-defined [collating sequences](https://www.sqlite.org/datatype3.html#collation) and user-defined [virtual tables](https://sqlite.org/vtab.html). [0.13.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.13.0 ### Added - Support user-defined collating sequences ([#21](https://github.com/zombiezen/go-sqlite/issues/21)). - Support user-defined virtual tables ([#15](https://github.com/zombiezen/go-sqlite/issues/15)). - New package `ext/generateseries` provides an optional `generate_series` table-valued function extension. - Exported the `regexp` function example as a new `ext/refunc` package. - Add `*Conn.Serialize` and `*Conn.Deserialize` methods ([#52](https://github.com/zombiezen/go-sqlite/issues/52)). ### Changed - The minimum supported Go version for this library is now Go 1.19. ### Fixed - The documentation for `AggregateFunction.WindowValue` incorrectly stated that it would not be called in non-window contexts. The sentence has been removed, but the behavior has not changed. ## [0.12.0][] - 2023-02-08 Version 0.12 added support for the [online backup API](https://www.sqlite.org/backup.html). [0.12.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.12.0 ### Added - Added support for the online backup API ([#47](https://github.com/zombiezen/go-sqlite/issues/47)). - Documented the `OpenFlags`. ### Changed - `OpenNoMutex` and `OpenFullMutex` no longer have an effect on `sqlite.OpenConn`. `OpenNoMutex` (i.e. [multi-thread mode](https://www.sqlite.org/threadsafe.html)) is now the only supported mode. `*sqlite.Conn` has never been safe to use concurrently from multiple goroutines, so this is mostly to prevent unnecessary locking and to avoid confusion. ([#32](https://github.com/zombiezen/go-sqlite/issues/32)). ## [0.11.0][] - 2022-12-11 Version 0.11 changes the aggregate function API. [0.11.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.11.0 ### Changed - User-defined aggregate functions are now encapsulated with a new interface, `AggregateFunction`. The previous 4-callback approach has been removed and replaced with a single `MakeAggregate` callback. Not only was the previous API unwieldy, but it failed to handle concurrent aggregate function calls in a single query. - Minimum `modernc.org/sqlite` version updated to 1.20.0. ## [0.10.1][] - 2022-07-17 Version 0.10.1 fixes a bug in user-defined window functions. Special thanks to Jan Mercl for assistance in debugging this issue. [0.10.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.10.1 ### Fixed - `AggregateFinal` is now called correctly at the end of window functions' usages. ## [0.10.0][] - 2022-07-10 Version 0.10 adds support for user-defined window functions. [0.10.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.10.0 ### Added - `FunctionImpl` has two new fields (`WindowValue` and `WindowInverse`) that allow creating [user-defined aggregate window functions][] ([#42](https://github.com/zombiezen/go-sqlite/issues/42)). [user-defined aggregate window functions]: https://www.sqlite.org/windowfunctions.html#user_defined_aggregate_window_functions ### Changed - The `AggregateStep` callback now returns an `error`. ## [0.9.3][] - 2022-05-30 Version 0.9.3 updates the version of `modernc.org/sqlite` used. [0.9.3]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.9.3 ### Changed - Minimum `modernc.org/sqlite` version updated to v1.17.3. ## [0.9.2][] - 2022-01-25 Version 0.9 adds new `Execute` functions to `sqlitex` and changes the default blocking behavior. Version 0.9 also includes various fixes to the schema migration behavior. [0.9.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.9.2 ### Added - Added `SetBlockOnBusy` method to set an indefinite timeout on acquiring a lock. - Official support for `windows/amd64`. - `sqlitex` has three new functions — `Execute`, `ExecuteTransient`, and `ExecuteScript` — that take in an `ExecOptions` struct. ([#5](https://github.com/zombiezen/go-sqlite/issues/5)) - New method `sqlite.ResultCode.ToError` to create error values. - New methods `ColumnBool` and `GetBool` on `*sqlite.Stmt` ([#37](https://github.com/zombiezen/go-sqlite/issues/37)). ### Changed - `OpenConn` calls `SetBlockOnBusy` on new connections instead of `SetBusyTimeout(10 * time.Second)`. - The `sqlitex.Execute*` family of functions now verify that the arguments passed match the SQL parameters. ([#31](https://github.com/zombiezen/go-sqlite/issues/31)) ### Deprecated - `sqlitex.ExecFS` has been renamed to `sqlitex.ExecuteFS`, `sqlitex.ExecTransientFS` has been renamed to `sqlitex.ExecuteTransientFS`, and `sqlitex.ExecScriptFS` has been renamed to `sqlitex.ExecuteScriptFS` for consistency with the new `Execute` functions. Aliases remain in this version, but will be removed in the next version. Use `zombiezen-sqlite-migrate` to clean up existing references. - `sqlitex.Exec` and `sqlitex.ExecTransient` have been marked deprecated because they do not perform the argument checks that the `Execute` functions now perform. These functions will remain into 1.0 and beyond for compatibility, but should not be used in new applications. ### Fixed - `sqlitemigration.Schema.RepeatableMigration` is now run as part of the final transaction. This ensures that the repeatable migration for migration `N` has executed if and only if `user_version == N`. Previously, the repeatable migration could fail independently of the final transaction, which would mean that a subsequent migration run would not trigger a retry of the repeatable transaction, but report success. - `sqlitemigration` will no longer skip applying the repeatable migration if the final migration is empty. - `OpenConn` now sets a busy handler before enabling WAL (thanks @anacrolix!). ## 0.9.0 and 0.9.1 Versions 0.9.0 was accidentally released before CI ran. A change in the underlying `modernc.org/libc` library caused the memory leak detection to identify a false positive. In an abundance of caution, 0.9.1 was released to mark both 0.9.1 and 0.9.0 as retracted. Version 0.9.2 is the first official release of 0.9. ## [0.8.0][] - 2021-11-07 Version 0.8 adds new transaction functions to `sqlitex`. [0.8.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.8.0 ### Added - Added `sqlitex.Transaction`, `sqlitex.ImmediateTransaction`, and `sqlitex.ExclusiveTransaction`. ## [0.7.2][] - 2021-09-11 [0.7.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.7.2 ### Fixed - Updated `modernc.org/sqlite` dependency to a released version instead of a prerelease ## [0.7.1][] - 2021-09-09 [0.7.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.7.1 ### Added - Added an example to `sqlitemigration.Schema` ## [0.7.0][] - 2021-08-27 [0.7.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.7.0 ### Added - `sqlitemigration.Schema` has a new option for disabling foreign keys for individual migrations. This makes it easier to perform migrations that require [reconstructing a table][]. ([#20](https://github.com/zombiezen/go-sqlite/issues/20)) [reconstructing a table]: https://sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes ### Changed - `sqlitemigration.Migrate` and `*sqlitemigration.Pool` no longer use a transaction to apply the entire set of migrations: they now only use transactions during each individual migration. This was never documented, so in theory no one should be depending on this behavior. However, this does mean that two processes trying to open and migrate a database concurrently may race to apply migrations, whereas before only one process would acquire the write lock and migrate. ### Fixed - Fixed compile breakage on 32-bit architectures. Thanks to Jan Mercl for the report. ## [0.6.2][] - 2021-08-17 [0.6.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.6.2 ### Changed - `*sqlitex.Pool.Put` now accepts `nil` instead of panicing. ([#17](https://github.com/zombiezen/go-sqlite/issues/17)) ## [0.6.1][] - 2021-08-16 [0.6.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.6.1 ### Fixed - Fixed a potential memory corruption issue introduced in 0.6.0. Thanks to Jan Mercl for the report. ## [0.6.0][] - 2021-08-15 [0.6.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.6.0 ### Added - Added back the session API: `Session`, `ChangesetIterator`, `Changegroup`, and various functions. There are some slight naming changes from the `crawshaw.io/sqlite` API, but they can all be migrated automatically with the migration tool. ([#16](https://github.com/zombiezen/go-sqlite/issues/16)) ### Changed - Method calls to a `nil` `*sqlite.Conn` will return an error rather than panic. ([#17](https://github.com/zombiezen/go-sqlite/issues/17)) ### Removed - Removed `OpenFlags` that are only used for VFS. ### Fixed - Properly clean up WAL when using `sqlitex.Pool` ([#14](https://github.com/zombiezen/go-sqlite/issues/14)) - Disabled double-quoted string literals. ## [0.5.0][] - 2021-05-22 [0.5.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.5.0 ### Added - Added `shell` package with basic [REPL][] - Added `SetAuthorizer`, `Limit`, and `SetDefensive` methods to `*Conn` for use in ([#12](https://github.com/zombiezen/go-sqlite/issues/12)) - Added `Version` and `VersionNumber` constants [REPL]: https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop ### Fixed - Documented compiled-in extensions ([#11](https://github.com/zombiezen/go-sqlite/issues/11)) - Internal objects are no longer susceptible to ID wraparound issues ([#13](https://github.com/zombiezen/go-sqlite/issues/13)) ## [0.4.0][] - 2021-05-13 [0.4.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.4.0 ### Added - Add Context.Conn method ([#10](https://github.com/zombiezen/go-sqlite/issues/10)) - Add methods to get and set auxiliary function data ([#3](https://github.com/zombiezen/go-sqlite/issues/3)) ## [0.3.1][] - 2021-05-03 [0.3.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.3.1 ### Fixed - Fix conversion of BLOB to TEXT when returning BLOB from a user-defined function ## [0.3.0][] - 2021-04-27 [0.3.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.3.0 ### Added - Implement `io.StringWriter`, `io.ReaderFrom`, and `io.WriterTo` on `Blob` ([#2](https://github.com/zombiezen/go-sqlite/issues/2)) - Add godoc examples for `Blob`, `sqlitemigration`, and `SetInterrupt` - Add more README documentation ## [0.2.2][] - 2021-04-24 [0.2.2]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.2.2 ### Changed - Simplified license to [ISC](https://github.com/zombiezen/go-sqlite/blob/v0.2.2/LICENSE) ### Fixed - Updated version of `modernc.org/sqlite` to 1.10.4 to use [mutex initialization](https://gitlab.com/cznic/sqlite/-/issues/52) - Fixed doc comment for `BindZeroBlob` ## [0.2.1][] - 2021-04-17 [0.2.1]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.2.1 ### Fixed - Removed bogus import comment ## [0.2.0][] - 2021-04-03 [0.2.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.2.0 ### Added - New migration tool. See [the README](https://github.com/zombiezen/go-sqlite/blob/v0.2.0/cmd/zombiezen-sqlite-migrate/README.md) to get started. ([#1](https://github.com/zombiezen/go-sqlite/issues/1)) ### Changed - `*Conn.CreateFunction` has changed entirely. See the [reference](https://pkg.go.dev/zombiezen.com/go/sqlite#Conn.CreateFunction) for details. - `sqlitex.File` and `sqlitex.Buffer` have been moved to the `sqlitefile` package - The `sqlitefile.Exec*` functions have been moved to the `sqlitex` package as `Exec*FS`. ## [0.1.0][] - 2021-03-31 Initial release [0.1.0]: https://github.com/zombiezen/go-sqlite/releases/tag/v0.1.0 zombiezen-go-sqlite-297af96/CODE_OF_CONDUCT.md000066400000000000000000000121451501417116500205720ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at roxy@zombiezen.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. zombiezen-go-sqlite-297af96/CONTRIBUTING.md000066400000000000000000000017121501417116500202220ustar00rootroot00000000000000# `zombiezen.com/go/sqlite` Contributor's Guide `zombiezen.com/go/sqlite` is [@zombiezen's][zombiezen] labor of love in providing a pleasant, performant, and powerful Go binding to SQLite. The design philosophy of `zombiezen.com/go/sqlite` is to be a direct binding to SQLite as much as possible while adapting the API to use Go idioms. @zombiezen is open to accepting bug fixes, but generally not larger changes or features. If in doubt, ask them. And remember, [a "no" is temporary, a "yes" is forever.](https://opensource.guide/best-practices/) File a bug on the [issue tracker][] or [open a pull request][]. Please follow the [Code of Conduct][] for all interactions. If applicable, add unit tests for your change before sending out for review. [Code of Conduct]: CODE_OF_CONDUCT.md [issue tracker]: https://github.com/zombiezen/go-sqlite/issues/new [open a pull request]: https://github.com/zombiezen/go-sqlite/compare [zombiezen]: https://github.com/zombiezen zombiezen-go-sqlite-297af96/LICENSE000066400000000000000000000014371501417116500170020ustar00rootroot00000000000000Copyright (c) 2018 David Crawshaw Copyright (c) 2021 Roxy Light Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. zombiezen-go-sqlite-297af96/README.md000066400000000000000000000060071501417116500172520ustar00rootroot00000000000000# `zombiezen.com/go/sqlite` [![Go Reference](https://pkg.go.dev/badge/zombiezen.com/go/sqlite.svg)][reference docs] This package provides a low-level Go interface to [SQLite 3][]. It is a fork of [`crawshaw.io/sqlite`][] that uses [`modernc.org/sqlite`][], a CGo-free SQLite package. It aims to be a mostly drop-in replacement for `crawshaw.io/sqlite`. This package deliberately does not provide a `database/sql` driver. See [David Crawshaw's rationale][] for an in-depth explanation. If you want to use `database/sql` with SQLite without CGo, use `modernc.org/sqlite` directly. [`crawshaw.io/sqlite`]: https://github.com/crawshaw/sqlite [David Crawshaw's rationale]: https://crawshaw.io/blog/go-and-sqlite [`modernc.org/sqlite`]: https://pkg.go.dev/modernc.org/sqlite [reference docs]: https://pkg.go.dev/zombiezen.com/go/sqlite [SQLite 3]: https://sqlite.org/ ## Features - Full SQLite functionality via `modernc.org/sqlite`, an automatically generated translation of the original C source code of SQLite into Go - Builds with `CGO_ENABLED=0`, allowing cross-compiling and data race detection - Allows access to SQLite-specific features like [blob I/O][] and [user-defined functions][] - Includes a simple [schema migration package][] - Utilities for [running embedded SQL scripts][ExecScriptFS] using the [Go 1.16 embedding feature][] - A [`go fix`-like tool][migration docs] for migrating existing code using `crawshaw.io/sqlite` - A [simple REPL][] for debugging [blob I/O]: https://pkg.go.dev/zombiezen.com/go/sqlite#Blob [ExecScriptFS]: https://pkg.go.dev/zombiezen.com/go/sqlite/sqlitex#ExecScriptFS [Go 1.16 embedding feature]: https://pkg.go.dev/embed [migration docs]: cmd/zombiezen-sqlite-migrate/README.md [schema migration package]: https://pkg.go.dev/zombiezen.com/go/sqlite/sqlitemigration [simple REPL]: https://pkg.go.dev/zombiezen.com/go/sqlite/shell [user-defined functions]: https://pkg.go.dev/zombiezen.com/go/sqlite#Conn.CreateFunction ## Install ```shell go get zombiezen.com/go/sqlite ``` While this library does not use CGo, make sure that you are building for one of the [supported architectures][]. [supported architectures]: https://pkg.go.dev/modernc.org/sqlite#hdr-Supported_platforms_and_architectures ## Getting Started ```go import ( "fmt" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // ... // Open an in-memory database. conn, err := sqlite.OpenConn(":memory:", sqlite.OpenReadWrite) if err != nil { return err } defer conn.Close() // Execute a query. err = sqlitex.ExecuteTransient(conn, "SELECT 'hello, world';", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Println(stmt.ColumnText(0)) return nil }, }) if err != nil { return err } ``` If you're creating a new application, see the [package examples][] or the [reference docs][]. If you're looking to switch existing code that uses `crawshaw.io/sqlite`, take a look at the [migration docs][]. [package examples]: https://pkg.go.dev/zombiezen.com/go/sqlite#pkg-examples ## License [ISC](LICENSE) zombiezen-go-sqlite-297af96/auth.go000066400000000000000000000204271501417116500172650ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "strings" "sync" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) // An Authorizer is called during statement preparation to see whether an action // is allowed by the application. An Authorizer must not modify the database // connection, including by preparing statements. // // See https://sqlite.org/c3ref/set_authorizer.html for a longer explanation. type Authorizer interface { Authorize(Action) AuthResult } // SetAuthorizer registers an authorizer for the database connection. // SetAuthorizer(nil) clears any authorizer previously set. func (c *Conn) SetAuthorizer(auth Authorizer) error { if c == nil { return fmt.Errorf("sqlite: set authorizer: nil connection") } if auth == nil { c.releaseAuthorizer() res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, 0, 0)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: set authorizer: %w", err) } return nil } authorizers.mu.Lock() if authorizers.m == nil { authorizers.m = make(map[uintptr]Authorizer) } authorizers.m[c.conn] = auth authorizers.mu.Unlock() xAuth := cFuncPointer(authTrampoline) res := ResultCode(lib.Xsqlite3_set_authorizer(c.tls, c.conn, xAuth, c.conn)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: set authorizer: %w", err) } return nil } func (c *Conn) releaseAuthorizer() { authorizers.mu.Lock() delete(authorizers.m, c.conn) authorizers.mu.Unlock() } var authorizers struct { mu sync.RWMutex m map[uintptr]Authorizer // sqlite3* -> Authorizer } func authTrampoline(tls *libc.TLS, conn uintptr, op int32, cArg1, cArg2, cDB, cTrigger uintptr) int32 { authorizers.mu.RLock() auth := authorizers.m[conn] authorizers.mu.RUnlock() return int32(auth.Authorize(Action{ op: OpType(op), arg1: libc.GoString(cArg1), arg2: libc.GoString(cArg2), database: libc.GoString(cDB), trigger: libc.GoString(cTrigger), })) } // AuthorizeFunc is a function that implements Authorizer. type AuthorizeFunc func(Action) AuthResult // Authorize calls f. func (f AuthorizeFunc) Authorize(action Action) AuthResult { return f(action) } // AuthResult is the result of a call to an Authorizer. The zero value is // AuthResultOK. type AuthResult int32 // Possible return values from Authorize. const ( // AuthResultOK allows the SQL statement to be compiled. AuthResultOK AuthResult = lib.SQLITE_OK // AuthResultDeny causes the entire SQL statement to be rejected with an error. AuthResultDeny AuthResult = lib.SQLITE_DENY // AuthResultIgnore disallows the specific action but allow the SQL statement // to continue to be compiled. For OpRead, this substitutes a NULL for the // column value. For OpDelete, the DELETE operation proceeds but the truncate // optimization is disabled and all rows are deleted individually. AuthResultIgnore AuthResult = lib.SQLITE_IGNORE ) // String returns the C constant name of the result. func (result AuthResult) String() string { switch result { case AuthResultOK: return "SQLITE_OK" case AuthResultDeny: return "SQLITE_DENY" case AuthResultIgnore: return "SQLITE_IGNORE" default: return fmt.Sprintf("AuthResult(%d)", int32(result)) } } // Action represents an action to be authorized. type Action struct { op OpType arg1 string arg2 string database string trigger string } // Mapping of argument position to concept at: // https://sqlite.org/c3ref/c_alter_table.html // Type returns the type of action being authorized. func (action Action) Type() OpType { return action.op } // Accessor returns the name of the inner-most trigger or view that is // responsible for the access attempt or the empty string if this access attempt // is directly from top-level SQL code. func (action Action) Accessor() string { return action.trigger } // Database returns the name of the database (e.g. "main", "temp", etc.) this // action affects or the empty string if not applicable. func (action Action) Database() string { switch action.op { case OpDetach, OpAlterTable: return action.arg1 default: return action.database } } // Index returns the name of the index this action affects or the empty string // if not applicable. func (action Action) Index() string { switch action.op { case OpCreateIndex, OpCreateTempIndex, OpDropIndex, OpDropTempIndex, OpReindex: return action.arg1 default: return "" } } // Table returns the name of the table this action affects or the empty string // if not applicable. func (action Action) Table() string { switch action.op { case OpCreateTable, OpCreateTempTable, OpDelete, OpDropTable, OpDropTempTable, OpInsert, OpRead, OpUpdate, OpAnalyze, OpCreateVTable, OpDropVTable: return action.arg1 case OpCreateIndex, OpCreateTempIndex, OpCreateTempTrigger, OpCreateTrigger, OpDropIndex, OpDropTempIndex, OpDropTempTrigger, OpDropTrigger, OpAlterTable: return action.arg2 default: return "" } } // Trigger returns the name of the trigger this action affects or the empty // string if not applicable. func (action Action) Trigger() string { switch action.op { case OpCreateTempTrigger, OpCreateTrigger, OpDropTempTrigger, OpDropTrigger: return action.arg1 default: return "" } } // View returns the name of the view this action affects or the empty string // if not applicable. func (action Action) View() string { switch action.op { case OpCreateTempView, OpCreateView, OpDropTempView, OpDropView: return action.arg1 default: return "" } } // Pragma returns the name of the action's PRAGMA command or the empty string // if the action does not represent a PRAGMA command. // See https://sqlite.org/pragma.html#toc for a list of possible values. func (action Action) Pragma() string { if action.op != OpPragma { return "" } return action.arg1 } // PragmaArg returns the argument to the PRAGMA command or the empty string if // the action does not represent a PRAGMA command or the PRAGMA command does not // take an argument. func (action Action) PragmaArg() string { if action.op != OpPragma { return "" } return action.arg2 } // Column returns the name of the column this action affects or the empty string // if not applicable. For OpRead actions, this will return the empty string if a // table is referenced but no column values are extracted from that table // (e.g. a query like "SELECT COUNT(*) FROM tab"). func (action Action) Column() string { switch action.op { case OpRead, OpUpdate: return action.arg2 default: return "" } } // Operation returns one of "BEGIN", "COMMIT", "RELEASE", or "ROLLBACK" for a // transaction or savepoint statement or the empty string otherwise. func (action Action) Operation() string { switch action.op { case OpTransaction, OpSavepoint: return action.arg1 default: return "" } } // File returns the name of the file being ATTACHed or the empty string if the // action does not represent an ATTACH DATABASE statement. func (action Action) File() string { if action.op != OpAttach { return "" } return action.arg1 } // Module returns the module name given to the virtual table statement or the // empty string if the action does not represent a CREATE VIRTUAL TABLE or // DROP VIRTUAL TABLE statement. func (action Action) Module() string { switch action.op { case OpCreateVTable, OpDropVTable: return action.arg2 default: return "" } } // Savepoint returns the name given to the SAVEPOINT statement or the empty // string if the action does not represent a SAVEPOINT statement. func (action Action) Savepoint() string { if action.op != OpSavepoint { return "" } return action.arg2 } // String returns a debugging representation of the action. func (action Action) String() string { sb := new(strings.Builder) sb.WriteString(action.op.String()) params := []struct { name, value string }{ {"database", action.Database()}, {"file", action.File()}, {"trigger", action.Trigger()}, {"index", action.Index()}, {"table", action.Table()}, {"view", action.View()}, {"module", action.Module()}, {"column", action.Column()}, {"operation", action.Operation()}, {"savepoint", action.Savepoint()}, {"pragma", action.Pragma()}, {"arg", action.PragmaArg()}, } for _, p := range params { if p.value != "" { sb.WriteString(" ") sb.WriteString(p.name) sb.WriteString(":") sb.WriteString(p.value) } } return sb.String() } zombiezen-go-sqlite-297af96/auth_test.go000066400000000000000000000024431501417116500203220ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite_test import ( "testing" "zombiezen.com/go/sqlite" ) func TestSetAuthorizer(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() authResult := sqlite.AuthResult(0) var lastAction sqlite.Action auth := sqlite.AuthorizeFunc(func(action sqlite.Action) sqlite.AuthResult { lastAction = action return authResult }) c.SetAuthorizer(auth) t.Run("Allowed", func(t *testing.T) { authResult = sqlite.AuthResultOK stmt, _, err := c.PrepareTransient("SELECT 1;") if err != nil { t.Fatal(err) } stmt.Finalize() if lastAction.Type() != sqlite.OpSelect { t.Errorf("action = %v; want %v", lastAction, sqlite.OpSelect) } }) t.Run("Denied", func(t *testing.T) { authResult = sqlite.AuthResultDeny stmt, _, err := c.PrepareTransient("SELECT 1;") if err == nil { stmt.Finalize() t.Fatal("PrepareTransient did not return an error") } if got, want := sqlite.ErrCode(err), sqlite.ResultAuth; got != want { t.Errorf("sqlite.ErrCode(err) = %v; want %v", got, want) } if lastAction.Type() != sqlite.OpSelect { t.Errorf("action = %v; want %v", lastAction, sqlite.OpSelect) } }) } zombiezen-go-sqlite-297af96/backup.go000066400000000000000000000074231501417116500175720ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) // A Backup represents a copy operation between two database connections. // See https://www.sqlite.org/backup.html for more details. type Backup struct { tls *libc.TLS ptr uintptr } // NewBackup creates a [Backup] that copies from one connection to another. // The database name is "main" or "" for the main database, // "temp" for the temporary database, // or the name specified after the AS keyword in an ATTACH statement for an attached database. // If src and dst are the same connection, NewBackup will return an error. // It is the caller's responsibility to call [Backup.Close] on the returned Backup object. func NewBackup(dst *Conn, dstName string, src *Conn, srcName string) (*Backup, error) { cDstName, freeCDstName, err := cDBName(dstName) if err != nil { return nil, fmt.Errorf("sqlite: new backup: %w", err) } defer freeCDstName() cSrcName, freeCSrcName, err := cDBName(srcName) if err != nil { return nil, fmt.Errorf("sqlite: new backup: %w", err) } defer freeCSrcName() backupPtr := lib.Xsqlite3_backup_init(dst.tls, dst.conn, cDstName, src.conn, cSrcName) if backupPtr == 0 { res := ResultCode(lib.Xsqlite3_errcode(dst.tls, dst.conn)) return nil, fmt.Errorf("sqlite: new backup: %w", dst.extreserr(res)) } return &Backup{dst.tls, backupPtr}, nil } // Step copies up to n pages from the source database to the destination database. // If n is negative, all remaining source pages are copied. // more will be true if there are pages still remaining to be copied. // Step may return both an error and that more pages are still remaining: // this indicates the error is temporary and that Step can be retried. func (b *Backup) Step(n int) (more bool, err error) { res := ResultCode(lib.Xsqlite3_backup_step(b.tls, b.ptr, int32(n))) switch res { case ResultOK: return true, nil case ResultDone: return false, nil case ResultBusy, ResultLocked: // SQLITE_BUSY and SQLITE_LOCKED are retriable errors. return true, fmt.Errorf("sqlite: backup step: %w", res.ToError()) default: return false, fmt.Errorf("sqlite: backup step: %w", res.ToError()) } } // Remaining returns the number of pages still to be backed up // at the conclusion of the most recent call to [Backup.Step]. // The return value of Remaining before calling [Backup.Step] is undefined. // If the source database is modified in a way that changes the number of pages remaining, // that change is not reflected in the output until after the next call to [Backup.Step]. func (b *Backup) Remaining() int { return int(lib.Xsqlite3_backup_remaining(b.tls, b.ptr)) } // PageCount returns the total number of pages in the source database // at the conclusion of the most recent call to [Backup.Step]. // The return value of PageCount before calling [Backup.Step] is undefined. // If the source database is modified in a way that changes the size of the source database, // that change is not reflected in the output until after the next call to [Backup.Step]. func (b *Backup) PageCount() int { return int(lib.Xsqlite3_backup_pagecount(b.tls, b.ptr)) } // Close releases all resources associated with the backup. // If [Backup.Step] has not yet returned (false, nil), // then any active write transaction on the destination database is rolled back. func (b *Backup) Close() error { // The error from sqlite3_backup_finish indicates whether // a previous call to sqlite3_backup_step returned an error. // Since we're assuming that the caller will handle errors as they arise, // we always return nil from Close. // However, I'm not fully confident that Close will always be infallible, // so I'm not documenting this as part of the API. lib.Xsqlite3_backup_finish(b.tls, b.ptr) return nil } zombiezen-go-sqlite-297af96/backup_example_test.go000066400000000000000000000035661501417116500223500ustar00rootroot00000000000000package sqlite_test import ( "fmt" "os" "time" "zombiezen.com/go/sqlite" ) // This example shows the basic use of a backup object. func ExampleBackup() { // Open database connections. src, err := sqlite.OpenConn(os.Args[1]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer src.Close() dst, err := sqlite.OpenConn(os.Args[2]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer func() { if err := dst.Close(); err != nil { fmt.Fprintln(os.Stderr, err) } }() // Create Backup object. backup, err := sqlite.NewBackup(dst, "main", src, "main") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } // Perform online backup/copy. _, err1 := backup.Step(-1) err2 := backup.Close() if err1 != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if err2 != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // This example shows how to use Step // to prevent holding a read lock on the source database // during the entire copy. func ExampleBackup_Step() { // Open database connections. src, err := sqlite.OpenConn(os.Args[1]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer src.Close() dst, err := sqlite.OpenConn(os.Args[2]) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer func() { if err := dst.Close(); err != nil { fmt.Fprintln(os.Stderr, err) } }() // Create Backup object. backup, err := sqlite.NewBackup(dst, "main", src, "main") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer func() { if err := backup.Close(); err != nil { fmt.Fprintln(os.Stderr, err) } }() // Each iteration of this loop copies 5 database pages, // waiting 250ms between iterations. for { more, err := backup.Step(5) if !more { if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } break } time.Sleep(250 * time.Millisecond) } } zombiezen-go-sqlite-297af96/backup_test.go000066400000000000000000000035301501417116500206240ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package sqlite_test import ( "testing" "github.com/google/go-cmp/cmp" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func TestBackup(t *testing.T) { for _, isFull := range []bool{false, true} { name := "Full" if !isFull { name = "Incremental" } t.Run(name, func(t *testing.T) { src, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer src.Close() err = sqlitex.ExecuteTransient(src, `CREATE TABLE foo (x INTEGER PRIMARY KEY NOT NULL);`, nil) if err != nil { t.Fatal(err) } err = sqlitex.ExecuteTransient(src, `INSERT INTO foo VALUES (1), (2), (3), (42);`, nil) if err != nil { t.Fatal(err) } dst, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer dst.Close() backup, err := sqlite.NewBackup(dst, "", src, "") if err != nil { t.Fatal(err) } defer func() { if err := backup.Close(); err != nil { t.Error(err) } }() if isFull { more, err := backup.Step(-1) if more || err != nil { t.Errorf("backup.Step(-1) = %t, %v; want false, ", more, err) } } else { for { more, err := backup.Step(1) t.Logf("after Step: Remaining() = %d, PageCount() = %d", backup.Remaining(), backup.PageCount()) if !more { if err != nil { t.Error("backup.Step(1):", err) } break } } } var got []int err = sqlitex.ExecuteTransient(dst, `SELECT x FROM foo ORDER BY x;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnInt(0)) return nil }, }) if err != nil { t.Error(err) } want := []int{1, 2, 3, 42} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("foo (-want +got):\n%s", diff) } }) } } zombiezen-go-sqlite-297af96/blob.go000066400000000000000000000215541501417116500172440ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "errors" "fmt" "io" "unsafe" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) const blobBufSize = 4096 var ( mainCString = mustCString("main") tempCString = mustCString("temp") ) // OpenBlob opens a blob in a particular {database,table,column,row}. // // https://www.sqlite.org/c3ref/blob_open.html func (c *Conn) OpenBlob(dbn, table, column string, row int64, write bool) (*Blob, error) { if c == nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: nil connection", table, column) } return c.openBlob(dbn, table, column, row, write) } func (c *Conn) openBlob(dbn, table, column string, row int64, write bool) (_ *Blob, err error) { cdb, freeCDB, err := cDBName(dbn) if err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } defer freeCDB() var writeFlag int32 if write { writeFlag = 1 } buf, err := malloc(c.tls, blobBufSize) if err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } defer func() { if err != nil { libc.Xfree(c.tls, buf) } }() ctable, err := libc.CString(table) if err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } defer libc.Xfree(c.tls, ctable) ccolumn, err := libc.CString(column) if err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } defer libc.Xfree(c.tls, ccolumn) blobPtrPtr, err := malloc(c.tls, ptrSize) if err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } defer libc.Xfree(c.tls, blobPtrPtr) for { if err := c.interrupted(); err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } res := ResultCode(lib.Xsqlite3_blob_open( c.tls, c.conn, cdb, ctable, ccolumn, row, writeFlag, blobPtrPtr, )) switch res { case ResultLockedSharedCache: if err := waitForUnlockNotify(c.tls, c.conn, c.unlockNote).ToError(); err != nil { return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, err) } // loop case ResultOK: blobPtr := *(*uintptr)(unsafe.Pointer(blobPtrPtr)) return &Blob{ conn: c, blob: blobPtr, buf: buf, size: lib.Xsqlite3_blob_bytes(c.tls, blobPtr), }, nil default: return nil, fmt.Errorf("sqlite: open blob %q.%q: %w", table, column, c.extreserr(res)) } } } // Blob provides streaming access to SQLite blobs. type Blob struct { conn *Conn blob uintptr buf uintptr off int32 size int32 } func (blob *Blob) bufSlice() []byte { return libc.GoBytes(blob.buf, blobBufSize) } // Read reads up to len(p) bytes from the blob into p. // https://www.sqlite.org/c3ref/blob_read.html func (blob *Blob) Read(p []byte) (int, error) { if blob.blob == 0 { return 0, fmt.Errorf("sqlite: read blob: %w", errInvalidBlob) } if blob.off >= blob.size { return 0, io.EOF } if err := blob.conn.interrupted(); err != nil { return 0, fmt.Errorf("sqlite: read blob: %w", err) } if rem := blob.size - blob.off; len(p) > int(rem) { p = p[:rem] } fullLen := len(p) for len(p) > 0 { nn := int32(blobBufSize) if int(nn) > len(p) { nn = int32(len(p)) } res := ResultCode(lib.Xsqlite3_blob_read(blob.conn.tls, blob.blob, blob.buf, nn, blob.off)) if err := res.ToError(); err != nil { return fullLen - len(p), fmt.Errorf("sqlite: read blob: %w", err) } copy(p, blob.bufSlice()[:int(nn)]) p = p[nn:] blob.off += nn } return fullLen, nil } // WriteTo copies the blob to w until there's no more data to write or // an error occurs. func (blob *Blob) WriteTo(w io.Writer) (n int64, err error) { if blob.blob == 0 { return 0, fmt.Errorf("sqlite: read blob: %w", errInvalidBlob) } if blob.off >= blob.size { return 0, nil } if err := blob.conn.interrupted(); err != nil { return 0, fmt.Errorf("sqlite: read blob: %w", err) } for blob.off < blob.size { buf := blob.bufSlice() if remaining := int(blob.size - blob.off); len(buf) > remaining { buf = buf[:remaining] } res := ResultCode(lib.Xsqlite3_blob_read(blob.conn.tls, blob.blob, blob.buf, int32(len(buf)), blob.off)) if err := res.ToError(); err != nil { return n, fmt.Errorf("sqlite: read blob: %w", err) } nn, err := w.Write(buf) blob.off += int32(nn) n += int64(nn) if err != nil { return n, err } } return n, nil } // Write writes len(p) from p to the blob. // https://www.sqlite.org/c3ref/blob_write.html func (blob *Blob) Write(p []byte) (int, error) { if blob.blob == 0 { return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) } if err := blob.conn.interrupted(); err != nil { return 0, fmt.Errorf("sqlite: write blob: %w", err) } fullLen := len(p) for len(p) > 0 { nn := copy(blob.bufSlice(), p) res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) if err := res.ToError(); err != nil { return fullLen - len(p), fmt.Errorf("sqlite: write blob: %w", err) } p = p[nn:] blob.off += int32(nn) } return fullLen, nil } // WriteString writes s to the blob. // https://www.sqlite.org/c3ref/blob_write.html func (blob *Blob) WriteString(s string) (int, error) { if blob.blob == 0 { return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) } if err := blob.conn.interrupted(); err != nil { return 0, fmt.Errorf("sqlite: write blob: %w", err) } fullLen := len(s) for len(s) > 0 { nn := copy(blob.bufSlice(), s) res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) if err := res.ToError(); err != nil { return fullLen - len(s), fmt.Errorf("sqlite: write blob: %w", err) } s = s[nn:] blob.off += int32(nn) } return fullLen, nil } // ReadFrom copies data from r to the blob until EOF or error. func (blob *Blob) ReadFrom(r io.Reader) (n int64, err error) { if blob.blob == 0 { return 0, fmt.Errorf("sqlite: write blob: %w", errInvalidBlob) } if err := blob.conn.interrupted(); err != nil { return 0, fmt.Errorf("sqlite: write blob: %w", err) } for { nn, err := r.Read(blob.bufSlice()) if nn > 0 { res := ResultCode(lib.Xsqlite3_blob_write(blob.conn.tls, blob.blob, blob.buf, int32(nn), blob.off)) if err := res.ToError(); err != nil { return n, fmt.Errorf("sqlite: write blob: %w", err) } n += int64(nn) blob.off += int32(nn) } if err != nil { if err == io.EOF { err = nil } return n, err } } } // Seek sets the offset for the next Read or Write and returns the offset. // Seeking past the end of the blob returns an error. func (blob *Blob) Seek(offset int64, whence int) (int64, error) { switch whence { case io.SeekStart: // use offset directly case io.SeekCurrent: offset += int64(blob.off) case io.SeekEnd: offset += int64(blob.size) default: return int64(blob.off), fmt.Errorf("sqlite: seek blob: invalid whence %d", whence) } if offset < 0 { return int64(blob.off), fmt.Errorf("sqlite: seek blob: negative offset %d", offset) } if offset > int64(blob.size) { return int64(blob.off), fmt.Errorf("sqlite: seek blob: offset %d is past size %d", offset, blob.size) } blob.off = int32(offset) return offset, nil } // Size returns the number of bytes in the blob. func (blob *Blob) Size() int64 { return int64(blob.size) } // Close releases any resources associated with the blob handle. // https://www.sqlite.org/c3ref/blob_close.html func (blob *Blob) Close() error { if blob.blob == 0 { return errInvalidBlob } libc.Xfree(blob.conn.tls, blob.buf) blob.buf = 0 res := ResultCode(lib.Xsqlite3_blob_close(blob.conn.tls, blob.blob)) blob.blob = 0 if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: close blob: %w", err) } return nil } var errInvalidBlob = errors.New("invalid blob") // cDBName converts a database name into a C string. func cDBName(dbn string) (uintptr, func(), error) { switch dbn { case "", "main": return mainCString, func() {}, nil case "temp": return tempCString, func() {}, nil default: cdb, err := libc.CString(dbn) if err != nil { return 0, nil, err } return cdb, func() { libc.Xfree(nil, cdb) }, nil } } // TODO: Blob Reopen zombiezen-go-sqlite-297af96/blob_test.go000066400000000000000000000307701501417116500203030ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite_test import ( "bytes" "compress/gzip" "io" "reflect" "strings" "sync" "testing" "time" "zombiezen.com/go/sqlite" ) var _ interface { io.Reader io.Writer io.Seeker io.Closer io.StringWriter io.ReaderFrom io.WriterTo } = (*sqlite.Blob)(nil) func TestBlob(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", 5) if _, err := stmt.Step(); err != nil { t.Fatal(err) } if err := stmt.Finalize(); err != nil { t.Error(err) } rowid := c.LastInsertRowID() t.Logf("blobs rowid: %d", rowid) b, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } if _, err := b.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } n, err := b.Write([]byte{1, 2, 3}) if err != nil { t.Fatal(err) } if n != 3 { t.Fatalf("b.Write n=%d, want 3", n) } if _, err := b.Seek(1, io.SeekStart); err != nil { t.Fatal(err) } n, err = b.WriteString("\x02\x03\x04\x05") if err != nil { t.Fatal(err) } if n != 4 { t.Fatalf("b.WriteString n=%d, want 4", n) } if size := b.Size(); size != 5 { t.Fatalf("b.Size=%d, want 5", size) } if _, err := b.Seek(1, io.SeekStart); err != nil { t.Fatal(err) } n, err = b.Write([]byte{2, 3, 4, 5, 6}) // too long if err == nil { t.Fatalf("Write too long, but no error") } if err := b.Close(); err != nil { t.Error(err) } b, err = c.OpenBlob("", "blobs", "col", rowid, false) if err != nil { t.Fatal(err) } defer b.Close() if _, err := b.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } got := make([]byte, 5) n, err = io.ReadFull(b, got) if n != len(got) { t.Fatalf("b.Read=%d, want len(got)=%d", n, len(got)) } want := []byte{1, 2, 3, 4, 5} if !reflect.DeepEqual(got, want) { t.Errorf("b.Read got %v, want %v", got, want) } if _, err := b.Seek(3, io.SeekStart); err != nil { t.Fatal(err) } n, err = io.ReadFull(b, got[3:]) if n != len(got)-3 { t.Fatalf("b.Read(got, 3)=%d, want len(got)-3=%d", n, len(got)-3) } if !reflect.DeepEqual(got, want) { t.Errorf("b.Read(got, 3) %v, want %v", got, want) } } func TestConcurrentBlobSpins(t *testing.T) { flags := sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI | sqlite.OpenSharedCache c, err := sqlite.OpenConn("file::memory:?mode=memory", flags) if err != nil { t.Fatal(err) } c2, err := sqlite.OpenConn("file::memory:?mode=memory", flags) if err != nil { t.Fatal(err) } defer c.Close() defer c2.Close() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", 1024) if _, err := stmt.Step(); err != nil { t.Fatal(err) } blobRow1 := c.LastInsertRowID() if err := stmt.Reset(); err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } blobRow2 := c.LastInsertRowID() if err := stmt.Finalize(); err != nil { t.Error(err) } blob1, err := c.OpenBlob("", "blobs", "col", blobRow1, true) if err != nil { t.Errorf("OpenBlob: %v", err) return } blob1Closed := make(chan struct{}) go func() { defer close(blob1Closed) time.Sleep(50 * time.Millisecond) blob1.Close() }() defer func() { <-blob1Closed }() // countBefore := sqlite.ConnCount(c2) blob2, err := c2.OpenBlob("", "blobs", "col", blobRow2, true) // countAfter := sqlite.ConnCount(c2) if err != nil { t.Fatalf("OpenBlob: %v", err) } blob2.Close() // TODO(maybe) // if spins := countAfter - countBefore - 1; spins > 1 { // t.Errorf("expecting no more than 1 spin, got %d", spins) // } } // TestConcurrentBlobWrites looks for unexpected SQLITE_LOCKED errors // when using the (default) shared cache. func TestConcurrentBlobWrites(t *testing.T) { flags := sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI | sqlite.OpenSharedCache const numBlobs = 5 c, err := sqlite.OpenConn("file::memory:?mode=memory", flags) if err != nil { t.Fatal(err) } defer c.Close() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", 1024) var blobRowIDs []int64 for i := 0; i < numBlobs; i++ { if _, err := stmt.Step(); err != nil { t.Fatal(err) } blobRowIDs = append(blobRowIDs, c.LastInsertRowID()) if err := stmt.Reset(); err != nil { t.Fatal(err) } } if err := stmt.Finalize(); err != nil { t.Error(err) } var wg sync.WaitGroup for i := 0; i < numBlobs; i++ { wg.Add(1) go func(i int) { defer wg.Done() b := make([]byte, 1024) b[0] = byte(i) c, err := sqlite.OpenConn("file::memory:?mode=memory", flags) if err != nil { t.Error(err) return } defer c.Close() blob, err := c.OpenBlob("", "blobs", "col", blobRowIDs[i], true) if err != nil { t.Errorf("OpenBlob: %v (i=%d)", err, i) return } defer blob.Close() for j := 0; j < 10; j++ { b[1] = byte(j) if _, err := blob.Seek(0, io.SeekStart); err != nil { t.Error(err) return } n, err := blob.Write(b) if err != nil { t.Errorf("Blob.Write: %v (i=%d, j=%d)", err, i, j) return } if n != len(b) { t.Errorf("n=%d, want %d (i=%d, j=%d)", n, len(b), i, j) return } } }(i) } wg.Wait() } func TestBlobClose(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", 5) if _, err := stmt.Step(); err != nil { t.Fatal(err) } rowid := c.LastInsertRowID() t.Logf("blobs rowid: %d", rowid) b, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } if err := b.Close(); err != nil { t.Fatal(err) } if err := b.Close(); err == nil { t.Error("no error on second close") } if _, err := b.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } if _, err := b.Write([]byte{1}); err == nil { t.Error("want error on write-after-close") } if _, err := b.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } if _, err = io.ReadFull(b, make([]byte, 1)); err == nil { t.Error("want error on read-after-close") } } func TestBlobReadWrite(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", 5) if _, err := stmt.Step(); err != nil { t.Fatal(err) } rowid := c.LastInsertRowID() t.Logf("blobs rowid: %d", rowid) b, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } defer b.Close() if _, err := b.Write([]byte{1, 2, 3}); err != nil { t.Fatal(err) } if _, err := b.Write([]byte{4, 5}); err != nil { t.Fatal(err) } if _, err := b.Write([]byte{6}); err == nil { t.Error("Write past end of blob: want error; got ") } if _, err := b.Seek(0, 0); err != nil { t.Fatal(err) } if got, err := io.ReadAll(io.Reader(b)); err != nil { t.Fatal(err) } else if want := []byte{1, 2, 3, 4, 5}; !reflect.DeepEqual(got, want) { t.Errorf("want %v, got %v", want, got) } } // See https://github.com/golang/go/issues/28606 func TestBlobPtrs(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } buf := new(bytes.Buffer) gzw := gzip.NewWriter(buf) gzw.Write([]byte("hello")) gzw.Close() n := buf.Len() stmt := c.Prep("INSERT INTO blobs (col) VALUES ($col);") stmt.SetZeroBlob("$col", int64(n)) if _, err := stmt.Step(); err != nil { t.Fatal(err) } rowid := c.LastInsertRowID() t.Logf("blobs rowid: %d", rowid) blob, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } defer blob.Close() gzw = gzip.NewWriter(blob) gzw.Write([]byte("hello")) gzw.Close() blob.Seek(0, 0) gzr, err := gzip.NewReader(blob) if err != nil { t.Fatal(err) } b, err := io.ReadAll(io.Reader(gzr)) if err != nil { t.Fatal(err) } if got := string(b); got != "hello" { t.Errorf("read %q, want %q", got, "hello") } if err := gzr.Close(); err != nil { t.Fatal(err) } } func TestBlobReadFrom(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } const data = "Hello, World!" stmt := c.Prep("INSERT INTO blobs (col) VALUES (zeroblob($size));") stmt.SetInt64("$size", int64(len(data))) if _, err := stmt.Step(); err != nil { t.Fatal(err) } rowid := c.LastInsertRowID() blob, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } defer func() { if err := blob.Close(); err != nil { t.Error(err) } }() if n, err := blob.ReadFrom(strings.NewReader(data)); n != int64(len(data)) || err != nil { t.Errorf("blob.ReadFrom(strings.NewReader(%q)) = %d, %v; want %d, ", data, n, err, len(data)) } if _, err := blob.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } if got, err := io.ReadAll(io.Reader(blob)); string(got) != data || err != nil { t.Errorf("io.ReadAll(blob) = %q, %v; want %q, ", got, err, data) } } func TestBlobWriteTo(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if _, err := c.Prep("DROP TABLE IF EXISTS blobs;").Step(); err != nil { t.Fatal(err) } if _, err := c.Prep("CREATE TABLE blobs (col BLOB);").Step(); err != nil { t.Fatal(err) } const data = "Hello, World!" stmt := c.Prep("INSERT INTO blobs (col) VALUES (zeroblob($size));") stmt.SetInt64("$size", int64(len(data))) if _, err := stmt.Step(); err != nil { t.Fatal(err) } rowid := c.LastInsertRowID() blob, err := c.OpenBlob("", "blobs", "col", rowid, true) if err != nil { t.Fatal(err) } defer func() { if err := blob.Close(); err != nil { t.Error(err) } }() if n, err := blob.WriteString(data); n != len(data) || err != nil { t.Errorf("blob.WriteString(%q) = %d, %v; want %d, ", data, n, err, len(data)) } if _, err := blob.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } buf := new(strings.Builder) if n, err := blob.WriteTo(buf); n != int64(buf.Len()) || err != nil { t.Errorf("blob.WriteTo(buf) = %d, %v; want %d, ", n, err, buf.Len()) } if got := buf.String(); got != data { t.Errorf("wrote %q; want %q", got, data) } } zombiezen-go-sqlite-297af96/blocking_step.go000066400000000000000000000045251501417116500211500ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "sync" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" lib "modernc.org/sqlite/lib" ) // See https://sqlite.org/unlock_notify.html for detailed explanation. // unlockNote is a C-allocated struct used as a condition variable. type unlockNote struct { mu sync.Mutex wait sync.Mutex // held while fired == false fired bool } func allocUnlockNote(tls *libc.TLS) (uintptr, error) { ptr := libc.Xcalloc(tls, 1, types.Size_t(unsafe.Sizeof(unlockNote{}))) if ptr == 0 { return 0, fmt.Errorf("out of memory for unlockNote") } un := (*unlockNote)(unsafe.Pointer(ptr)) un.wait.Lock() return ptr, nil } func fireUnlockNote(tls *libc.TLS, ptr uintptr) { un := (*unlockNote)(unsafe.Pointer(ptr)) un.mu.Lock() if !un.fired { un.fired = true un.wait.Unlock() } un.mu.Unlock() } func unlockNotifyCallback(tls *libc.TLS, apArg uintptr, nArg int32) { for ; nArg > 0; nArg-- { fireUnlockNote(tls, *(*uintptr)(unsafe.Pointer(apArg))) // apArg is a C array of pointers. apArg += unsafe.Sizeof(uintptr(0)) } } func waitForUnlockNotify(tls *libc.TLS, db uintptr, unPtr uintptr) ResultCode { un := (*unlockNote)(unsafe.Pointer(unPtr)) if un.fired { un.wait.Lock() } un.fired = false cbPtr := cFuncPointer(unlockNotifyCallback) res := ResultCode(lib.Xsqlite3_unlock_notify(tls, db, cbPtr, unPtr)) if res == ResultOK { un.mu.Lock() fired := un.fired un.mu.Unlock() if !fired { un.wait.Lock() un.wait.Unlock() } } return res } zombiezen-go-sqlite-297af96/cmd/000077500000000000000000000000001501417116500165335ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/000077500000000000000000000000001501417116500234625ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/README.md000066400000000000000000000064341501417116500247500ustar00rootroot00000000000000# Migrating from `crawshaw.io/sqlite` `zombiezen.com/go/sqlite` is designed to mostly be a drop-in replacement for `crawshaw.io/sqlite`. However, there are some incompatible API changes. To aid in migrating, I've prepared a program that rewrites Go source code using `crawshaw.io/sqlite` to use `zombiezen.com/go/sqlite`. ## Installation ```shell go install zombiezen.com/go/sqlite/cmd/zombiezen-sqlite-migrate@latest ``` ## Usage Preview changes with: ```shell zombiezen-sqlite-migrate ./... ``` And then apply them with: ```shell zombiezen-sqlite-migrate -w ./... ``` ## Automatically fixed changes The `zombiezen-sqlite-migrate` tool automatically makes a number of mechanical changes beyond changing the import paths to preserve semantics. - **`ErrorCode` renamed to `ResultCode`.** The `crawshaw.io/sqlite.ErrorCode` type actually represents a SQLite [result code][], not just error codes. To better capture this, the new type is named `zombiezen.com/go/sqlite.ResultCode`. - **Friendlier constant names.** The constant names in `crawshaw.io/sqlite` are written in upper snake case with `SQLITE_` prefixed (e.g. `sqlite.SQLITE_OK`); the constant names in `zombiezen.com/go/sqlite` are written in upper camel case with the type prefixed (e.g. `sqlite.ResultOK`). - `sqlitex.File` and `sqlitex.Buffer` are in `zombiezen.com/go/sqlite/sqlitefile` instead of `zombiezen.com/go/sqlite/sqlitex`. - The **session API** has some symbols renamed for clarity. - `sqlitex.ExecFS` will rename to `sqlitex.ExecuteFS`, `sqlitex.ExecTransientFS` will rename to `sqlitex.ExecuteTransientFS`, and `sqlitex.ExecScriptFS` will rename to `sqlitex.ExecuteScriptFS`. [result code]: https://sqlite.org/rescode.html ## Changes that require manual effort Other usages of the `crawshaw.io/sqlite` may require manual effort to migrate, but the `zombiezen-sqlite-migrate` tool will point them out. ### Application-Defined Functions The `crawshaw.io/sqlite.Conn.CreateFunction` method and supporting APIs like `Context` and `Value` have been re-tooled in `zombiezen.com/go/sqlite` with better ergonomics. See the [`CreateFunction` reference][] for more details. [`CreateFunction` reference]: https://pkg.go.dev/zombiezen.com/go/sqlite#Conn.CreateFunction ### Removed `Blob` methods `zombiezen.com/go/sqlite.Blob` does not implement the [`io.ReaderAt`][] or [`io.WriterAt`][] interfaces. Technically, neither did `crawshaw.io/sqlite.Blob`, because it was not safe to call in parallel, which these interfaces require. To avoid these methods being used incorrectly, I removed them. I also removed the unused embedded interface fields. [`io.ReaderAt`]: https://pkg.go.dev/io#ReaderAt [`io.WriterAt`]: https://pkg.go.dev/io#WriterAt ### No dedicated `sqlite.Error` type I don't want to commit to a specific error type in `zombiezen.com/go/sqlite`, so there I removed the `Error` type. [`zombiezen.com/go/sqlite.ErrCode`][] still extracts the `ResultCode` from an `error`, which covers most needs. [`zombiezen.com/go/sqlite.ErrCode`]: https://pkg.go.dev/zombiezen.com/go/sqlite#ErrCode ### Authorizer `Action` The [`zombiezen.com/go/sqlite.Action`][] uses accessor methods instead of struct fields. Custom `Authorizer`s will need to be rewritten. [`zombiezen.com/go/sqlite.Action`]: https://pkg.go.dev/zombiezen.com/go/sqlite#Action zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/go.mod000066400000000000000000000004421501417116500245700ustar00rootroot00000000000000module zombiezen.com/go/sqlite/cmd/zombiezen-sqlite-migrate go 1.20 require ( github.com/google/go-cmp v0.5.3 golang.org/x/tools v0.16.0 zombiezen.com/go/bass v0.0.0-20210402215001-cb0af0b391a4 ) require ( golang.org/x/mod v0.14.0 // indirect golang.org/x/sys v0.14.0 // indirect ) zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/go.sum000066400000000000000000000750021501417116500246210ustar00rootroot00000000000000cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= zombiezen.com/go/bass v0.0.0-20210402215001-cb0af0b391a4 h1:Yr7oLAYy1vfMqDKljbsEggzFc5Lffd++Bk05TK2MZa4= zombiezen.com/go/bass v0.0.0-20210402215001-cb0af0b391a4/go.mod h1:OECUzBacIBDFAw0+YVcHZ/YrWwORoY3GdP5TPo2R71c= zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/migrate.go000066400000000000000000000640221501417116500254450ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import ( "bytes" "context" "errors" "flag" "fmt" "go/ast" "go/format" "go/token" "go/types" "os" "os/exec" "os/signal" slashpath "path" "strconv" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" goimports "golang.org/x/tools/imports" "zombiezen.com/go/bass/sigterm" ) const programName = "zombiezen-sqlite-migrate" func main() { writeFiles := flag.Bool("w", false, "write to file") flag.Parse() ctx, cancel := signal.NotifyContext(context.Background(), sigterm.Signals()...) exitCode := run(ctx, *writeFiles, flag.Args()) cancel() os.Exit(exitCode) } func run(ctx context.Context, writeFiles bool, patterns []string) int { cfg := &packages.Config{ Context: ctx, Mode: processMode, // TODO(soon): Tests: true, } fmt.Fprintf(os.Stderr, "%s: loading packages...\n", programName) pkgs, err := packages.Load(cfg, patterns...) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", programName, err) return 1 } fmt.Fprintf(os.Stderr, "%s: packages loaded\n", programName) buf := new(bytes.Buffer) var errorList []error for _, pkg := range pkgs { for _, f := range pkg.Syntax { errorList = append(errorList, process(pkg, f)...) origPath := pkg.Fset.File(f.Pos()).Name() if writeFiles { if err := writeFile(buf, origPath, pkg.Fset, f); err != nil { errorList = append(errorList, err) } } else { if err := diff(ctx, buf, origPath, pkg.Fset, f); err != nil { errorList = append(errorList, err) } } } } for _, err := range errorList { fmt.Fprintf(os.Stderr, "%s: %v\n", programName, err) } installFailed := false if writeFiles { if err := installModule(ctx); err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", programName, err) installFailed = true } } if len(errorList) > 0 || installFailed { return 1 } return 0 } const ( crawshaw = "crawshaw.io/sqlite" crawshawX = crawshaw + "/sqlitex" zombiezen = "zombiezen.com/go/sqlite" zombiezenX = zombiezen + "/sqlitex" zombiezenFile = zombiezen + "/sqlitefile" bass = "zombiezen.com/go/bass/sql" bassFile = "zombiezen.com/go/bass/sql/sqlitefile" ) var importRemaps = map[string]string{ crawshaw: zombiezen, crawshawX: zombiezenX, bass + "/sqlitemigration": zombiezen + "/sqlitemigration", } type symbol struct { importPath string typeName string name string } var symbolRewrites = map[symbol]symbol{ {crawshaw, "", "ErrorCode"}: {zombiezen, "", "ResultCode"}, {crawshaw, "", "ChangesetInvert"}: {zombiezen, "", "InvertChangeset"}, {crawshaw, "", "ChangesetConcat"}: {zombiezen, "", "ConcatChangesets"}, {crawshaw, "Conn", "GetAutocommit"}: {zombiezen, "Conn", "AutocommitEnabled"}, {crawshaw, "Conn", "ChangesetApply"}: {zombiezen, "Conn", "ApplyChangeset"}, {crawshaw, "Conn", "ChangesetApplyInverse"}: {zombiezen, "Conn", "ApplyInverseChangeset"}, // Session {crawshaw, "Session", "Changeset"}: {zombiezen, "Session", "WriteChangeset"}, {crawshaw, "Session", "Patchset"}: {zombiezen, "Session", "WritePatchset"}, // ChangesetIterator {crawshaw, "", "ChangesetIter"}: {zombiezen, "", "ChangesetIterator"}, {crawshaw, "", "ChangesetIterStart"}: {zombiezen, "", "NewChangesetIterator"}, {crawshaw, "ChangesetIter", "Finalize"}: {zombiezen, "ChangesetIterator", "Close"}, {crawshaw, "ChangesetIter", "Conflict"}: {zombiezen, "ChangesetIterator", "ConflictValue"}, {crawshaw, "ChangesetIter", "FKConflicts"}: {zombiezen, "ChangesetIterator", "ForeignKeyConflicts"}, {crawshaw, "ChangesetIter", "PK"}: {zombiezen, "ChangesetIterator", "PrimaryKey"}, // Changegroup {crawshaw, "Changegroup", "Delete"}: {zombiezen, "Changegroup", "Clear"}, {crawshaw, "Changegroup", "Output"}: {zombiezen, "Changegroup", "WriteTo"}, // New sqlitefile {crawshawX, "", "Buffer"}: {zombiezenFile, "", "Buffer"}, {crawshawX, "", "File"}: {zombiezenFile, "", "File"}, {crawshawX, "", "NewBuffer"}: {zombiezenFile, "", "NewBuffer"}, {crawshawX, "", "NewBufferSize"}: {zombiezenFile, "", "NewBufferSize"}, {crawshawX, "", "NewFile"}: {zombiezenFile, "", "NewFile"}, {crawshawX, "", "NewFileSize"}: {zombiezenFile, "", "NewFileSize"}, // bass sqlitefile {bassFile, "", "ExecOptions"}: {zombiezenX, "", "ExecOptions"}, {bassFile, "", "Exec"}: {zombiezenX, "", "ExecuteFS"}, {bassFile, "", "ExecTransient"}: {zombiezenX, "", "ExecuteTransientFS"}, {bassFile, "", "PrepareTransient"}: {zombiezenX, "", "PrepareTransientFS"}, {bassFile, "", "ExecScript"}: {zombiezenX, "", "ExecuteScriptFS"}, // Exec -> Execute for older versions of zombiezen. {zombiezenX, "", "ExecFS"}: {zombiezenX, "", "ExecuteFS"}, {zombiezenX, "", "ExecTransientFS"}: {zombiezenX, "", "ExecuteTransientFS"}, {zombiezenX, "", "ExecScriptFS"}: {zombiezenX, "", "ExecuteScriptFS"}, // OpenFlags {crawshaw, "", "SQLITE_OPEN_READONLY"}: {zombiezen, "", "OpenReadOnly"}, {crawshaw, "", "SQLITE_OPEN_READWRITE"}: {zombiezen, "", "OpenReadWrite"}, {crawshaw, "", "SQLITE_OPEN_CREATE"}: {zombiezen, "", "OpenCreate"}, {crawshaw, "", "SQLITE_OPEN_URI"}: {zombiezen, "", "OpenURI"}, {crawshaw, "", "SQLITE_OPEN_MEMORY"}: {zombiezen, "", "OpenMemory"}, {crawshaw, "", "SQLITE_OPEN_NOMUTEX"}: {zombiezen, "", "OpenNoMutex"}, {crawshaw, "", "SQLITE_OPEN_FULLMUTEX"}: {zombiezen, "", "OpenFullMutex"}, {crawshaw, "", "SQLITE_OPEN_SHAREDCACHE"}: {zombiezen, "", "OpenSharedCache"}, {crawshaw, "", "SQLITE_OPEN_PRIVATECACHE"}: {zombiezen, "", "OpenPrivateCache"}, {crawshaw, "", "SQLITE_OPEN_WAL"}: {zombiezen, "", "OpenWAL"}, // ColumnType {crawshaw, "", "SQLITE_INTEGER"}: {zombiezen, "", "TypeInteger"}, {crawshaw, "", "SQLITE_FLOAT"}: {zombiezen, "", "TypeFloat"}, {crawshaw, "", "SQLITE_TEXT"}: {zombiezen, "", "TypeText"}, {crawshaw, "", "SQLITE_BLOB"}: {zombiezen, "", "TypeBlob"}, {crawshaw, "", "SQLITE_NULL"}: {zombiezen, "", "TypeNull"}, // Primary result codes. {crawshaw, "", "SQLITE_OK"}: {zombiezen, "", "ResultOK"}, {crawshaw, "", "SQLITE_ERROR"}: {zombiezen, "", "ResultError"}, {crawshaw, "", "SQLITE_INTERNAL"}: {zombiezen, "", "ResultInternal"}, {crawshaw, "", "SQLITE_PERM"}: {zombiezen, "", "ResultPerm"}, {crawshaw, "", "SQLITE_ABORT"}: {zombiezen, "", "ResultAbort"}, {crawshaw, "", "SQLITE_BUSY"}: {zombiezen, "", "ResultBusy"}, {crawshaw, "", "SQLITE_LOCKED"}: {zombiezen, "", "ResultLocked"}, {crawshaw, "", "SQLITE_NOMEM"}: {zombiezen, "", "ResultNoMem"}, {crawshaw, "", "SQLITE_READONLY"}: {zombiezen, "", "ResultReadOnly"}, {crawshaw, "", "SQLITE_INTERRUPT"}: {zombiezen, "", "ResultInterrupt"}, {crawshaw, "", "SQLITE_IOERR"}: {zombiezen, "", "ResultIOErr"}, {crawshaw, "", "SQLITE_CORRUPT"}: {zombiezen, "", "ResultCorrupt"}, {crawshaw, "", "SQLITE_NOTFOUND"}: {zombiezen, "", "ResultNotFound"}, {crawshaw, "", "SQLITE_FULL"}: {zombiezen, "", "ResultFull"}, {crawshaw, "", "SQLITE_CANTOPEN"}: {zombiezen, "", "ResultCantOpen"}, {crawshaw, "", "SQLITE_PROTOCOL"}: {zombiezen, "", "ResultProtocol"}, {crawshaw, "", "SQLITE_EMPTY"}: {zombiezen, "", "ResultEmpty"}, {crawshaw, "", "SQLITE_SCHEMA"}: {zombiezen, "", "ResultSchema"}, {crawshaw, "", "SQLITE_TOOBIG"}: {zombiezen, "", "ResultTooBig"}, {crawshaw, "", "SQLITE_CONSTRAINT"}: {zombiezen, "", "ResultConstraint"}, {crawshaw, "", "SQLITE_MISMATCH"}: {zombiezen, "", "ResultMismatch"}, {crawshaw, "", "SQLITE_MISUSE"}: {zombiezen, "", "ResultMisuse"}, {crawshaw, "", "SQLITE_NOLFS"}: {zombiezen, "", "ResultNoLFS"}, {crawshaw, "", "SQLITE_AUTH"}: {zombiezen, "", "ResultAuth"}, {crawshaw, "", "SQLITE_FORMAT"}: {zombiezen, "", "ResultFormat"}, {crawshaw, "", "SQLITE_RANGE"}: {zombiezen, "", "ResultRange"}, {crawshaw, "", "SQLITE_NOTADB"}: {zombiezen, "", "ResultNotADB"}, {crawshaw, "", "SQLITE_NOTICE"}: {zombiezen, "", "ResultNotice"}, {crawshaw, "", "SQLITE_WARNING"}: {zombiezen, "", "ResultWarning"}, {crawshaw, "", "SQLITE_ROW"}: {zombiezen, "", "ResultRow"}, {crawshaw, "", "SQLITE_DONE"}: {zombiezen, "", "ResultDone"}, // Extended result codes. {crawshaw, "", "SQLITE_ERROR_MISSING_COLLSEQ"}: {zombiezen, "", "ResultErrorMissingCollSeq"}, {crawshaw, "", "SQLITE_ERROR_RETRY"}: {zombiezen, "", "ResultErrorRetry"}, {crawshaw, "", "SQLITE_ERROR_SNAPSHOT"}: {zombiezen, "", "ResultErrorSnapshot"}, {crawshaw, "", "SQLITE_IOERR_READ"}: {zombiezen, "", "ResultIOErrRead"}, {crawshaw, "", "SQLITE_IOERR_SHORT_READ"}: {zombiezen, "", "ResultIOErrShortRead"}, {crawshaw, "", "SQLITE_IOERR_WRITE"}: {zombiezen, "", "ResultIOErrWrite"}, {crawshaw, "", "SQLITE_IOERR_FSYNC"}: {zombiezen, "", "ResultIOErrFsync"}, {crawshaw, "", "SQLITE_IOERR_DIR_FSYNC"}: {zombiezen, "", "ResultIOErrDirFsync"}, {crawshaw, "", "SQLITE_IOERR_TRUNCATE"}: {zombiezen, "", "ResultIOErrTruncate"}, {crawshaw, "", "SQLITE_IOERR_FSTAT"}: {zombiezen, "", "ResultIOErrFstat"}, {crawshaw, "", "SQLITE_IOERR_UNLOCK"}: {zombiezen, "", "ResultIOErrUnlock"}, {crawshaw, "", "SQLITE_IOERR_RDLOCK"}: {zombiezen, "", "ResultIOErrReadLock"}, {crawshaw, "", "SQLITE_IOERR_DELETE"}: {zombiezen, "", "ResultIOErrDelete"}, {crawshaw, "", "SQLITE_IOERR_BLOCKED"}: {zombiezen, "", "ResultIOErrBlocked"}, {crawshaw, "", "SQLITE_IOERR_NOMEM"}: {zombiezen, "", "ResultIOErrNoMem"}, {crawshaw, "", "SQLITE_IOERR_ACCESS"}: {zombiezen, "", "ResultIOErrAccess"}, {crawshaw, "", "SQLITE_IOERR_CHECKRESERVEDLOCK"}: {zombiezen, "", "ResultIOErrCheckReservedLock"}, {crawshaw, "", "SQLITE_IOERR_LOCK"}: {zombiezen, "", "ResultIOErrLock"}, {crawshaw, "", "SQLITE_IOERR_CLOSE"}: {zombiezen, "", "ResultIOErrClose"}, {crawshaw, "", "SQLITE_IOERR_DIR_CLOSE"}: {zombiezen, "", "ResultIOErrDirClose"}, {crawshaw, "", "SQLITE_IOERR_SHMOPEN"}: {zombiezen, "", "ResultIOErrSHMOpen"}, {crawshaw, "", "SQLITE_IOERR_SHMSIZE"}: {zombiezen, "", "ResultIOErrSHMSize"}, {crawshaw, "", "SQLITE_IOERR_SHMLOCK"}: {zombiezen, "", "ResultIOErrSHMLock"}, {crawshaw, "", "SQLITE_IOERR_SHMMAP"}: {zombiezen, "", "ResultIOErrSHMMap"}, {crawshaw, "", "SQLITE_IOERR_SEEK"}: {zombiezen, "", "ResultIOErrSeek"}, {crawshaw, "", "SQLITE_IOERR_DELETE_NOENT"}: {zombiezen, "", "ResultIOErrDeleteNoEnt"}, {crawshaw, "", "SQLITE_IOERR_MMAP"}: {zombiezen, "", "ResultIOErrMMap"}, {crawshaw, "", "SQLITE_IOERR_GETTEMPPATH"}: {zombiezen, "", "ResultIOErrGetTempPath"}, {crawshaw, "", "SQLITE_IOERR_CONVPATH"}: {zombiezen, "", "ResultIOErrConvPath"}, {crawshaw, "", "SQLITE_IOERR_VNODE"}: {zombiezen, "", "ResultIOErrVNode"}, {crawshaw, "", "SQLITE_IOERR_AUTH"}: {zombiezen, "", "ResultIOErrAuth"}, {crawshaw, "", "SQLITE_IOERR_BEGIN_ATOMIC"}: {zombiezen, "", "ResultIOErrBeginAtomic"}, {crawshaw, "", "SQLITE_IOERR_COMMIT_ATOMIC"}: {zombiezen, "", "ResultIOErrCommitAtomic"}, {crawshaw, "", "SQLITE_IOERR_ROLLBACK_ATOMIC"}: {zombiezen, "", "ResultIOErrRollbackAtomic"}, {crawshaw, "", "SQLITE_LOCKED_SHAREDCACHE"}: {zombiezen, "", "ResultLockedSharedCache"}, {crawshaw, "", "SQLITE_BUSY_RECOVERY"}: {zombiezen, "", "ResultBusyRecovery"}, {crawshaw, "", "SQLITE_BUSY_SNAPSHOT"}: {zombiezen, "", "ResultBusySnapshot"}, {crawshaw, "", "SQLITE_CANTOPEN_NOTEMPDIR"}: {zombiezen, "", "ResultCantOpenNoTempDir"}, {crawshaw, "", "SQLITE_CANTOPEN_ISDIR"}: {zombiezen, "", "ResultCantOpenIsDir"}, {crawshaw, "", "SQLITE_CANTOPEN_FULLPATH"}: {zombiezen, "", "ResultCantOpenFullPath"}, {crawshaw, "", "SQLITE_CANTOPEN_CONVPATH"}: {zombiezen, "", "ResultCantOpenConvPath"}, {crawshaw, "", "SQLITE_CORRUPT_VTAB"}: {zombiezen, "", "ResultCorruptVTab"}, {crawshaw, "", "SQLITE_READONLY_RECOVERY"}: {zombiezen, "", "ResultReadOnlyRecovery"}, {crawshaw, "", "SQLITE_READONLY_CANTLOCK"}: {zombiezen, "", "ResultReadOnlyCantLock"}, {crawshaw, "", "SQLITE_READONLY_ROLLBACK"}: {zombiezen, "", "ResultReadOnlyRollback"}, {crawshaw, "", "SQLITE_READONLY_DBMOVED"}: {zombiezen, "", "ResultReadOnlyDBMoved"}, {crawshaw, "", "SQLITE_READONLY_CANTINIT"}: {zombiezen, "", "ResultReadOnlyCantInit"}, {crawshaw, "", "SQLITE_READONLY_DIRECTORY"}: {zombiezen, "", "ResultReadOnlyDirectory"}, {crawshaw, "", "SQLITE_ABORT_ROLLBACK"}: {zombiezen, "", "ResultAbortRollback"}, {crawshaw, "", "SQLITE_CONSTRAINT_CHECK"}: {zombiezen, "", "ResultConstraintCheck"}, {crawshaw, "", "SQLITE_CONSTRAINT_COMMITHOOK"}: {zombiezen, "", "ResultConstraintCommitHook"}, {crawshaw, "", "SQLITE_CONSTRAINT_FOREIGNKEY"}: {zombiezen, "", "ResultConstraintForeignKey"}, {crawshaw, "", "SQLITE_CONSTRAINT_FUNCTION"}: {zombiezen, "", "ResultConstraintFunction"}, {crawshaw, "", "SQLITE_CONSTRAINT_NOTNULL"}: {zombiezen, "", "ResultConstraintNotNull"}, {crawshaw, "", "SQLITE_CONSTRAINT_PRIMARYKEY"}: {zombiezen, "", "ResultConstraintPrimaryKey"}, {crawshaw, "", "SQLITE_CONSTRAINT_TRIGGER"}: {zombiezen, "", "ResultConstraintTrigger"}, {crawshaw, "", "SQLITE_CONSTRAINT_UNIQUE"}: {zombiezen, "", "ResultConstraintUnique"}, {crawshaw, "", "SQLITE_CONSTRAINT_VTAB"}: {zombiezen, "", "ResultConstraintVTab"}, {crawshaw, "", "SQLITE_CONSTRAINT_ROWID"}: {zombiezen, "", "ResultConstraintRowID"}, {crawshaw, "", "SQLITE_NOTICE_RECOVER_WAL"}: {zombiezen, "", "ResultNoticeRecoverWAL"}, {crawshaw, "", "SQLITE_NOTICE_RECOVER_ROLLBACK"}: {zombiezen, "", "ResultNoticeRecoverRollback"}, {crawshaw, "", "SQLITE_WARNING_AUTOINDEX"}: {zombiezen, "", "ResultWarningAutoIndex"}, {crawshaw, "", "SQLITE_AUTH_USER"}: {zombiezen, "", "ResultAuthUser"}, // OpType {crawshaw, "", "SQLITE_CREATE_INDEX"}: {zombiezen, "", "OpCreateIndex"}, {crawshaw, "", "SQLITE_CREATE_TABLE"}: {zombiezen, "", "OpCreateTable"}, {crawshaw, "", "SQLITE_CREATE_TEMP_INDEX"}: {zombiezen, "", "OpCreateTempIndex"}, {crawshaw, "", "SQLITE_CREATE_TEMP_TABLE"}: {zombiezen, "", "OpCreateTempTable"}, {crawshaw, "", "SQLITE_CREATE_TEMP_TRIGGER"}: {zombiezen, "", "OpCreateTempTrigger"}, {crawshaw, "", "SQLITE_CREATE_TEMP_VIEW"}: {zombiezen, "", "OpCreateTempView"}, {crawshaw, "", "SQLITE_CREATE_TRIGGER"}: {zombiezen, "", "OpCreateTrigger"}, {crawshaw, "", "SQLITE_CREATE_VIEW"}: {zombiezen, "", "OpCreateView"}, {crawshaw, "", "SQLITE_DELETE"}: {zombiezen, "", "OpDelete"}, {crawshaw, "", "SQLITE_DROP_INDEX"}: {zombiezen, "", "OpDropIndex"}, {crawshaw, "", "SQLITE_DROP_TABLE"}: {zombiezen, "", "OpDropTable"}, {crawshaw, "", "SQLITE_DROP_TEMP_INDEX"}: {zombiezen, "", "OpDropTempIndex"}, {crawshaw, "", "SQLITE_DROP_TEMP_TABLE"}: {zombiezen, "", "OpDropTempTable"}, {crawshaw, "", "SQLITE_DROP_TEMP_TRIGGER"}: {zombiezen, "", "OpDropTempTrigger"}, {crawshaw, "", "SQLITE_DROP_TEMP_VIEW"}: {zombiezen, "", "OpDropTempView"}, {crawshaw, "", "SQLITE_DROP_TRIGGER"}: {zombiezen, "", "OpDropTrigger"}, {crawshaw, "", "SQLITE_DROP_VIEW"}: {zombiezen, "", "OpDropView"}, {crawshaw, "", "SQLITE_INSERT"}: {zombiezen, "", "OpInsert"}, {crawshaw, "", "SQLITE_PRAGMA"}: {zombiezen, "", "OpPragma"}, {crawshaw, "", "SQLITE_READ"}: {zombiezen, "", "OpRead"}, {crawshaw, "", "SQLITE_SELECT"}: {zombiezen, "", "OpSelect"}, {crawshaw, "", "SQLITE_TRANSACTION"}: {zombiezen, "", "OpTransaction"}, {crawshaw, "", "SQLITE_UPDATE"}: {zombiezen, "", "OpUpdate"}, {crawshaw, "", "SQLITE_ATTACH"}: {zombiezen, "", "OpAttach"}, {crawshaw, "", "SQLITE_DETACH"}: {zombiezen, "", "OpDetach"}, {crawshaw, "", "SQLITE_ALTER_TABLE"}: {zombiezen, "", "OpAlterTable"}, {crawshaw, "", "SQLITE_REINDEX"}: {zombiezen, "", "OpReindex"}, {crawshaw, "", "SQLITE_ANALYZE"}: {zombiezen, "", "OpAnalyze"}, {crawshaw, "", "SQLITE_CREATE_VTABLE"}: {zombiezen, "", "OpCreateVTable"}, {crawshaw, "", "SQLITE_DROP_VTABLE"}: {zombiezen, "", "OpDropVTable"}, {crawshaw, "", "SQLITE_FUNCTION"}: {zombiezen, "", "OpFunction"}, {crawshaw, "", "SQLITE_SAVEPOINT"}: {zombiezen, "", "OpSavepoint"}, {crawshaw, "", "SQLITE_COPY"}: {zombiezen, "", "OpCopy"}, {crawshaw, "", "SQLITE_RECURSIVE"}: {zombiezen, "", "OpRecursive"}, // Limit {crawshaw, "", "SQLITE_LIMIT_LENGTH"}: {zombiezen, "", "LimitLength"}, {crawshaw, "", "SQLITE_LIMIT_SQL_LENGTH"}: {zombiezen, "", "LimitSQLLength"}, {crawshaw, "", "SQLITE_LIMIT_COLUMN"}: {zombiezen, "", "LimitColumn"}, {crawshaw, "", "SQLITE_LIMIT_EXPR_DEPTH"}: {zombiezen, "", "LimitExprDepth"}, {crawshaw, "", "SQLITE_LIMIT_COMPOUND_SELECT"}: {zombiezen, "", "LimitCompoundSelect"}, {crawshaw, "", "SQLITE_LIMIT_VDBE_OP"}: {zombiezen, "", "LimitVDBEOp"}, {crawshaw, "", "SQLITE_LIMIT_FUNCTION_ARG"}: {zombiezen, "", "LimitFunctionArg"}, {crawshaw, "", "SQLITE_LIMIT_ATTACHED"}: {zombiezen, "", "LimitAttached"}, {crawshaw, "", "SQLITE_LIMIT_LIKE_PATTERN_LENGTH"}: {zombiezen, "", "LimitLikePatternLength"}, {crawshaw, "", "SQLITE_LIMIT_VARIABLE_NUMBER"}: {zombiezen, "", "LimitVariableNumber"}, {crawshaw, "", "SQLITE_LIMIT_TRIGGER_DEPTH"}: {zombiezen, "", "LimitTriggerDepth"}, {crawshaw, "", "SQLITE_LIMIT_WORKER_THREADS"}: {zombiezen, "", "LimitWorkerThreads"}, // AuthResult {crawshaw, "", "SQLITE_DENY"}: {zombiezen, "", "AuthResultDeny"}, {crawshaw, "", "SQLITE_IGNORE"}: {zombiezen, "", "AuthResultIgnore"}, // ConflictType {crawshaw, "", "SQLITE_CHANGESET_DATA"}: {zombiezen, "", "ChangesetData"}, {crawshaw, "", "SQLITE_CHANGESET_NOTFOUND"}: {zombiezen, "", "ChangesetNotFound"}, {crawshaw, "", "SQLITE_CHANGESET_CONFLICT"}: {zombiezen, "", "ChangesetConflict"}, {crawshaw, "", "SQLITE_CHANGESET_CONSTRAINT"}: {zombiezen, "", "ChangesetConstraint"}, {crawshaw, "", "SQLITE_CHANGESET_FOREIGN_KEY"}: {zombiezen, "", "ChangesetForeignKey"}, // ConflictAction {crawshaw, "", "SQLITE_CHANGESET_OMIT"}: {zombiezen, "", "ChangesetOmit"}, {crawshaw, "", "SQLITE_CHANGESET_ABORT"}: {zombiezen, "", "ChangesetAbort"}, {crawshaw, "", "SQLITE_CHANGESET_REPLACE"}: {zombiezen, "", "ChangesetReplace"}, } const ( removedWarning = "gone with no replacement available" ) var symbolWarnings = map[symbol]string{ {crawshaw, "Blob", "ReadAt"}: removedWarning, {crawshaw, "Blob", "WriteAt"}: removedWarning, {crawshaw, "Blob", "Closer"}: removedWarning, {crawshaw, "Blob", "ReadWriteSeeker"}: removedWarning, {crawshaw, "Blob", "ReaderAt"}: removedWarning, {crawshaw, "Blob", "WriterAt"}: removedWarning, {crawshaw, "", "Error"}: "use sqlite.ErrorCode instead", {crawshaw, "Conn", "CreateFunction"}: "CreateFunction's API has changed substantially and this code needs to be rewritten", {crawshaw, "Conn", "EnableDoubleQuotedStringLiterals"}: removedWarning, {crawshaw, "Conn", "EnableLoadExtension"}: removedWarning, {crawshaw, "Value", "IsNil"}: removedWarning, {crawshaw, "Value", "Len"}: "use Value.Blob or Value.Text methods", } const processMode = packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo func process(pkg *packages.Package, file *ast.File) []error { var warnings []error for _, imp := range file.Imports { if imp.Path == nil { continue } impPath, err := strconv.Unquote(imp.Path.Value) if err != nil { continue } if remapped := importRemaps[impPath]; remapped != "" { if local := localImportName(pkg.TypesInfo, imp); local != slashpath.Base(remapped) { if imp.Name == nil { imp.Name = ast.NewIdent(local) } else { imp.Name.Name = local } } imp.Path.Value = strconv.Quote(remapped) } } skipList := make(map[ast.Node]struct{}) for _, decl := range file.Decls { astutil.Apply(decl, func(c *astutil.Cursor) bool { if _, skip := skipList[c.Node()]; skip { return false } switch node := c.Node().(type) { case *ast.Ident: obj := pkg.TypesInfo.ObjectOf(node) if obj == nil { return true } objPkg := obj.Pkg() if objPkg == nil { // Universe scope (built-ins). return true } sym := symbol{ importPath: objPkg.Path(), name: obj.Name(), } if sig, ok := obj.Type().(*types.Signature); ok { if recv := sig.Recv(); recv != nil { if named, ok := depointerType(recv.Type()).(*types.Named); ok { sym.typeName = named.Obj().Name() } } } if newSym := symbolRewrites[sym]; newSym.name != "" { node.Name = newSym.name if newSym.importPath != importRemaps[sym.importPath] { // Your symbol is in a different package, Mario! if sel, ok := c.Parent().(*ast.SelectorExpr); ok { if pkgIdent, ok := sel.X.(*ast.Ident); ok { // Qualified identifier. pkgIdent.Name = acquirePackageID(pkg.Fset, pkg.TypesInfo, file, newSym.importPath) } } } } if warning := symbolWarnings[sym]; warning != "" { pos := pkg.Fset.Position(node.NamePos) warnings = append(warnings, fmt.Errorf("%v: %s", pos, warning)) } case *ast.SelectorExpr: sel := pkg.TypesInfo.Selections[node] if sel == nil { return true } obj := sel.Obj() objPkg := obj.Pkg() if objPkg == nil { // Universe scope (built-ins). return true } sym := symbol{ importPath: objPkg.Path(), name: obj.Name(), } if recv := sel.Recv(); recv != nil { if named, ok := depointerType(recv).(*types.Named); ok { sym.typeName = named.Obj().Name() } } if newSym := symbolRewrites[sym]; newSym.name != "" { // Selections doesn't include qualified identifiers, // so no import changes. node.Sel.Name = newSym.name } if warning := symbolWarnings[sym]; warning != "" { pos := pkg.Fset.Position(node.Sel.NamePos) warnings = append(warnings, fmt.Errorf("%v: %s", pos, warning)) } // We don't want to visit the identifier during descent, // since we've already rewritten it. skipList[node.Sel] = struct{}{} } return true }, nil) } return warnings } func acquirePackageID(fset *token.FileSet, info *types.Info, file *ast.File, importPath string) string { usedIDs := make(map[string]struct{}, len(file.Imports)) for _, imp := range file.Imports { ipath, err := strconv.Unquote(imp.Path.Value) if err != nil { continue } id := localImportName(info, imp) if id == "_" { continue } if ipath == importPath { return id } usedIDs[id] = struct{}{} } base := slashpath.Base(importPath) if _, baseUsed := usedIDs[base]; !baseUsed { astutil.AddImport(fset, file, importPath) return base } for i := 2; ; i++ { id := fmt.Sprintf("%s%d", base, i) if _, used := usedIDs[id]; !used { astutil.AddNamedImport(fset, file, id, importPath) return id } } } // localImportName returns the identifier being used for an import declaration. func localImportName(info *types.Info, imp *ast.ImportSpec) string { if imp.Name != nil { return imp.Name.Name } if resolvedName, ok := info.Implicits[imp].(*types.PkgName); ok { return resolvedName.Name() } // Fallback: Use last path component of import path if it is an identifier. path, err := strconv.Unquote(imp.Path.Value) if err != nil { return "" } base := slashpath.Base(path) if !token.IsIdentifier(base) { return "" } return base } func depointerType(t types.Type) types.Type { for { p, ok := t.(*types.Pointer) if !ok { return t } t = p.Elem() } } func installModule(ctx context.Context) error { pkgs, err := packages.Load(&packages.Config{ Context: ctx, Mode: packages.NeedName | packages.NeedFiles, }, zombiezen) if err != nil { return fmt.Errorf("go get %s: %w", zombiezen, err) } for _, pkg := range pkgs { if len(pkg.Errors) == 0 && pkg.PkgPath == zombiezen { // Already installed. return nil } } getCmd := exec.Command("go", "get", "-d", zombiezen+"@v0.2.0") getCmd.Stdout = os.Stderr getCmd.Stderr = os.Stderr if err := sigterm.Run(ctx, getCmd); err != nil { return fmt.Errorf("go get %s: %w", zombiezen, err) } return nil } func formatFile(buf *bytes.Buffer, path string, fset *token.FileSet, file *ast.File) ([]byte, error) { buf.Reset() if err := format.Node(buf, fset, file); err != nil { return nil, err } return goimports.Process(path, buf.Bytes(), nil) } func writeFile(buf *bytes.Buffer, origPath string, fset *token.FileSet, file *ast.File) error { formatted, err := formatFile(buf, origPath, fset, file) if err != nil { return fmt.Errorf("write %s: %w", origPath, err) } if err := os.WriteFile(origPath, formatted, 0o666); err != nil { return err } return nil } func diff(ctx context.Context, buf *bytes.Buffer, origPath string, fset *token.FileSet, file *ast.File) error { formatted, err := formatFile(buf, origPath, fset, file) if err != nil { return fmt.Errorf("diff %s: %w", origPath, err) } f, err := os.CreateTemp("", "zombiezen-sqlite-*.go") if err != nil { return fmt.Errorf("diff %s: %w", origPath, err) } fname := f.Name() defer func() { f.Close() if err := os.Remove(fname); err != nil { fmt.Fprintf(os.Stderr, "%s: cleaning up temp file: %v\n", programName, err) } }() if _, err := f.Write(formatted); err != nil { return fmt.Errorf("diff %s %s: %w", origPath, fname, err) } if err := f.Close(); err != nil { return fmt.Errorf("diff %s %s: %w", origPath, fname, err) } buf.Reset() c := exec.Command("diff", "-u", origPath, fname) c.Stdout = buf c.Stderr = os.Stderr err = sigterm.Run(ctx, c) if err == nil { // Files are identical. return nil } if exitErr := new(exec.ExitError); !errors.As(err, &exitErr) || exitErr.ExitCode() != 1 { return fmt.Errorf("diff %s %s: %w", origPath, fname, err) } fmt.Printf("diff -u %s %s\n", origPath, fname) os.Stdout.Write(buf.Bytes()) return nil } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/migrate_test.go000066400000000000000000000067621501417116500265130ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import ( "bytes" "fmt" "go/format" "os" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" ) func TestProcess(t *testing.T) { crawshawModule, err := skeletonModule("crawshaw.io/sqlite") if err != nil { t.Fatal(err) } bassModule, err := skeletonModule("zombiezen.com/go/bass") if err != nil { t.Fatal(err) } sqliteModule, err := skeletonModule("zombiezen.com/go/sqlite") if err != nil { t.Fatal(err) } rootDir := filepath.Join("testdata", "TestProcess") testRunContents, err := os.ReadDir(rootDir) if err != nil { t.Fatal(err) } const mainPkgPath = "example.com/foo" const mainFilename = "file.go" for _, ent := range testRunContents { name := ent.Name() if !ent.IsDir() || strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { continue } t.Run(name, func(t *testing.T) { dir := filepath.Join(rootDir, name) want, err := os.ReadFile(filepath.Join(dir, "want.go")) if err != nil { t.Fatal(err) } want, err = format.Source(want) if err != nil { t.Fatal(err) } originalFile := filepath.Join(dir, "original.go") e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ crawshawModule, bassModule, sqliteModule, { Name: mainPkgPath, Files: map[string]any{ mainFilename: packagestest.Copy(originalFile), }, }, }) cfg := new(packages.Config) *cfg = *e.Config cfg.Mode = processMode pkgs, err := packages.Load(cfg, "pattern="+mainPkgPath) if err != nil { t.Fatal(err) } if len(pkgs) != 1 { t.Fatalf("Found %d packages; want 1", len(pkgs)) } pkg := pkgs[0] if len(pkg.Errors) > 0 { for _, err := range pkg.Errors { t.Errorf("Load %s: %v", originalFile, err) } return } if len(pkg.Syntax) != 1 { t.Fatalf("Found %d parsed files; want 1", len(pkg.Syntax)) } file := pkg.Syntax[0] for _, err := range process(pkg, file) { t.Logf("process: %v", err) } got, err := formatFile(new(bytes.Buffer), originalFile, pkg.Fset, file) if err != nil { t.Fatalf("Formatting output: %v", err) } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("diff a/%s b/%s:\n%s", mainFilename, mainFilename, diff) } }) } } func skeletonModule(importPath string) (packagestest.Module, error) { mod := packagestest.Module{ Name: importPath, Files: make(map[string]any), } dir := filepath.Join("testdata", "skeleton", filepath.FromSlash(importPath)) err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } relpath := strings.TrimPrefix(path, dir+string(filepath.Separator)) mod.Files[filepath.ToSlash(relpath)] = packagestest.Copy(path) return nil }) if err != nil { return packagestest.Module{}, fmt.Errorf("load skeleton module %q: %w", importPath, err) } return mod, nil } type logWriter struct { logger interface{ Logf(string, ...any) } buf []byte } func (lw *logWriter) Write(p []byte) (int, error) { lastLF := bytes.LastIndexByte(p, '\n') if lastLF == -1 { lw.buf = append(lw.buf, p...) return len(p), nil } if len(lw.buf) > 0 { lw.buf = append(lw.buf, p[:lastLF]...) lw.logger.Logf("%s", lw.buf) lw.buf = lw.buf[:0] } else { lw.logger.Logf("%s", p[:lastLF]) } lw.buf = append(lw.buf, p[lastLF+1:]...) return len(p), nil } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/000077500000000000000000000000001501417116500252735ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/000077500000000000000000000000001501417116500275515ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/AnonymousInterface/000077500000000000000000000000001501417116500333625ustar00rootroot00000000000000original.go000066400000000000000000000002051501417116500354330ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/AnonymousInterface// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main func main() { var foo interface { Bar() } _ = foo } want.go000066400000000000000000000002051501417116500346000ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/AnonymousInterface// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main func main() { var foo interface { Bar() } _ = foo } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Autocommit/000077500000000000000000000000001501417116500316725ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Autocommit/original.go000066400000000000000000000003121501417116500340210ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "crawshaw.io/sqlite" func main() { var db *sqlite.Conn db, _ = sqlite.OpenConn(":memory:", 0) db.GetAutocommit() } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Autocommit/want.go000066400000000000000000000003231501417116500331700ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "zombiezen.com/go/sqlite" func main() { var db *sqlite.Conn db, _ = sqlite.OpenConn(":memory:", 0) db.AutocommitEnabled() } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ErrorCode/000077500000000000000000000000001501417116500314355ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ErrorCode/original.go000066400000000000000000000002571501417116500335740ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "crawshaw.io/sqlite" func main() { var res sqlite.ErrorCode = sqlite.SQLITE_OK _ = res } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ErrorCode/want.go000066400000000000000000000002641501417116500327370ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "zombiezen.com/go/sqlite" func main() { var res sqlite.ResultCode = sqlite.ResultOK _ = res } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Exec/000077500000000000000000000000001501417116500304355ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Exec/original.go000066400000000000000000000006161501417116500325730ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import ( "crawshaw.io/sqlite" "crawshaw.io/sqlite/sqlitex" "zombiezen.com/go/bass/sql/sqlitefile" ) func main() { var conn *sqlite.Conn var file *sqlitex.File sqlitefile.ExecScript(conn, nil, "foo.sql", &sqlitefile.ExecOptions{ Args: []interface{}{1, "foo"}, }) sqlitex.Exec(conn, `SELECT 1;`, nil) _ = file } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/Exec/want.go000066400000000000000000000006451501417116500317420ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import ( "zombiezen.com/go/sqlite" sqlitefile2 "zombiezen.com/go/sqlite/sqlitefile" "zombiezen.com/go/sqlite/sqlitex" ) func main() { var conn *sqlite.Conn var file *sqlitefile2.File sqlitex.ExecuteScriptFS(conn, nil, "foo.sql", &sqlitex.ExecOptions{ Args: []interface{}{1, "foo"}, }) sqlitex.Exec(conn, `SELECT 1;`, nil) _ = file } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ExecScriptFS/000077500000000000000000000000001501417116500320535ustar00rootroot00000000000000original.go000066400000000000000000000002401501417116500341230ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ExecScriptFS// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "zombiezen.com/go/sqlite/sqlitex" func main() { _ = sqlitex.ExecScriptFS } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ExecScriptFS/want.go000066400000000000000000000002431501417116500333520ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "zombiezen.com/go/sqlite/sqlitex" func main() { _ = sqlitex.ExecuteScriptFS } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/File/000077500000000000000000000000001501417116500304305ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/File/original.go000066400000000000000000000003751501417116500325700ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "crawshaw.io/sqlite/sqlitex" func main() { var f *sqlitex.File f, _ = sqlitex.NewFile(nil) var buf *sqlitex.Buffer buf, _ = sqlitex.NewBuffer(nil) _, _ = f, buf } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/File/want.go000066400000000000000000000004261501417116500317320ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import ( "zombiezen.com/go/sqlite/sqlitefile" ) func main() { var f *sqlitefile.File f, _ = sqlitefile.NewFile(nil) var buf *sqlitefile.Buffer buf, _ = sqlitefile.NewBuffer(nil) _, _ = f, buf } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ImportRewrite/000077500000000000000000000000001501417116500323655ustar00rootroot00000000000000original.go000066400000000000000000000003271501417116500344430ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ImportRewrite// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "crawshaw.io/sqlite" func main() { var db *sqlite.Conn var err error db, err = sqlite.OpenConn(":memory:", 0) _, _ = db, err } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/TestProcess/ImportRewrite/want.go000066400000000000000000000003341501417116500336650ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package main import "zombiezen.com/go/sqlite" func main() { var db *sqlite.Conn var err error db, err = sqlite.OpenConn(":memory:", 0) _, _ = db, err } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/000077500000000000000000000000001501417116500271175ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/000077500000000000000000000000001501417116500313445ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlite/000077500000000000000000000000001501417116500326455ustar00rootroot00000000000000sqlitex/000077500000000000000000000000001501417116500342575ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlitestubs.go000066400000000000000000000013111501417116500357420ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlite/sqlitex// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Test stubs for crawshaw.io/sqlite/sqlitex. package sqlitex import "crawshaw.io/sqlite" type File struct { io.Reader io.Writer io.Seeker } func NewFile(conn *sqlite.Conn) (*File, error) { return nil, nil } func NewFileSize(conn *sqlite.Conn, initSize int) (*File, error) { return nil, nil } type Buffer struct { io.Reader io.Writer io.ByteScanner } func NewBuffer(conn *sqlite.Conn) (*Buffer, error) { return nil, nil } func NewBufferSize(conn *sqlite.Conn, pageSize int) (*Buffer, error) { return nil, nil } func Exec(conn *sqlite.Conn, query string, resultFn func(stmt *sqlite.Stmt) error, args ...interface{}) error { return nil } stubs.go000066400000000000000000000005311501417116500342540ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/crawshaw.io/sqlite// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Test stubs for crawshaw.io/sqlite. package sqlite type Conn struct { } func OpenConn(path string, flags OpenFlags) (*Conn, error) { return nil, nil } func (c *Conn) GetAutocommit() bool { return false } type ErrorCode int const SQLITE_OK = ErrorCode(0) type Stmt struct { } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/000077500000000000000000000000001501417116500316765ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/000077500000000000000000000000001501417116500323035ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/bass/000077500000000000000000000000001501417116500332335ustar00rootroot00000000000000sql/000077500000000000000000000000001501417116500337535ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/basssqlitefile/000077500000000000000000000000001501417116500361145ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/bass/sqlstubs.go000066400000000000000000000012411501417116500376010ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/bass/sql/sqlitefile// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Test stubs for zombiezen.com/go/bass/sql/sqlitefile. package sqlitefile import ( "os" "crawshaw.io/sqlite" ) type ExecOptions struct { Args []interface{} Named map[string]interface{} ResultFunc func(stmt *sqlite.Stmt) error } func ExecScript(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) (err error) { return nil } // FS is a copy of Go 1.16's io/fs.FS interface. type FS interface { Open(name string) (File, error) } // File is a copy of Go 1.16's io/fs.File interface. type File interface { Stat() (os.FileInfo, error) Read([]byte) (int, error) Close() error } zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlite/000077500000000000000000000000001501417116500336045ustar00rootroot00000000000000sqlitex/000077500000000000000000000000001501417116500352165ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlitestubs.go000066400000000000000000000014061501417116500367060ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlite/sqlitex// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Test stubs for zombiezen.com/go/sqlite/sqlitex. package sqlitex import ( "os" "zombiezen.com/go/sqlite" ) type ExecOptions struct { Args []interface{} Named map[string]interface{} ResultFunc func(stmt *sqlite.Stmt) error } func ExecScriptFS(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) error { return nil } func ExecuteScriptFS(conn *sqlite.Conn, fsys FS, filename string, opts *ExecOptions) error { return nil } // FS is a copy of Go 1.16's io/fs.FS interface. type FS interface { Open(name string) (File, error) } // File is a copy of Go 1.16's io/fs.File interface. type File interface { Stat() (os.FileInfo, error) Read([]byte) (int, error) Close() error } stubs.go000066400000000000000000000002361501417116500352150ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/cmd/zombiezen-sqlite-migrate/testdata/skeleton/zombiezen.com/go/sqlite// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Test stubs for zombiezen.com/go/sqlite. package sqlite import ( "os" ) type Conn struct{} zombiezen-go-sqlite-297af96/doc.go000066400000000000000000000100121501417116500170560ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC /* Package sqlite provides a Go interface to SQLite 3. The semantics of this package are deliberately close to the [SQLite3 C API]. See the [official C API introduction] for an overview of the basics. An SQLite connection is represented by a [*Conn]. Connections cannot be used concurrently. A typical Go program will create a pool of connections (e.g. by using [zombiezen.com/go/sqlite/sqlitex.NewPool] to create a [*zombiezen.com/go/sqlite/sqlitex.Pool]) so goroutines can borrow a connection while they need to talk to the database. This package assumes SQLite will be used concurrently by the process through several connections, so the build options for SQLite enable multi-threading and the [shared cache]. The implementation automatically handles shared cache locking, see the documentation on [Stmt.Step] for details. The optional SQLite 3 extensions compiled in are: session, FTS5, RTree, JSON1, and GeoPoly. This is not a [database/sql] driver. For helper functions to make it easier to execute statements, see the [zombiezen.com/go/sqlite/sqlitex] package. # Statement Caching Statements are prepared with the [Conn.Prepare] and [Conn.PrepareTransient] methods. When using [Conn.Prepare], statements are keyed inside a connection by the original query string used to create them. This means long-running high-performance code paths can write: stmt, err := conn.Prepare("SELECT ...") After all the connections in a pool have been warmed up by passing through one of these Prepare calls, subsequent calls are simply a map lookup that returns an existing statement. # Transactions SQLite transactions can be managed manually with this package by directly executing BEGIN / COMMIT / ROLLBACK or SAVEPOINT / RELEASE / ROLLBACK statements, but there are also helper functions available in [zombiezen.com/go/sqlite/sqlitex]: - [zombiezen.com/go/sqlite/sqlitex.Transaction] - [zombiezen.com/go/sqlite/sqlitex.ImmediateTransaction] - [zombiezen.com/go/sqlite/sqlitex.ExclusiveTransaction] - [zombiezen.com/go/sqlite/sqlitex.Save] # Schema Migrations For simple schema migration needs, see the [zombiezen.com/go/sqlite/sqlitemigration] package. # User-Defined Functions Use [Conn.CreateFunction] to register Go functions for use as [SQL functions]. # Streaming Blobs The sqlite package supports the SQLite incremental I/O interface for streaming blob data into and out of the the database without loading the entire blob into a single []byte. (This is important when working either with very large blobs, or more commonly, a large number of moderate-sized blobs concurrently.) See [Conn.OpenBlob] for more details. # Deadlines and Cancellation Every connection can have a done channel associated with it using the [Conn.SetInterrupt] method. This is typically the channel returned by a [context.Context.Done] method. As database connections are long-lived, the [Conn.SetInterrupt] method can be called multiple times to reset the associated lifetime. [official C API introduction]: https://www.sqlite.org/cintro.html [shared cache]: https://www.sqlite.org/sharedcache.html [SQL functions]: https://sqlite.org/appfunc.html [SQLite3 C API]: https://www.sqlite.org/c3ref/intro.html. */ package sqlite zombiezen-go-sqlite-297af96/example_test.go000066400000000000000000000216301501417116500210130ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite_test import ( "bytes" "context" "fmt" "io" "regexp" "time" "golang.org/x/text/collate" "golang.org/x/text/language" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func Example() { // Open an in-memory database. conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Execute a query. err = sqlitex.ExecuteTransient(conn, "SELECT 'hello, world';", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Println(stmt.ColumnText(0)) return nil }, }) if err != nil { // handle error } // Output: // hello, world } // This is the same as the main package example, but uses the SQLite // statement API instead of sqlitex. func Example_withoutX() { // Open an in-memory database. conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Prepare a statement. stmt, _, err := conn.PrepareTransient("SELECT 'hello, world';") if err != nil { // handle error } // Transient statements must always be finalized. defer stmt.Finalize() for { row, err := stmt.Step() if err != nil { // handle error } if !row { break } fmt.Println(stmt.ColumnText(0)) } // Output: // hello, world } func ExampleConn_SetInterrupt() { conn, err := sqlite.OpenConn(":memory:") if err != nil { panic(err) } defer conn.Close() // You can use the Done() channel from a context to set deadlines and timeouts // on queries. ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) defer cancel() conn.SetInterrupt(ctx.Done()) } // This example shows how to register a basic scalar function. // // If you're looking to use regular expressions in your application, // use [zombiezen.com/go/sqlite/ext/refunc.Register]. func ExampleConn_CreateFunction() { conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Add a regexp(pattern, string) function. err = conn.CreateFunction("regexp", &sqlite.FunctionImpl{ NArgs: 2, Deterministic: true, Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { re, err := regexp.Compile(args[0].Text()) if err != nil { return sqlite.Value{}, fmt.Errorf("regexp: %w", err) } found := 0 if re.MatchString(args[1].Text()) { found = 1 } return sqlite.IntegerValue(int64(found)), nil }, }) if err != nil { // handle error } matches, err := sqlitex.ResultBool(conn.Prep(`SELECT regexp('fo+', 'foo');`)) if err != nil { // handle error } fmt.Println("First matches:", matches) matches, err = sqlitex.ResultBool(conn.Prep(`SELECT regexp('fo+', 'bar');`)) if err != nil { // handle error } fmt.Println("Second matches:", matches) // Output: // First matches: true // Second matches: false } // This example shows the same regexp function as in the CreateFunction example, // but it uses auxiliary data to avoid recompiling the regular expression. // // This is the implementation used in [zombiezen.com/go/sqlite/ext/refunc]. func ExampleContext_AuxData() { conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Add a regexp(pattern, string) function. usedAux := false err = conn.CreateFunction("regexp", &sqlite.FunctionImpl{ NArgs: 2, Deterministic: true, Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { // First: attempt to retrieve the compiled regexp from a previous call. re, ok := ctx.AuxData(0).(*regexp.Regexp) if ok { usedAux = true } else { // Auxiliary data not present. Either this is the first call with this // argument, or SQLite has discarded the auxiliary data. var err error re, err = regexp.Compile(args[0].Text()) if err != nil { return sqlite.Value{}, fmt.Errorf("regexp: %w", err) } // Store the auxiliary data for future calls. ctx.SetAuxData(0, re) } found := 0 if re.MatchString(args[1].Text()) { found = 1 } return sqlite.IntegerValue(int64(found)), nil }, }) if err != nil { // handle error } const query = `WITH test_strings(i, s) AS (VALUES (1, 'foo'), (2, 'bar')) ` + `SELECT i, regexp('fo+', s) FROM test_strings ORDER BY i;` err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Printf("Match %d: %t\n", stmt.ColumnInt(0), stmt.ColumnInt(1) != 0) return nil }, }) if err != nil { // handle error } if usedAux { fmt.Println("Used aux data to speed up query!") } // Output: // Match 1: true // Match 2: false // Used aux data to speed up query! } func ExampleBlob() { // Create a new database with a "blobs" table with a single column, "myblob". conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() err = sqlitex.ExecuteTransient(conn, `CREATE TABLE blobs (myblob blob);`, nil) if err != nil { // handle error } // Insert a new row with enough space for the data we want to insert. const dataToInsert = "Hello, World!" err = sqlitex.ExecuteTransient( conn, `INSERT INTO blobs (myblob) VALUES (zeroblob(?));`, &sqlitex.ExecOptions{ Args: []any{len(dataToInsert)}, }, ) if err != nil { // handle error } // Open a handle to the "myblob" column on the row we just inserted. blob, err := conn.OpenBlob("", "blobs", "myblob", conn.LastInsertRowID(), true) if err != nil { // handle error } _, writeErr := blob.WriteString(dataToInsert) closeErr := blob.Close() if writeErr != nil { // handle error } if closeErr != nil { // handle error } // Read back the blob. var data []byte err = sqlitex.ExecuteTransient(conn, `SELECT myblob FROM blobs;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { data = make([]byte, stmt.ColumnLen(0)) stmt.ColumnBytes(0, data) return nil }, }) if err != nil { // handle error } fmt.Printf("%s\n", data) // Output: // Hello, World! } func ExampleConn_SetAuthorizer() { // Create a new database. conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Set an authorizer that prevents any mutations. err = conn.SetAuthorizer(sqlite.AuthorizeFunc(func(action sqlite.Action) sqlite.AuthResult { typ := action.Type() if typ == sqlite.OpSelect || typ == sqlite.OpRead || // Permit function calls. typ == sqlite.OpFunction || // Permit transactions. typ == sqlite.OpTransaction || typ == sqlite.OpSavepoint { return sqlite.AuthResultOK } return sqlite.AuthResultDeny })) if err != nil { // handle error } // Authorizers operate during statement preparation, so this will succeed: stmt, _, err := conn.PrepareTransient(`SELECT 'Hello, World!';`) if err != nil { panic(err) } else { fmt.Println("Read-only statement prepared!") if err := stmt.Finalize(); err != nil { panic(err) } } // But this will not: stmt, _, err = conn.PrepareTransient(`CREATE TABLE foo (id INTEGER PRIMARY KEY);`) if err != nil { fmt.Println("Prepare CREATE TABLE failed with code", sqlite.ErrCode(err)) } else if err := stmt.Finalize(); err != nil { panic(err) } // Output: // Read-only statement prepared! // Prepare CREATE TABLE failed with code SQLITE_AUTH } // This example shows how to use a changegroup to produce similar results to // a call to ConcatChangesets. func ExampleChangegroup() { // Get changesets from somewhere. var changeset1, changeset2 io.Reader // Create a changegroup. grp := new(sqlite.Changegroup) defer grp.Clear() // Add changesets to the changegroup. if err := grp.Add(changeset1); err != nil { // Handle error } if err := grp.Add(changeset2); err != nil { // Handle error } // Write the changegroup to a buffer. output := new(bytes.Buffer) if _, err := grp.WriteTo(output); err != nil { // Handle error } } func ExampleConn_SetCollation() { // Create a new database. conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle error } defer conn.Close() // Override the built-in NOCASE collating sequence // to be Unicode aware. nocaseCollator := collate.New(language.Und, collate.IgnoreCase) if err := conn.SetCollation("NOCASE", nocaseCollator.CompareString); err != nil { // handle error } // Create a table that uses the NOCASE collating sequence. err = sqlitex.ExecuteScript(conn, ` CREATE TABLE foo (mytext TEXT COLLATE NOCASE); INSERT INTO foo VALUES ('atext'), ('btext'), ('ctext'), ('ątext'), ('ćtext'); `, nil) if err != nil { // handle error } // The column will be implicitly ordered using its collating sequence. err = sqlitex.ExecuteTransient(conn, `SELECT mytext FROM foo ORDER BY mytext ASC;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Println(stmt.ColumnText(0)) return nil }, }) if err != nil { // handle error } // Output: // atext // ątext // btext // ctext // ćtext } zombiezen-go-sqlite-297af96/export_test.go000066400000000000000000000022701501417116500207000ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC // Expose sqlite internals to tests in sqlite_test package. package sqlite // TODO(maybe) // func ConnCount(conn *Conn) int { return conn.count } func InterruptedStmt(conn *Conn, query string) *Stmt { return &Stmt{ conn: conn, query: query, colNames: make(map[string]int), prepInterrupt: true, } } zombiezen-go-sqlite-297af96/ext/000077500000000000000000000000001501417116500165705ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/ext/generateseries/000077500000000000000000000000001501417116500215755ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/ext/generateseries/example_test.go000066400000000000000000000013371501417116500246220ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package generateseries_test import ( "fmt" "log" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/ext/generateseries" "zombiezen.com/go/sqlite/sqlitex" ) func Example() { conn, err := sqlite.OpenConn(":memory:") if err != nil { log.Fatal(err) } defer conn.Close() if err := generateseries.Register(conn); err != nil { log.Fatal(err) } err = sqlitex.ExecuteTransient( conn, `SELECT * FROM generate_series(0, 20, 5);`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Printf("%2d\n", stmt.ColumnInt(0)) return nil }, }, ) if err != nil { log.Fatal(err) } // Output: // 0 // 5 // 10 // 15 // 20 } zombiezen-go-sqlite-297af96/ext/generateseries/generateseries.go000066400000000000000000000154031501417116500251340ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC // Package generateseries provides a port of the [generate_series] table-valued function // from the SQLite tree. // // [generate_series]: https://sqlite.org/src/file/ext/misc/series.c package generateseries import ( "fmt" "zombiezen.com/go/sqlite" ) // Module is a virtual table module that can be registered with [sqlite.Conn.SetModule]. var Module = &sqlite.Module{ Connect: connect, } // Register registers the "generate_series" table-valued function on the given connection. func Register(c *sqlite.Conn) error { return c.SetModule("generate_series", Module) } type vtab struct{} const ( seriesColumnValue = iota seriesColumnStart seriesColumnStop seriesColumnStep ) func connect(c *sqlite.Conn, opts *sqlite.VTableConnectOptions) (sqlite.VTable, *sqlite.VTableConfig, error) { vtab := new(vtab) cfg := &sqlite.VTableConfig{ Declaration: "CREATE TABLE x(value,start hidden,stop hidden,step hidden)", AllowIndirect: true, } return vtab, cfg, nil } // BestIndex looks for equality constraints against the hidden start, stop, and step columns, // and if present, it uses those constraints to bound the sequence of generated values. // If the equality constraints are missing, it uses 0 for start, 4294967295 for stop, // and 1 for step. // BestIndex returns a small cost when both start and stop are available, // and a very large cost if either start or stop are unavailable. // This encourages the query planner to order joins such that the bounds of the // series are well-defined. // // SQLite will invoke this method one or more times // while planning a query that uses the generate_series virtual table. // This routine needs to create a query plan for each invocation // and compute an estimated cost for that plan. // // In this implementation ID.Num is used to represent the query plan. // ID.String is unused. // // The query plan is represented by bits in idxNum: // // (1) start = $value -- constraint exists // (2) stop = $value -- constraint exists // (4) step = $value -- constraint exists // (8) output in descending order func (vt *vtab) BestIndex(inputs *sqlite.IndexInputs) (*sqlite.IndexOutputs, error) { var idxNum int32 startSeen := false var unusableMask uint aIdx := [3]int{-1, -1, -1} for i, c := range inputs.Constraints { if c.Column < seriesColumnStart { continue } col := c.Column - seriesColumnStart // [0, 2] mask := uint(1 << col) if col == 0 { startSeen = true } if !c.Usable { unusableMask |= mask continue } if c.Op == sqlite.IndexConstraintEq { idxNum |= int32(mask) aIdx[col] = i } } outputs := &sqlite.IndexOutputs{ ID: sqlite.IndexID{Num: idxNum}, ConstraintUsage: make([]sqlite.IndexConstraintUsage, len(inputs.Constraints)), } nArg := 0 for _, j := range aIdx { if j >= 0 { nArg++ outputs.ConstraintUsage[j] = sqlite.IndexConstraintUsage{ ArgvIndex: nArg, Omit: true, } } } if !startSeen { return nil, fmt.Errorf("first argument to \"generate_series()\" missing or unusable") } if unusableMask&^uint(idxNum) != 0 { // The start, stop, and step columns are inputs. // Therefore if there are unusable constraints on any of start, stop, or step then // this plan is unusable. return nil, sqlite.ResultConstraint.ToError() } if idxNum&3 == 3 { // Both start= and stop= boundaries are available. // This is the preferred case. if idxNum&4 != 0 { outputs.EstimatedCost = 1 } else { outputs.EstimatedCost = 2 } outputs.EstimatedRows = 1000 if len(inputs.OrderBy) >= 1 && inputs.OrderBy[0].Column == 0 { if inputs.OrderBy[0].Desc { idxNum |= 8 } else { idxNum |= 16 } outputs.OrderByConsumed = true } } else { // If either boundary is missing, we have to generate a huge span of numbers. // Make this case very expensive so that the query planner will work hard to avoid it. outputs.EstimatedRows = 2147483647 } return outputs, nil } func (vt *vtab) Open() (sqlite.VTableCursor, error) { return new(cursor), nil } func (vt *vtab) Disconnect() error { return nil } func (vt *vtab) Destroy() error { return nil } type cursor struct { isDesc bool rowid int64 value int64 mnValue int64 mxValue int64 step int64 } // Filter is called to "rewind" the cursor object back to the first row of output. // This method is always called at least once // prior to any call to Column or RowID or EOF. // // The query plan selected by BestIndex is passed in the id parameter. // (id.String is not used in this implementation.) // id.Num is a bitmask showing which constraints are available: // // 1: start=VALUE // 2: stop=VALUE // 4: step=VALUE // // Also, if bit 8 is set, that means that the series should be output in descending order // rather than in ascending order. // If bit 16 is set, then output must appear in ascending order. // // This routine should initialize the cursor and position it // so that it is pointing at the first row, // or pointing off the end of the table (so that EOF will return true) // if the table is empty. func (cur *cursor) Filter(id sqlite.IndexID, argv []sqlite.Value) error { i := 0 if id.Num&1 != 0 { cur.mnValue = argv[i].Int64() i++ } else { cur.mnValue = 0 } if id.Num&2 != 0 { cur.mxValue = argv[i].Int64() i++ } else { cur.mxValue = 0xffffffff } if id.Num&4 != 0 { cur.step = argv[i].Int64() i++ if cur.step == 0 { cur.step = 1 } else if cur.step < 0 { cur.step = -cur.step if id.Num&16 == 0 { id.Num |= 8 } } } else { cur.step = 1 } for _, arg := range argv { if arg.Type() == sqlite.TypeNull { // If any of the constraints have a NULL value, then return no rows. // See ticket https://www.sqlite.org/src/info/fac496b61722daf2 cur.mnValue = 1 cur.mxValue = 0 break } } if id.Num&8 != 0 { cur.isDesc = true cur.value = cur.mxValue if cur.step > 0 { cur.value -= (cur.mxValue - cur.mnValue) % cur.step } } else { cur.isDesc = false cur.value = cur.mnValue } cur.rowid = 1 return nil } func (cur *cursor) Next() error { if cur.isDesc { cur.value -= cur.step } else { cur.value += cur.step } cur.rowid++ return nil } func (cur *cursor) Column(i int, noChange bool) (sqlite.Value, error) { switch i { case seriesColumnValue: return sqlite.IntegerValue(cur.value), nil case seriesColumnStart: return sqlite.IntegerValue(cur.mnValue), nil case seriesColumnStop: return sqlite.IntegerValue(cur.mxValue), nil case seriesColumnStep: return sqlite.IntegerValue(cur.step), nil default: panic("unreachable") } } func (cur *cursor) RowID() (int64, error) { return cur.rowid, nil } func (cur *cursor) EOF() bool { if cur.isDesc { return cur.value < cur.mnValue } else { return cur.value > cur.mxValue } } func (cur *cursor) Close() error { return nil } zombiezen-go-sqlite-297af96/ext/refunc/000077500000000000000000000000001501417116500200525ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/ext/refunc/refunc.go000066400000000000000000000025041501417116500216640ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC // Package refunc provides an implementation of the [REGEXP operator] // that uses the Go [regexp] package. // // [REGEXP operator]: https://sqlite.org/lang_expr.html#the_like_glob_regexp_match_and_extract_operators package refunc import ( "fmt" "regexp" "zombiezen.com/go/sqlite" ) // Impl is the implementation of the REGEXP function. var Impl = &sqlite.FunctionImpl{ NArgs: 2, Deterministic: true, AllowIndirect: true, Scalar: regexpFunc, } // Register registers the "regexp" function on the given connection. func Register(c *sqlite.Conn) error { return c.CreateFunction("regexp", Impl) } func regexpFunc(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { // First: attempt to retrieve the compiled regexp from a previous call. re, ok := ctx.AuxData(0).(*regexp.Regexp) if !ok { // Auxiliary data not present. Either this is the first call with this // argument, or SQLite has discarded the auxiliary data. var err error re, err = regexp.Compile(args[0].Text()) if err != nil { return sqlite.Value{}, fmt.Errorf("regexp: %w", err) } // Store the auxiliary data for future calls. ctx.SetAuxData(0, re) } found := 0 if re.MatchString(args[1].Text()) { found = 1 } return sqlite.IntegerValue(int64(found)), nil } zombiezen-go-sqlite-297af96/ext/refunc/refunc_test.go000066400000000000000000000022241501417116500227220ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package refunc import ( "testing" "zombiezen.com/go/sqlite" ) func TestImpl(t *testing.T) { c, err := sqlite.OpenConn("", sqlite.OpenMemory|sqlite.OpenReadWrite) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() if err := Register(c); err != nil { t.Error("Register:", err) } tests := []struct { x, y string want bool }{ {"", "foo", false}, {"foo", "", true}, {"foo", "^fo*$", true}, {"bar", "^fo*$", false}, } stmt := c.Prep("VALUES (:x REGEXP :y);") for _, test := range tests { stmt.SetText(":x", test.x) stmt.SetText(":y", test.y) rowReturned, err := stmt.Step() if err != nil { t.Errorf("%q REGEXP %q: %v", test.x, test.y, err) stmt.Reset() continue } if !rowReturned { t.Errorf("%q REGEXP %q: no row returned", test.x, test.y) stmt.Reset() continue } if got := stmt.ColumnBool(0); got != test.want { t.Errorf("%q REGEXP %q = %t; want %t", test.x, test.y, got, test.want) } if err := stmt.Reset(); err != nil { t.Errorf("%q REGEXP %q: %v", test.x, test.y, err) } } } zombiezen-go-sqlite-297af96/flake.lock000066400000000000000000000026021501417116500177240ustar00rootroot00000000000000{ "nodes": { "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1701680307, "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { "id": "flake-utils", "type": "indirect" } }, "nixpkgs": { "locked": { "lastModified": 1746152631, "narHash": "sha256-zBuvmL6+CUsk2J8GINpyy8Hs1Zp4PP6iBWSmZ4SCQ/s=", "owner": "NixOS", "repo": "nixpkgs", "rev": "032bc6539bd5f14e9d0c51bd79cfe9a055b094c3", "type": "github" }, "original": { "id": "nixpkgs", "type": "indirect" } }, "root": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } zombiezen-go-sqlite-297af96/flake.nix000066400000000000000000000010541501417116500175720ustar00rootroot00000000000000{ description = "zombiezen.com/go/sqlite"; inputs = { nixpkgs.url = "nixpkgs"; flake-utils.url = "flake-utils"; }; outputs = { nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { devShells.default = pkgs.mkShell { packages = [ pkgs.go-tools # staticcheck pkgs.go_1_24 pkgs.gotools # godoc, etc. ]; hardeningDisable = [ "fortify" ]; }; } ); } zombiezen-go-sqlite-297af96/func.go000066400000000000000000000551401501417116500172570ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "errors" "fmt" "math" "math/bits" "strconv" "strings" "sync" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" lib "modernc.org/sqlite/lib" ) var auxdata struct { mu sync.RWMutex m map[uintptr]any ids idGen } // Context is a SQL function execution context. // It is in no way related to a Go context.Context. // https://sqlite.org/c3ref/context.html type Context struct { tls *libc.TLS ptr uintptr } // Conn returns the database connection that is calling the SQL function. func (ctx Context) Conn() *Conn { connPtr := lib.Xsqlite3_context_db_handle(ctx.tls, ctx.ptr) allConns.mu.RLock() defer allConns.mu.RUnlock() return allConns.table[connPtr] } // AuxData returns the auxiliary data associated with the given argument, with // zero being the leftmost argument, or nil if no such data is present. // // Auxiliary data may be used by (non-aggregate) SQL functions to associate // metadata with argument values. If the same value is passed to multiple // invocations of the same SQL function during query execution, under some // circumstances the associated metadata may be preserved. An example of where // this might be useful is in a regular-expression matching function. The // compiled version of the regular expression can be stored as metadata // associated with the pattern string. Then as long as the pattern string // remains the same, the compiled regular expression can be reused on multiple // invocations of the same function. // // For more details, see https://www.sqlite.org/c3ref/get_auxdata.html func (ctx Context) AuxData(arg int) any { id := lib.Xsqlite3_get_auxdata(ctx.tls, ctx.ptr, int32(arg)) if id == 0 { return nil } auxdata.mu.RLock() defer auxdata.mu.RUnlock() return auxdata.m[id] } // SetAuxData sets the auxiliary data associated with the given argument, with // zero being the leftmost argument. SQLite is free to discard the metadata at // any time, including during the call to SetAuxData. // // Auxiliary data may be used by (non-aggregate) SQL functions to associate // metadata with argument values. If the same value is passed to multiple // invocations of the same SQL function during query execution, under some // circumstances the associated metadata may be preserved. An example of where // this might be useful is in a regular-expression matching function. The // compiled version of the regular expression can be stored as metadata // associated with the pattern string. Then as long as the pattern string // remains the same, the compiled regular expression can be reused on multiple // invocations of the same function. // // For more details, see https://www.sqlite.org/c3ref/get_auxdata.html func (ctx Context) SetAuxData(arg int, data any) { auxdata.mu.Lock() id := auxdata.ids.next() if auxdata.m == nil { auxdata.m = make(map[uintptr]any) } auxdata.m[id] = data auxdata.mu.Unlock() deleteFn := cFuncPointer(freeAuxData) lib.Xsqlite3_set_auxdata(ctx.tls, ctx.ptr, int32(arg), id, deleteFn) } func freeAuxData(tls *libc.TLS, id uintptr) { auxdata.mu.Lock() defer auxdata.mu.Unlock() delete(auxdata.m, id) auxdata.ids.reclaim(id) } func (ctx Context) result(v Value, err error) { if err != nil { ctx.resultError(err) return } if v.tls != nil { if ctx.tls != v.tls { ctx.resultError(fmt.Errorf("function result Value from different connection")) return } lib.Xsqlite3_result_value(ctx.tls, ctx.ptr, v.ptrOrType) return } switch ColumnType(v.ptrOrType) { case 0, TypeNull: lib.Xsqlite3_result_null(ctx.tls, ctx.ptr) case TypeInteger: lib.Xsqlite3_result_int64(ctx.tls, ctx.ptr, v.n) case TypeFloat: lib.Xsqlite3_result_double(ctx.tls, ctx.ptr, v.float()) case TypeText: if len(v.s) == 0 { lib.Xsqlite3_result_text(ctx.tls, ctx.ptr, emptyCString, 0, sqliteStatic) } else { cv, err := libc.CString(v.s) if err != nil { ctx.resultError(fmt.Errorf("alloc function result: %w", err)) return } lib.Xsqlite3_result_text(ctx.tls, ctx.ptr, cv, int32(len(v.s)), freeFuncPtr) } case TypeBlob: if len(v.s) == 0 { lib.Xsqlite3_result_blob(ctx.tls, ctx.ptr, emptyCString, 0, sqliteStatic) } else { cv, err := malloc(ctx.tls, types.Size_t(len(v.s))) if err != nil { ctx.resultError(fmt.Errorf("alloc function result: %w", err)) return } copy(libc.GoBytes(cv, len(v.s)), v.s) lib.Xsqlite3_result_blob(ctx.tls, ctx.ptr, cv, int32(len(v.s)), freeFuncPtr) } default: panic("unknown result Value type") } } func (ctx Context) resultError(err error) { errstr := err.Error() cerrstr, err := libc.CString(errstr) if err != nil { panic(err) } defer libc.Xfree(ctx.tls, cerrstr) lib.Xsqlite3_result_error(ctx.tls, ctx.ptr, cerrstr, int32(len(errstr))) lib.Xsqlite3_result_error_code(ctx.tls, ctx.ptr, int32(ErrCode(err))) } // Value represents a value that can be stored in a database table. // The zero value is NULL. // The accessor methods on Value may perform automatic conversions // and thus methods on Value must not be called concurrently. type Value struct { tls *libc.TLS ptrOrType uintptr // pointer to sqlite_value if tls != nil, ColumnType otherwise s string n int64 // if ptrOrType == 0 and n != 0, then indicates the "nochange" NULL. } // IntegerValue returns a new Value representing the given integer. func IntegerValue(i int64) Value { return Value{ptrOrType: uintptr(TypeInteger), n: i} } // FloatValue returns a new Value representing the given floating-point number. func FloatValue(f float64) Value { return Value{ptrOrType: uintptr(TypeFloat), n: int64(math.Float64bits(f))} } // TextValue returns a new Value representing the given string. func TextValue(s string) Value { return Value{ptrOrType: uintptr(TypeText), s: s} } // BlobValue returns a new blob Value, copying the bytes from the given // byte slice. func BlobValue(b []byte) Value { return Value{ptrOrType: uintptr(TypeBlob), s: string(b)} } // Unchanged returns a NULL Value for which [Value.NoChange] reports true. // This is only significant as the return value for the Column method of [VTableCursor]. func Unchanged() Value { return Value{n: 1} } // Type returns the data type of the value. The result of Type is undefined if // an automatic type conversion has occurred due to calling one of the other // accessor methods. func (v Value) Type() ColumnType { if v.ptrOrType == 0 { return TypeNull } if v.tls == nil { return ColumnType(v.ptrOrType) } return ColumnType(lib.Xsqlite3_value_type(v.tls, v.ptrOrType)) } // Conversions follow the table in https://sqlite.org/c3ref/column_blob.html // Int returns the value as an integer. func (v Value) Int() int { return int(v.Int64()) } // Int64 returns the value as a 64-bit integer. func (v Value) Int64() int64 { if v.ptrOrType == 0 { return 0 } if v.tls == nil { switch ColumnType(v.ptrOrType) { case TypeNull: return 0 case TypeInteger: return v.n case TypeFloat: return int64(v.float()) case TypeBlob, TypeText: return castTextToInteger(v.s) default: panic("unknown value type") } } return int64(lib.Xsqlite3_value_int64(v.tls, v.ptrOrType)) } // castTextToInteger emulates the SQLite CAST operator for a TEXT value to // INTEGER, as documented in https://sqlite.org/lang_expr.html#castexpr func castTextToInteger(s string) int64 { const digits = "0123456789" s = strings.TrimSpace(s) if len(s) > 0 && (s[0] == '+' || s[0] == '-') { s = s[:1+len(longestPrefix(s[1:], digits))] } else { s = longestPrefix(s, digits) } n, _ := strconv.ParseInt(s, 10, 64) return n } func longestPrefix(s string, allowSet string) string { sloop: for i := 0; i < len(s); i++ { for j := 0; j < len(allowSet); j++ { if s[i] == allowSet[j] { continue sloop } } return s[:i] } return s } // Float returns the value as floating-point number func (v Value) Float() float64 { if v.ptrOrType == 0 { return 0 } if v.tls == nil { switch ColumnType(v.ptrOrType) { case TypeNull: return 0 case TypeInteger: return float64(v.n) case TypeFloat: return v.float() case TypeBlob, TypeText: return castTextToReal(v.s) default: panic("unknown value type") } } return float64(lib.Xsqlite3_value_double(v.tls, v.ptrOrType)) } func (v Value) float() float64 { return math.Float64frombits(uint64(v.n)) } // castTextToReal emulates the SQLite CAST operator for a TEXT value to // REAL, as documented in https://sqlite.org/lang_expr.html#castexpr func castTextToReal(s string) float64 { s = strings.TrimSpace(s) for ; len(s) > 0; s = s[:len(s)-1] { n, err := strconv.ParseFloat(s, 64) if !errors.Is(err, strconv.ErrSyntax) { return n } } return 0 } // Text returns the value as a string. func (v Value) Text() string { if v.ptrOrType == 0 { return "" } if v.tls == nil { switch ColumnType(v.ptrOrType) { case TypeNull: return "" case TypeInteger: return strconv.FormatInt(v.n, 10) case TypeFloat: return strconv.FormatFloat(v.float(), 'g', -1, 64) case TypeText, TypeBlob: return v.s default: panic("unknown value type") } } ptr := lib.Xsqlite3_value_text(v.tls, v.ptrOrType) return goStringN(ptr, int(lib.Xsqlite3_value_bytes(v.tls, v.ptrOrType))) } // Blob returns a copy of the value as a blob. func (v Value) Blob() []byte { if v.ptrOrType == 0 { return nil } if v.tls == nil { switch ColumnType(v.ptrOrType) { case TypeNull: return nil case TypeInteger: return strconv.AppendInt(nil, v.n, 10) case TypeFloat: return strconv.AppendFloat(nil, v.float(), 'g', -1, 64) case TypeBlob, TypeText: return []byte(v.s) default: panic("unknown value type") } } ptr := lib.Xsqlite3_value_blob(v.tls, v.ptrOrType) return libc.GoBytes(ptr, int(lib.Xsqlite3_value_bytes(v.tls, v.ptrOrType))) } // NoChange reports whether a column // corresponding to this value in a [VTable] Update method // is unchanged by the UPDATE operation // that the Update method call was invoked to implement // and if the prior [VTableCursor] Column method call that was invoked // to extract the value for that column returned [Unchanged]. func (v Value) NoChange() bool { if v.ptrOrType == 0 { return v.n != 0 } if v.tls == nil { return false } return lib.Xsqlite3_value_nochange(v.tls, v.ptrOrType) != 0 } // FunctionImpl describes an [application-defined SQL function]. // Either Scalar or MakeAggregate must be set, but not both. // // [application-defined SQL function]: https://sqlite.org/appfunc.html type FunctionImpl struct { // NArgs is the required number of arguments that the function accepts. // If NArgs is negative, then the function is variadic. // // Multiple function implementations may be registered with the same name // with different numbers of required arguments. NArgs int // Scalar is called when a scalar function is invoked in SQL. // The argument Values are not valid past the return of the function. Scalar func(ctx Context, args []Value) (Value, error) // MakeAggregate is called at the beginning of an evaluation of an aggregate function. MakeAggregate func(ctx Context) (AggregateFunction, error) // If Deterministic is true, the function must always give the same output // when the input parameters are the same. This enables functions to be used // in additional contexts like the WHERE clause of partial indexes and enables // additional optimizations. // // See https://sqlite.org/c3ref/c_deterministic.html#sqlitedeterministic for // more details. Deterministic bool // If AllowIndirect is false, then the function may only be invoked from // top-level SQL. If AllowIndirect is true, then the function can be used in // VIEWs, TRIGGERs, and schema structures (e.g. CHECK constraints and DEFAULT // clauses). // // This is the inverse of SQLITE_DIRECTONLY. See // https://sqlite.org/c3ref/c_deterministic.html#sqlitedirectonly for more // details. This defaults to false for better security. AllowIndirect bool } // An AggregateFunction is an invocation of an aggregate function. // See the documentation for [aggregate function callbacks] // and [application-defined window functions] for an overview. // // [aggregate function callbacks]: https://www.sqlite.org/appfunc.html#the_aggregate_function_callbacks // [application-defined window functions]: https://www.sqlite.org/windowfunctions.html#user_defined_aggregate_window_functions type AggregateFunction interface { // Step is called for each row // of an aggregate function's SQL invocation. // The argument Values are not valid past the return of the function. Step(ctx Context, rowArgs []Value) error // WindowInverse is called to remove // the oldest presently aggregated result of Step // from the current window. // The arguments are those passed to Step for the row being removed. // The argument Values are not valid past the return of the function. WindowInverse(ctx Context, rowArgs []Value) error // WindowValue is called to get the current value of an aggregate function. WindowValue(ctx Context) (Value, error) // Finalize is called after all of the aggregate function's input rows // have been stepped through. // No other methods will be called on the AggregateFunction after calling Finalize. Finalize(ctx Context) } // CreateFunction registers a Go function with SQLite // for use in SQL queries. // // https://sqlite.org/appfunc.html func (c *Conn) CreateFunction(name string, impl *FunctionImpl) error { if c == nil { return fmt.Errorf("sqlite: create function: nil connection") } if name == "" { return fmt.Errorf("sqlite: create function: no name provided") } if impl.NArgs > 127 { return fmt.Errorf("sqlite: create function %s: too many permitted arguments (%d)", name, impl.NArgs) } if impl.Scalar == nil && impl.MakeAggregate == nil { return fmt.Errorf("sqlite: create function %s: must specify one of Scalar or MakeAggregate", name) } if impl.Scalar != nil && impl.MakeAggregate != nil { return fmt.Errorf("sqlite: create function %s: both Scalar and MakeAggregate specified", name) } cname, err := libc.CString(name) if err != nil { return fmt.Errorf("sqlite: create function %s: %w", name, err) } defer libc.Xfree(c.tls, cname) eTextRep := int32(lib.SQLITE_UTF8) if impl.Deterministic { eTextRep |= lib.SQLITE_DETERMINISTIC } if !impl.AllowIndirect { eTextRep |= lib.SQLITE_DIRECTONLY } numArgs := impl.NArgs if numArgs < 0 { numArgs = -1 } var res ResultCode if impl.Scalar != nil { xfuncs.mu.Lock() id := xfuncs.ids.next() xfuncs.m[id] = impl.Scalar xfuncs.mu.Unlock() res = ResultCode(lib.Xsqlite3_create_function_v2( c.tls, c.conn, cname, int32(numArgs), eTextRep, id, cFuncPointer(funcTrampoline), 0, 0, cFuncPointer(destroyScalarFunc), )) } else { xAggregateFactories.mu.Lock() id := xAggregateFactories.ids.next() xAggregateFactories.m[id] = impl.MakeAggregate xAggregateFactories.mu.Unlock() res = ResultCode(lib.Xsqlite3_create_window_function( c.tls, c.conn, cname, int32(numArgs), eTextRep, id, cFuncPointer(stepTrampoline), cFuncPointer(finalTrampoline), cFuncPointer(valueTrampoline), cFuncPointer(inverseTrampoline), cFuncPointer(destroyAggregateFunc), )) } if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: create function %s: %w", name, err) } return nil } var xfuncs = struct { mu sync.RWMutex m map[uintptr]func(Context, []Value) (Value, error) ids idGen }{ m: make(map[uintptr]func(Context, []Value) (Value, error)), } func funcTrampoline(tls *libc.TLS, ctx uintptr, n int32, valarray uintptr) { id := lib.Xsqlite3_user_data(tls, ctx) xfuncs.mu.RLock() x := xfuncs.m[id] xfuncs.mu.RUnlock() vals := make([]Value, 0, int(n)) for ; len(vals) < cap(vals); valarray += uintptr(ptrSize) { vals = append(vals, Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(valarray)), }) } goCtx := Context{tls: tls, ptr: ctx} goCtx.result(x(goCtx, vals)) } func destroyScalarFunc(tls *libc.TLS, id uintptr) { xfuncs.mu.Lock() defer xfuncs.mu.Unlock() delete(xfuncs.m, id) xfuncs.ids.reclaim(id) } var ( xAggregateFactories = struct { mu sync.RWMutex m map[uintptr]func(Context) (AggregateFunction, error) ids idGen }{ m: make(map[uintptr]func(Context) (AggregateFunction, error)), } xAggregateContext = struct { mu sync.RWMutex m map[uintptr]AggregateFunction ids idGen }{ m: make(map[uintptr]AggregateFunction), } ) func makeAggregate(tls *libc.TLS, ctx uintptr) (AggregateFunction, uintptr) { goCtx := Context{tls: tls, ptr: ctx} aggCtx := (*uintptr)(unsafe.Pointer(lib.Xsqlite3_aggregate_context(tls, ctx, int32(ptrSize)))) if aggCtx == nil { goCtx.resultError(errors.New("insufficient memory for aggregate")) return nil, 0 } if *aggCtx != 0 { // Already created. xAggregateContext.mu.RLock() f := xAggregateContext.m[*aggCtx] xAggregateContext.mu.RUnlock() return f, *aggCtx } factoryID := lib.Xsqlite3_user_data(tls, ctx) xAggregateFactories.mu.RLock() factory := xAggregateFactories.m[factoryID] xAggregateFactories.mu.RUnlock() f, err := factory(goCtx) if err != nil { goCtx.resultError(err) return nil, 0 } if f == nil { goCtx.resultError(errors.New("MakeAggregate function returned nil")) return nil, 0 } xAggregateContext.mu.Lock() *aggCtx = xAggregateContext.ids.next() xAggregateContext.m[*aggCtx] = f xAggregateContext.mu.Unlock() return f, *aggCtx } func stepTrampoline(tls *libc.TLS, ctx uintptr, n int32, valarray uintptr) { x, _ := makeAggregate(tls, ctx) if x == nil { return } vals := make([]Value, 0, int(n)) for ; len(vals) < cap(vals); valarray += uintptr(ptrSize) { vals = append(vals, Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(valarray)), }) } goCtx := Context{tls: tls, ptr: ctx} if err := x.Step(goCtx, vals); err != nil { goCtx.resultError(err) } } func finalTrampoline(tls *libc.TLS, ctx uintptr) { x, id := makeAggregate(tls, ctx) if x == nil { return } goCtx := Context{tls: tls, ptr: ctx} goCtx.result(x.WindowValue(goCtx)) x.Finalize(goCtx) xAggregateContext.mu.Lock() defer xAggregateContext.mu.Unlock() delete(xAggregateContext.m, id) xAggregateContext.ids.reclaim(id) } func valueTrampoline(tls *libc.TLS, ctx uintptr) { x, _ := makeAggregate(tls, ctx) if x == nil { return } goCtx := Context{tls: tls, ptr: ctx} goCtx.result(x.WindowValue(goCtx)) } func inverseTrampoline(tls *libc.TLS, ctx uintptr, n int32, valarray uintptr) { x, _ := makeAggregate(tls, ctx) if x == nil { return } vals := make([]Value, 0, int(n)) for ; len(vals) < cap(vals); valarray += uintptr(ptrSize) { vals = append(vals, Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(valarray)), }) } goCtx := Context{tls: tls, ptr: ctx} if err := x.WindowInverse(goCtx, vals); err != nil { goCtx.resultError(err) } } func destroyAggregateFunc(tls *libc.TLS, id uintptr) { xAggregateFactories.mu.Lock() defer xAggregateFactories.mu.Unlock() delete(xAggregateFactories.m, id) xAggregateFactories.ids.reclaim(id) } // CollatingFunc is a [collating function/sequence], // that is, a function that compares two strings. // The function returns a negative number if a < b, // a positive number if a > b, // or zero if a == b. // A collating function must always return the same answer given the same inputs. // The collating function must obey the following properties for all strings A, B, and C: // // 1. If A==B then B==A. // 2. If A==B and B==C then A==C. // 3. If AA. // 4. If A 0: return 1 case x < 0: return -1 default: return 0 } } func destroyCollation(tls *libc.TLS, id uintptr) { xcollations.mu.Lock() defer xcollations.mu.Unlock() delete(xcollations.m, id) xcollations.ids.reclaim(id) } // idGen is an ID generator. The zero value is ready to use. type idGen struct { bitset []uint64 } func (gen *idGen) next() uintptr { base := uintptr(1) for i := 0; i < len(gen.bitset); i, base = i+1, base+64 { b := gen.bitset[i] if b != 1<<64-1 { n := uintptr(bits.TrailingZeros64(^b)) gen.bitset[i] |= 1 << n return base + n } } gen.bitset = append(gen.bitset, 1) return base } func (gen *idGen) reclaim(id uintptr) { bit := id - 1 gen.bitset[bit/64] &^= 1 << (bit % 64) } zombiezen-go-sqlite-297af96/func_test.go000066400000000000000000000237171501417116500203230ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "strings" "testing" "unsafe" "github.com/google/go-cmp/cmp" "modernc.org/libc" ) func TestFunc(t *testing.T) { c, err := OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = c.CreateFunction("addints", &FunctionImpl{ NArgs: 2, Deterministic: true, Scalar: func(ctx Context, args []Value) (Value, error) { if got := ctx.Conn(); got != c { t.Errorf("ctx.Conn() = %p; want %p", got, c) } return IntegerValue(args[0].Int64() + args[1].Int64()), nil }, }) if err != nil { t.Fatal(err) } stmt, _, err := c.PrepareTransient("SELECT addints(2, 3);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } if got, want := stmt.ColumnInt(0), 5; got != want { t.Errorf("addints(2, 3)=%d, want %d", got, want) } stmt.Finalize() } func TestAggFunc(t *testing.T) { c, err := OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, _, err := c.PrepareTransient("CREATE TABLE t (c integer);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } if err := stmt.Finalize(); err != nil { t.Error(err) } cVals := []int{3, 5, 7} want := 3 + 5 + 7 stmt, err = c.Prepare("INSERT INTO t (c) VALUES ($c);") if err != nil { t.Fatal(err) } defer stmt.Finalize() for _, val := range cVals { stmt.SetInt64("$c", int64(val)) if _, err = stmt.Step(); err != nil { t.Errorf("INSERT %q: %v", val, err) } if err = stmt.Reset(); err != nil { t.Errorf("INSERT reset %q: %v", val, err) } } finalCalled := false err = c.CreateFunction("sumints", &FunctionImpl{ NArgs: 1, Deterministic: true, AllowIndirect: true, MakeAggregate: func(ctx Context) (AggregateFunction, error) { return &sumintsFunction{finalCalled: &finalCalled}, nil }, }) if err != nil { t.Fatal(err) } stmt, _, err = c.PrepareTransient("SELECT sumints(c) FROM t;") if err != nil { t.Fatal(err) } defer stmt.Finalize() if _, err := stmt.Step(); err != nil { t.Fatal(err) } if got := stmt.ColumnInt(0); got != want { t.Errorf("sum(c)=%d, want %d", got, want) } if !finalCalled { t.Error("xFinal not called") } } // Equivalent of https://www.sqlite.org/windowfunctions.html#user_defined_aggregate_window_functions func TestWindowFunc(t *testing.T) { c, err := OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, _, err := c.PrepareTransient("CREATE TABLE t3 (x, y);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } if err := stmt.Finalize(); err != nil { t.Error(err) } stmt, err = c.Prepare("INSERT INTO t3 VALUES ('a', 4), " + "('b', 5), " + "('c', 3), " + "('d', 8), " + "('e', 1);") if err != nil { t.Fatal(err) } defer stmt.Finalize() if _, err := stmt.Step(); err != nil { t.Errorf("INSERT: %v", err) } finalCalled := false err = c.CreateFunction("sumint", &FunctionImpl{ NArgs: 1, Deterministic: true, AllowIndirect: true, MakeAggregate: func(ctx Context) (AggregateFunction, error) { return &sumintsFunction{finalCalled: &finalCalled}, nil }, }) if err != nil { t.Fatal(err) } stmt, _, err = c.PrepareTransient("SELECT x, sumint(y) OVER (" + "ORDER BY x ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING" + ") AS sum_y " + "FROM t3 ORDER BY x;") if err != nil { t.Fatal(err) } defer stmt.Finalize() type row struct { x string sumY int64 } var got []row for { hasData, err := stmt.Step() if err != nil { t.Error("SELECT:", err) break } if !hasData { break } got = append(got, row{ stmt.ColumnText(0), stmt.ColumnInt64(1), }) } want := []row{ {"a", 9}, {"b", 12}, {"c", 16}, {"d", 12}, {"e", 9}, } if diff := cmp.Diff(want, got, cmp.AllowUnexported(row{})); diff != "" { t.Errorf("-want +got:\n%s", diff) } if !finalCalled { t.Error("xFinal not called") } } type sumintsFunction struct { sum int64 finalCalled *bool } func (f *sumintsFunction) Step(ctx Context, args []Value) error { if args[0].Type() != TypeInteger { return fmt.Errorf("invalid argument") } f.sum += args[0].Int64() return nil } func (f *sumintsFunction) WindowInverse(ctx Context, args []Value) error { f.sum -= args[0].Int64() return nil } func (f *sumintsFunction) WindowValue(ctx Context) (Value, error) { return IntegerValue(f.sum), nil } func (f *sumintsFunction) Finalize(ctx Context) { *f.finalCalled = true } func TestCastTextToInteger(t *testing.T) { tests := []struct { text string want int64 }{ { text: "abc", want: 0, }, { text: "123", want: 123, }, { text: " 123 ", want: 123, }, { text: "+123", want: 123, }, { text: "-123", want: -123, }, { text: "123e+5", want: 123, }, { text: "0x123", want: 0, }, { text: "9223372036854775808", want: 9223372036854775807, }, { text: "-9223372036854775809", want: -9223372036854775808, }, } for _, test := range tests { if got := castTextToInteger(test.text); got != test.want { t.Errorf("castTextToInteger(%q) = %d; want %d", test.text, got, test.want) } } } func TestCastTextToReal(t *testing.T) { tests := []struct { text string want float64 }{ { text: "abc", want: 0, }, { text: "123", want: 123, }, { text: "123.45", want: 123.45, }, { text: " 123.45 ", want: 123.45, }, { text: "+123", want: 123, }, { text: "-123", want: -123, }, { text: "123e+5", want: 123e+5, }, { text: "123.45xxx", want: 123.45, }, { text: "0x123", want: 0, }, { text: "9223372036854775808", want: 9223372036854775808, }, { text: "-9223372036854775809", want: -9223372036854775809, }, } for _, test := range tests { if got := castTextToReal(test.text); got != test.want { t.Errorf("castTextToReal(%q) = %g; want %g", test.text, got, test.want) } } } func TestSetCollation(t *testing.T) { c, err := OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = c.SetCollation("foo", func(a, b string) int { return -strings.Compare(a, b) }) if err != nil { t.Error(err) } stmt := c.Prep(`select ?1 < ?2 collate foo, ?1 = ?2 collate foo, ?1 > ?2 collate foo;`) defer func() { if err := stmt.Finalize(); err != nil { t.Error(err) } }() tests := []struct { name string a string b string lt bool eq bool gt bool }{ {"Greater", "abc", "def", false, false, true}, {"Less", "def", "abc", true, false, false}, {"Equal", "abc", "abc", false, true, false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if err := stmt.Reset(); err != nil { t.Error(err) } stmt.BindText(1, test.a) stmt.BindText(2, test.b) if _, err := stmt.Step(); err != nil { t.Fatal(err) } lt := stmt.ColumnBool(0) eq := stmt.ColumnBool(1) gt := stmt.ColumnBool(2) if lt != test.lt { t.Errorf("%q < %q == %t; want %t", test.a, test.b, lt, test.lt) } if eq != test.eq { t.Errorf("(%q == %q) == %t; want %t", test.a, test.b, eq, test.eq) } if gt != test.gt { t.Errorf("%q > %q == %t; want %t", test.a, test.b, gt, test.gt) } }) } } func TestIDGen(t *testing.T) { const newID = -1 repeat := func(n int, seq ...int) []int { slice := make([]int, 0, len(seq)*n) for i := 0; i < n; i++ { slice = append(slice, seq...) } return slice } tests := []struct { name string actions []int // non-negative means reclaim the ID at the given action index }{ { name: "Single", actions: []int{newID}, }, { name: "LongSequence", actions: repeat(129, newID), }, { name: "Reclaim", actions: []int{ 0: newID, 1: 0, 2: newID, }, }, { name: "ReclaimAfterAnother", actions: []int{ 0: newID, 1: newID, 2: 0, 3: newID, 4: newID, }, }, { name: "LongSequenceWithMiddleReclaim", actions: append(repeat(129, newID), 42, newID), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { gen := new(idGen) got := make([]uintptr, len(test.actions)) used := make(map[uintptr]struct{}) for i, reclaimIdx := range test.actions { if reclaimIdx < 0 { got[i] = gen.next() t.Logf("gen.next() = %d", got[i]) if _, alreadyUsed := used[got[i]]; got[i] == 0 || alreadyUsed { t.Fail() } used[got[i]] = struct{}{} } else { id := got[reclaimIdx] t.Logf("gen.reclaim(%d)", id) gen.reclaim(id) delete(used, id) } } }) } } // TestCFuncPointer verifies that the cFuncPointer function // is identical to taking the function address in the libc style. func TestCFuncPointer(t *testing.T) { got := cFuncPointer(libc.Xfree) want := *(*uintptr)(unsafe.Pointer(&struct{ f func(*libc.TLS, uintptr) }{libc.Xfree})) if got != want { t.Errorf("cFuncPointer(libc.Xfree) = %#x; want %#x", got, want) } } zombiezen-go-sqlite-297af96/go.mod000066400000000000000000000015021501417116500170740ustar00rootroot00000000000000module zombiezen.com/go/sqlite go 1.23.0 toolchain go1.24.2 require ( crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 github.com/chzyer/readline v1.5.0 github.com/google/go-cmp v0.6.0 golang.org/x/text v0.14.0 modernc.org/libc v1.65.7 modernc.org/sqlite v1.37.1 ) require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.33.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) retract ( v0.9.1 // Contains retractions only. v0.9.0 // Had libc memgrind issues. ) zombiezen-go-sqlite-297af96/go.sum000066400000000000000000000114361501417116500171300ustar00rootroot00000000000000crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw= crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= zombiezen-go-sqlite-297af96/go.work000066400000000000000000000001121501417116500172730ustar00rootroot00000000000000go 1.23.0 toolchain go1.24.2 use ( . ./cmd/zombiezen-sqlite-migrate ) zombiezen-go-sqlite-297af96/go.work.sum000066400000000000000000000122441501417116500201070ustar00rootroot00000000000000github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= zombiezen-go-sqlite-297af96/http_example_test.go000066400000000000000000000017321501417116500220530ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite_test import ( "fmt" "log" "net/http" "zombiezen.com/go/sqlite/sqlitex" ) var dbpool *sqlitex.Pool // Using a Pool to execute SQL in a concurrent HTTP handler. func Example_http() { var err error dbpool, err = sqlitex.NewPool("file:memory:?mode=memory", sqlitex.PoolOptions{ PoolSize: 10, }) if err != nil { log.Fatal(err) } http.HandleFunc("/", handle) log.Fatal(http.ListenAndServe(":8080", nil)) } func handle(w http.ResponseWriter, r *http.Request) { conn, err := dbpool.Take(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } defer dbpool.Put(conn) stmt := conn.Prep("SELECT foo FROM footable WHERE id = $id;") stmt.SetText("$id", "_user_id_") for { if hasRow, err := stmt.Step(); err != nil { // ... handle error } else if !hasRow { break } foo := stmt.GetText("foo") // ... use foo fmt.Fprintln(w, foo) } } zombiezen-go-sqlite-297af96/index_constraint.go000066400000000000000000000111701501417116500216720ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "unsafe" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) // IndexConstraint is a constraint term in the WHERE clause // of a query that uses a virtual table. type IndexConstraint struct { // Column is the left-hand operand. // Column indices start at 0. // -1 indicates the left-hand operand is the rowid. // Column should be ignored when Op is [IndexConstraintLimit] or [IndexConstraintOffset]. Column int // Op is the constraint's operator. Op IndexConstraintOp // Usable indicates whether BestIndex should consider the constraint. // Usable may false depending on how tables are ordered in a join. Usable bool // Collation is the name of the collating sequence // that should be used when evaluating the constraint. Collation string // RValue is the right-hand operand, if known during statement preparation. // It's only valid until the end of BestIndex. RValue Value // RValueKnown indicates whether RValue is set. RValueKnown bool } func (c *IndexConstraint) copyFromC(tls *libc.TLS, infoPtr uintptr, i int32, ppVal uintptr) { info := (*lib.Sqlite3_index_info)(unsafe.Pointer(infoPtr)) src := (*lib.Sqlite3_index_constraint)(unsafe.Pointer(info.FaConstraint + uintptr(i)*unsafe.Sizeof(lib.Sqlite3_index_constraint{}))) *c = IndexConstraint{ Column: int(src.FiColumn), Op: IndexConstraintOp(src.Fop), Usable: src.Fusable != 0, } const binaryCollation = "BINARY" cCollation := lib.Xsqlite3_vtab_collation(tls, infoPtr, int32(i)) if isCStringEqual(cCollation, binaryCollation) { // BINARY is the most common, so avoid allocations in this case. c.Collation = binaryCollation } else { c.Collation = libc.GoString(cCollation) } if ppVal != 0 { res := ResultCode(lib.Xsqlite3_vtab_rhs_value(tls, infoPtr, int32(i), ppVal)) if res == ResultOK { c.RValue = Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(ppVal)), } c.RValueKnown = true } } } // IndexConstraintOp is an enumeration of virtual table constraint operators // used in [IndexConstraint]. type IndexConstraintOp uint8 const ( IndexConstraintEq IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_EQ IndexConstraintGT IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GT IndexConstraintLE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LE IndexConstraintLT IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LT IndexConstraintGE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GE IndexConstraintMatch IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_MATCH IndexConstraintLike IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LIKE IndexConstraintGlob IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_GLOB IndexConstraintRegexp IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_REGEXP IndexConstraintNE IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_NE IndexConstraintIsNot IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNOT IndexConstraintIsNotNull IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNOTNULL IndexConstraintIsNull IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_ISNULL IndexConstraintIs IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_IS IndexConstraintLimit IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_LIMIT IndexConstraintOffset IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_OFFSET ) const indexConstraintFunction IndexConstraintOp = lib.SQLITE_INDEX_CONSTRAINT_FUNCTION // String returns the operator symbol or keyword. func (op IndexConstraintOp) String() string { switch op { case IndexConstraintEq: return "=" case IndexConstraintGT: return ">" case IndexConstraintLE: return "<=" case IndexConstraintLT: return "<" case IndexConstraintGE: return ">=" case IndexConstraintMatch: return "MATCH" case IndexConstraintLike: return "LIKE" case IndexConstraintGlob: return "GLOB" case IndexConstraintRegexp: return "REGEXP" case IndexConstraintNE: return "<>" case IndexConstraintIsNot: return "IS NOT" case IndexConstraintIsNotNull: return "IS NOT NULL" case IndexConstraintIsNull: return "IS NULL" case IndexConstraintIs: return "IS" case IndexConstraintLimit: return "LIMIT" case IndexConstraintOffset: return "OFFSET" default: if op < indexConstraintFunction { return fmt.Sprintf("IndexConstraintOp(%d)", uint8(op)) } return fmt.Sprintf("", uint8(op)) } } func isCStringEqual(c uintptr, s string) bool { if c == 0 { return s == "" } for { cc := *(*byte)(unsafe.Pointer(c)) if cc == 0 { return len(s) == 0 } if len(s) == 0 || cc != s[0] { return false } c++ s = s[1:] } } zombiezen-go-sqlite-297af96/internal_test.go000066400000000000000000000012301501417116500211660ustar00rootroot00000000000000// Copyright 2024 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "testing" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" ) func BenchmarkGoStringN(b *testing.B) { tls := libc.NewTLS() const want = "Hello, World!\n" ptr, err := malloc(tls, types.Size_t(len(want)+1)) if err != nil { b.Fatal(err) } defer libc.Xfree(tls, ptr) for i := 0; i < len(want); i++ { *(*byte)(unsafe.Pointer(ptr + uintptr(i))) = want[i] } b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { got := goStringN(ptr, len(want)) if got != want { b.Errorf("goStringN(%#x, %d) = %q; want %q", ptr, len(want), got, want) } } } zombiezen-go-sqlite-297af96/op_type.go000066400000000000000000000074451501417116500200100ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" lib "modernc.org/sqlite/lib" ) // OpType is an enumeration of SQLite statements and authorizable actions. type OpType int32 // Operation types const ( OpCreateIndex OpType = lib.SQLITE_CREATE_INDEX OpCreateTable OpType = lib.SQLITE_CREATE_TABLE OpCreateTempIndex OpType = lib.SQLITE_CREATE_TEMP_INDEX OpCreateTempTable OpType = lib.SQLITE_CREATE_TEMP_TABLE OpCreateTempTrigger OpType = lib.SQLITE_CREATE_TEMP_TRIGGER OpCreateTempView OpType = lib.SQLITE_CREATE_TEMP_VIEW OpCreateTrigger OpType = lib.SQLITE_CREATE_TRIGGER OpCreateView OpType = lib.SQLITE_CREATE_VIEW OpDelete OpType = lib.SQLITE_DELETE OpDropIndex OpType = lib.SQLITE_DROP_INDEX OpDropTable OpType = lib.SQLITE_DROP_TABLE OpDropTempIndex OpType = lib.SQLITE_DROP_TEMP_INDEX OpDropTempTable OpType = lib.SQLITE_DROP_TEMP_TABLE OpDropTempTrigger OpType = lib.SQLITE_DROP_TEMP_TRIGGER OpDropTempView OpType = lib.SQLITE_DROP_TEMP_VIEW OpDropTrigger OpType = lib.SQLITE_DROP_TRIGGER OpDropView OpType = lib.SQLITE_DROP_VIEW OpInsert OpType = lib.SQLITE_INSERT OpPragma OpType = lib.SQLITE_PRAGMA OpRead OpType = lib.SQLITE_READ OpSelect OpType = lib.SQLITE_SELECT OpTransaction OpType = lib.SQLITE_TRANSACTION OpUpdate OpType = lib.SQLITE_UPDATE OpAttach OpType = lib.SQLITE_ATTACH OpDetach OpType = lib.SQLITE_DETACH OpAlterTable OpType = lib.SQLITE_ALTER_TABLE OpReindex OpType = lib.SQLITE_REINDEX OpAnalyze OpType = lib.SQLITE_ANALYZE OpCreateVTable OpType = lib.SQLITE_CREATE_VTABLE OpDropVTable OpType = lib.SQLITE_DROP_VTABLE OpFunction OpType = lib.SQLITE_FUNCTION OpSavepoint OpType = lib.SQLITE_SAVEPOINT OpCopy OpType = lib.SQLITE_COPY OpRecursive OpType = lib.SQLITE_RECURSIVE ) // String returns the C constant name of the operation type. func (op OpType) String() string { switch op { case OpCreateIndex: return "SQLITE_CREATE_INDEX" case OpCreateTable: return "SQLITE_CREATE_TABLE" case OpCreateTempIndex: return "SQLITE_CREATE_TEMP_INDEX" case OpCreateTempTable: return "SQLITE_CREATE_TEMP_TABLE" case OpCreateTempTrigger: return "SQLITE_CREATE_TEMP_TRIGGER" case OpCreateTempView: return "SQLITE_CREATE_TEMP_VIEW" case OpCreateTrigger: return "SQLITE_CREATE_TRIGGER" case OpCreateView: return "SQLITE_CREATE_VIEW" case OpDelete: return "SQLITE_DELETE" case OpDropIndex: return "SQLITE_DROP_INDEX" case OpDropTable: return "SQLITE_DROP_TABLE" case OpDropTempIndex: return "SQLITE_DROP_TEMP_INDEX" case OpDropTempTable: return "SQLITE_DROP_TEMP_TABLE" case OpDropTempTrigger: return "SQLITE_DROP_TEMP_TRIGGER" case OpDropTempView: return "SQLITE_DROP_TEMP_VIEW" case OpDropTrigger: return "SQLITE_DROP_TRIGGER" case OpDropView: return "SQLITE_DROP_VIEW" case OpInsert: return "SQLITE_INSERT" case OpPragma: return "SQLITE_PRAGMA" case OpRead: return "SQLITE_READ" case OpSelect: return "SQLITE_SELECT" case OpTransaction: return "SQLITE_TRANSACTION" case OpUpdate: return "SQLITE_UPDATE" case OpAttach: return "SQLITE_ATTACH" case OpDetach: return "SQLITE_DETACH" case OpAlterTable: return "SQLITE_ALTER_TABLE" case OpReindex: return "SQLITE_REINDEX" case OpAnalyze: return "SQLITE_ANALYZE" case OpCreateVTable: return "SQLITE_CREATE_VTABLE" case OpDropVTable: return "SQLITE_DROP_VTABLE" case OpFunction: return "SQLITE_FUNCTION" case OpSavepoint: return "SQLITE_SAVEPOINT" case OpCopy: return "SQLITE_COPY" case OpRecursive: return "SQLITE_RECURSIVE" default: return fmt.Sprintf("OpType(%d)", int32(op)) } } zombiezen-go-sqlite-297af96/openflags.go000066400000000000000000000070401501417116500202760ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "strings" lib "modernc.org/sqlite/lib" ) // OpenFlags are [flags] used when opening a [Conn] via [OpenConn]. // Either [OpenReadOnly] or [OpenReadWrite] must always be present. // // [flags]: https://www.sqlite.org/c3ref/c_open_autoproxy.html type OpenFlags uint // Required flags, one of which must be passed to [OpenConn]. const ( // OpenReadOnly opens the database in read-only mode. // If the database does not already exist, an error is returned. OpenReadOnly OpenFlags = lib.SQLITE_OPEN_READONLY // OpenReadWrite opens the database for reading and writing if possible, // or reading only if the file is write protected by the operating system. // If the database does not already exist, // an error is returned unless OpenCreate is also passed. OpenReadWrite OpenFlags = lib.SQLITE_OPEN_READWRITE ) // Optional flags to pass to [OpenConn]. const ( // OpenCreate will create the file if it does not already exist. // It is only valid with [OpenReadWrite]. OpenCreate OpenFlags = lib.SQLITE_OPEN_CREATE // OpenURI allows the path to be interpreted as a URI. OpenURI OpenFlags = lib.SQLITE_OPEN_URI // OpenMemory will be opened as an in-memory database. // The path is ignored unless [OpenSharedCache] is used. OpenMemory OpenFlags = lib.SQLITE_OPEN_MEMORY // OpenSharedCache opens the database with [shared-cache]. // This is mostly only useful for sharing in-memory databases: // it's [not recommended] for other purposes. // // [shared-cache]: https://www.sqlite.org/sharedcache.html // [not recommended]: https://www.sqlite.org/sharedcache.html#dontuse OpenSharedCache OpenFlags = lib.SQLITE_OPEN_SHAREDCACHE // OpenPrivateCache forces the database to not use shared-cache. OpenPrivateCache OpenFlags = lib.SQLITE_OPEN_PRIVATECACHE // OpenWAL enables the [write-ahead log] for the database. // // [write-ahead log]: https://www.sqlite.org/wal.html OpenWAL OpenFlags = lib.SQLITE_OPEN_WAL // OpenNoMutex has no effect. // // Deprecated: This flag is now implied. OpenNoMutex OpenFlags = lib.SQLITE_OPEN_NOMUTEX // OpenFullMutex has no effect. // // Deprecated: This flag has no equivalent and is ignored. OpenFullMutex OpenFlags = lib.SQLITE_OPEN_FULLMUTEX ) // String returns a pipe-separated list of the C constant names set in flags. func (flags OpenFlags) String() string { var parts []string if flags&OpenReadOnly != 0 { parts = append(parts, "SQLITE_OPEN_READONLY") flags &^= OpenReadOnly } if flags&OpenReadWrite != 0 { parts = append(parts, "SQLITE_OPEN_READWRITE") flags &^= OpenReadWrite } if flags&OpenCreate != 0 { parts = append(parts, "SQLITE_OPEN_CREATE") flags &^= OpenCreate } if flags&OpenURI != 0 { parts = append(parts, "SQLITE_OPEN_URI") flags &^= OpenURI } if flags&OpenMemory != 0 { parts = append(parts, "SQLITE_OPEN_MEMORY") flags &^= OpenMemory } if flags&OpenNoMutex != 0 { parts = append(parts, "SQLITE_OPEN_NOMUTEX") flags &^= OpenNoMutex } if flags&OpenFullMutex != 0 { parts = append(parts, "SQLITE_OPEN_FULLMUTEX") flags &^= OpenFullMutex } if flags&OpenSharedCache != 0 { parts = append(parts, "SQLITE_OPEN_SHAREDCACHE") flags &^= OpenSharedCache } if flags&OpenPrivateCache != 0 { parts = append(parts, "SQLITE_OPEN_PRIVATECACHE") flags &^= OpenPrivateCache } if flags&OpenWAL != 0 { parts = append(parts, "SQLITE_OPEN_WAL") flags &^= OpenWAL } if flags != 0 || len(parts) == 0 { parts = append(parts, fmt.Sprintf("%#x", uint(flags))) } return strings.Join(parts, "|") } zombiezen-go-sqlite-297af96/result.go000066400000000000000000000332051501417116500176400ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "errors" "fmt" "modernc.org/libc" lib "modernc.org/sqlite/lib" ) // ResultCode is an SQLite extended result code. type ResultCode int32 // Primary result codes. const ( ResultOK ResultCode = lib.SQLITE_OK ResultError ResultCode = lib.SQLITE_ERROR ResultInternal ResultCode = lib.SQLITE_INTERNAL ResultPerm ResultCode = lib.SQLITE_PERM ResultAbort ResultCode = lib.SQLITE_ABORT ResultBusy ResultCode = lib.SQLITE_BUSY ResultLocked ResultCode = lib.SQLITE_LOCKED ResultNoMem ResultCode = lib.SQLITE_NOMEM ResultReadOnly ResultCode = lib.SQLITE_READONLY ResultInterrupt ResultCode = lib.SQLITE_INTERRUPT ResultIOErr ResultCode = lib.SQLITE_IOERR ResultCorrupt ResultCode = lib.SQLITE_CORRUPT ResultNotFound ResultCode = lib.SQLITE_NOTFOUND ResultFull ResultCode = lib.SQLITE_FULL ResultCantOpen ResultCode = lib.SQLITE_CANTOPEN ResultProtocol ResultCode = lib.SQLITE_PROTOCOL ResultEmpty ResultCode = lib.SQLITE_EMPTY ResultSchema ResultCode = lib.SQLITE_SCHEMA ResultTooBig ResultCode = lib.SQLITE_TOOBIG ResultConstraint ResultCode = lib.SQLITE_CONSTRAINT ResultMismatch ResultCode = lib.SQLITE_MISMATCH ResultMisuse ResultCode = lib.SQLITE_MISUSE ResultNoLFS ResultCode = lib.SQLITE_NOLFS ResultAuth ResultCode = lib.SQLITE_AUTH ResultFormat ResultCode = lib.SQLITE_FORMAT ResultRange ResultCode = lib.SQLITE_RANGE ResultNotADB ResultCode = lib.SQLITE_NOTADB ResultNotice ResultCode = lib.SQLITE_NOTICE ResultWarning ResultCode = lib.SQLITE_WARNING ResultRow ResultCode = lib.SQLITE_ROW ResultDone ResultCode = lib.SQLITE_DONE ) // Extended result codes. const ( ResultErrorMissingCollSeq ResultCode = lib.SQLITE_ERROR_MISSING_COLLSEQ ResultErrorRetry ResultCode = lib.SQLITE_ERROR_RETRY ResultErrorSnapshot ResultCode = lib.SQLITE_ERROR_SNAPSHOT ResultIOErrRead ResultCode = lib.SQLITE_IOERR_READ ResultIOErrShortRead ResultCode = lib.SQLITE_IOERR_SHORT_READ ResultIOErrWrite ResultCode = lib.SQLITE_IOERR_WRITE ResultIOErrFsync ResultCode = lib.SQLITE_IOERR_FSYNC ResultIOErrDirFsync ResultCode = lib.SQLITE_IOERR_DIR_FSYNC ResultIOErrTruncate ResultCode = lib.SQLITE_IOERR_TRUNCATE ResultIOErrFstat ResultCode = lib.SQLITE_IOERR_FSTAT ResultIOErrUnlock ResultCode = lib.SQLITE_IOERR_UNLOCK ResultIOErrReadLock ResultCode = lib.SQLITE_IOERR_RDLOCK ResultIOErrDelete ResultCode = lib.SQLITE_IOERR_DELETE ResultIOErrBlocked ResultCode = lib.SQLITE_IOERR_BLOCKED ResultIOErrNoMem ResultCode = lib.SQLITE_IOERR_NOMEM ResultIOErrAccess ResultCode = lib.SQLITE_IOERR_ACCESS ResultIOErrCheckReservedLock ResultCode = lib.SQLITE_IOERR_CHECKRESERVEDLOCK ResultIOErrLock ResultCode = lib.SQLITE_IOERR_LOCK ResultIOErrClose ResultCode = lib.SQLITE_IOERR_CLOSE ResultIOErrDirClose ResultCode = lib.SQLITE_IOERR_DIR_CLOSE ResultIOErrSHMOpen ResultCode = lib.SQLITE_IOERR_SHMOPEN ResultIOErrSHMSize ResultCode = lib.SQLITE_IOERR_SHMSIZE ResultIOErrSHMLock ResultCode = lib.SQLITE_IOERR_SHMLOCK ResultIOErrSHMMap ResultCode = lib.SQLITE_IOERR_SHMMAP ResultIOErrSeek ResultCode = lib.SQLITE_IOERR_SEEK ResultIOErrDeleteNoEnt ResultCode = lib.SQLITE_IOERR_DELETE_NOENT ResultIOErrMMap ResultCode = lib.SQLITE_IOERR_MMAP ResultIOErrGetTempPath ResultCode = lib.SQLITE_IOERR_GETTEMPPATH ResultIOErrConvPath ResultCode = lib.SQLITE_IOERR_CONVPATH ResultIOErrVNode ResultCode = lib.SQLITE_IOERR_VNODE ResultIOErrAuth ResultCode = lib.SQLITE_IOERR_AUTH ResultIOErrBeginAtomic ResultCode = lib.SQLITE_IOERR_BEGIN_ATOMIC ResultIOErrCommitAtomic ResultCode = lib.SQLITE_IOERR_COMMIT_ATOMIC ResultIOErrRollbackAtomic ResultCode = lib.SQLITE_IOERR_ROLLBACK_ATOMIC ResultLockedSharedCache ResultCode = lib.SQLITE_LOCKED_SHAREDCACHE ResultBusyRecovery ResultCode = lib.SQLITE_BUSY_RECOVERY ResultBusySnapshot ResultCode = lib.SQLITE_BUSY_SNAPSHOT ResultCantOpenNoTempDir ResultCode = lib.SQLITE_CANTOPEN_NOTEMPDIR ResultCantOpenIsDir ResultCode = lib.SQLITE_CANTOPEN_ISDIR ResultCantOpenFullPath ResultCode = lib.SQLITE_CANTOPEN_FULLPATH ResultCantOpenConvPath ResultCode = lib.SQLITE_CANTOPEN_CONVPATH ResultCorruptVTab ResultCode = lib.SQLITE_CORRUPT_VTAB ResultReadOnlyRecovery ResultCode = lib.SQLITE_READONLY_RECOVERY ResultReadOnlyCantLock ResultCode = lib.SQLITE_READONLY_CANTLOCK ResultReadOnlyRollback ResultCode = lib.SQLITE_READONLY_ROLLBACK ResultReadOnlyDBMoved ResultCode = lib.SQLITE_READONLY_DBMOVED ResultReadOnlyCantInit ResultCode = lib.SQLITE_READONLY_CANTINIT ResultReadOnlyDirectory ResultCode = lib.SQLITE_READONLY_DIRECTORY ResultAbortRollback ResultCode = lib.SQLITE_ABORT_ROLLBACK ResultConstraintCheck ResultCode = lib.SQLITE_CONSTRAINT_CHECK ResultConstraintCommitHook ResultCode = lib.SQLITE_CONSTRAINT_COMMITHOOK ResultConstraintForeignKey ResultCode = lib.SQLITE_CONSTRAINT_FOREIGNKEY ResultConstraintFunction ResultCode = lib.SQLITE_CONSTRAINT_FUNCTION ResultConstraintNotNull ResultCode = lib.SQLITE_CONSTRAINT_NOTNULL ResultConstraintPrimaryKey ResultCode = lib.SQLITE_CONSTRAINT_PRIMARYKEY ResultConstraintTrigger ResultCode = lib.SQLITE_CONSTRAINT_TRIGGER ResultConstraintUnique ResultCode = lib.SQLITE_CONSTRAINT_UNIQUE ResultConstraintVTab ResultCode = lib.SQLITE_CONSTRAINT_VTAB ResultConstraintRowID ResultCode = lib.SQLITE_CONSTRAINT_ROWID ResultNoticeRecoverWAL ResultCode = lib.SQLITE_NOTICE_RECOVER_WAL ResultNoticeRecoverRollback ResultCode = lib.SQLITE_NOTICE_RECOVER_ROLLBACK ResultWarningAutoIndex ResultCode = lib.SQLITE_WARNING_AUTOINDEX ResultAuthUser ResultCode = lib.SQLITE_AUTH_USER ) // ToPrimary returns the primary result code of the given code. // https://sqlite.org/rescode.html#primary_result_codes_versus_extended_result_codes func (code ResultCode) ToPrimary() ResultCode { return code & 0xff } // IsSuccess reports whether code indicates success. func (code ResultCode) IsSuccess() bool { return code == ResultOK || code == ResultRow || code == ResultDone } // String returns the C constant name of the result code. func (code ResultCode) String() string { switch code { case ResultOK: return "SQLITE_OK" case ResultError: return "SQLITE_ERROR" case ResultInternal: return "SQLITE_INTERNAL" case ResultPerm: return "SQLITE_PERM" case ResultAbort: return "SQLITE_ABORT" case ResultBusy: return "SQLITE_BUSY" case ResultLocked: return "SQLITE_LOCKED" case ResultNoMem: return "SQLITE_NOMEM" case ResultReadOnly: return "SQLITE_READONLY" case ResultInterrupt: return "SQLITE_INTERRUPT" case ResultIOErr: return "SQLITE_IOERR" case ResultCorrupt: return "SQLITE_CORRUPT" case ResultNotFound: return "SQLITE_NOTFOUND" case ResultFull: return "SQLITE_FULL" case ResultCantOpen: return "SQLITE_CANTOPEN" case ResultProtocol: return "SQLITE_PROTOCOL" case ResultEmpty: return "SQLITE_EMPTY" case ResultSchema: return "SQLITE_SCHEMA" case ResultTooBig: return "SQLITE_TOOBIG" case ResultConstraint: return "SQLITE_CONSTRAINT" case ResultMismatch: return "SQLITE_MISMATCH" case ResultMisuse: return "SQLITE_MISUSE" case ResultNoLFS: return "SQLITE_NOLFS" case ResultAuth: return "SQLITE_AUTH" case ResultFormat: return "SQLITE_FORMAT" case ResultRange: return "SQLITE_RANGE" case ResultNotADB: return "SQLITE_NOTADB" case ResultNotice: return "SQLITE_NOTICE" case ResultWarning: return "SQLITE_WARNING" case ResultRow: return "SQLITE_ROW" case ResultDone: return "SQLITE_DONE" case ResultErrorMissingCollSeq: return "SQLITE_ERROR_MISSING_COLLSEQ" case ResultErrorRetry: return "SQLITE_ERROR_RETRY" case ResultErrorSnapshot: return "SQLITE_ERROR_SNAPSHOT" case ResultIOErrRead: return "SQLITE_IOERR_READ" case ResultIOErrShortRead: return "SQLITE_IOERR_SHORT_READ" case ResultIOErrWrite: return "SQLITE_IOERR_WRITE" case ResultIOErrFsync: return "SQLITE_IOERR_FSYNC" case ResultIOErrDirFsync: return "SQLITE_IOERR_DIR_FSYNC" case ResultIOErrTruncate: return "SQLITE_IOERR_TRUNCATE" case ResultIOErrFstat: return "SQLITE_IOERR_FSTAT" case ResultIOErrUnlock: return "SQLITE_IOERR_UNLOCK" case ResultIOErrReadLock: return "SQLITE_IOERR_RDLOCK" case ResultIOErrDelete: return "SQLITE_IOERR_DELETE" case ResultIOErrBlocked: return "SQLITE_IOERR_BLOCKED" case ResultIOErrNoMem: return "SQLITE_IOERR_NOMEM" case ResultIOErrAccess: return "SQLITE_IOERR_ACCESS" case ResultIOErrCheckReservedLock: return "SQLITE_IOERR_CHECKRESERVEDLOCK" case ResultIOErrLock: return "SQLITE_IOERR_LOCK" case ResultIOErrClose: return "SQLITE_IOERR_CLOSE" case ResultIOErrDirClose: return "SQLITE_IOERR_DIR_CLOSE" case ResultIOErrSHMOpen: return "SQLITE_IOERR_SHMOPEN" case ResultIOErrSHMSize: return "SQLITE_IOERR_SHMSIZE" case ResultIOErrSHMLock: return "SQLITE_IOERR_SHMLOCK" case ResultIOErrSHMMap: return "SQLITE_IOERR_SHMMAP" case ResultIOErrSeek: return "SQLITE_IOERR_SEEK" case ResultIOErrDeleteNoEnt: return "SQLITE_IOERR_DELETE_NOENT" case ResultIOErrMMap: return "SQLITE_IOERR_MMAP" case ResultIOErrGetTempPath: return "SQLITE_IOERR_GETTEMPPATH" case ResultIOErrConvPath: return "SQLITE_IOERR_CONVPATH" case ResultIOErrVNode: return "SQLITE_IOERR_VNODE" case ResultIOErrAuth: return "SQLITE_IOERR_AUTH" case ResultIOErrBeginAtomic: return "SQLITE_IOERR_BEGIN_ATOMIC" case ResultIOErrCommitAtomic: return "SQLITE_IOERR_COMMIT_ATOMIC" case ResultIOErrRollbackAtomic: return "SQLITE_IOERR_ROLLBACK_ATOMIC" case ResultLockedSharedCache: return "SQLITE_LOCKED_SHAREDCACHE" case ResultBusyRecovery: return "SQLITE_BUSY_RECOVERY" case ResultBusySnapshot: return "SQLITE_BUSY_SNAPSHOT" case ResultCantOpenNoTempDir: return "SQLITE_CANTOPEN_NOTEMPDIR" case ResultCantOpenIsDir: return "SQLITE_CANTOPEN_ISDIR" case ResultCantOpenFullPath: return "SQLITE_CANTOPEN_FULLPATH" case ResultCantOpenConvPath: return "SQLITE_CANTOPEN_CONVPATH" case ResultCorruptVTab: return "SQLITE_CORRUPT_VTAB" case ResultReadOnlyRecovery: return "SQLITE_READONLY_RECOVERY" case ResultReadOnlyCantLock: return "SQLITE_READONLY_CANTLOCK" case ResultReadOnlyRollback: return "SQLITE_READONLY_ROLLBACK" case ResultReadOnlyDBMoved: return "SQLITE_READONLY_DBMOVED" case ResultReadOnlyCantInit: return "SQLITE_READONLY_CANTINIT" case ResultReadOnlyDirectory: return "SQLITE_READONLY_DIRECTORY" case ResultAbortRollback: return "SQLITE_ABORT_ROLLBACK" case ResultConstraintCheck: return "SQLITE_CONSTRAINT_CHECK" case ResultConstraintCommitHook: return "SQLITE_CONSTRAINT_COMMITHOOK" case ResultConstraintForeignKey: return "SQLITE_CONSTRAINT_FOREIGNKEY" case ResultConstraintFunction: return "SQLITE_CONSTRAINT_FUNCTION" case ResultConstraintNotNull: return "SQLITE_CONSTRAINT_NOTNULL" case ResultConstraintPrimaryKey: return "SQLITE_CONSTRAINT_PRIMARYKEY" case ResultConstraintTrigger: return "SQLITE_CONSTRAINT_TRIGGER" case ResultConstraintUnique: return "SQLITE_CONSTRAINT_UNIQUE" case ResultConstraintVTab: return "SQLITE_CONSTRAINT_VTAB" case ResultConstraintRowID: return "SQLITE_CONSTRAINT_ROWID" case ResultNoticeRecoverWAL: return "SQLITE_NOTICE_RECOVER_WAL" case ResultNoticeRecoverRollback: return "SQLITE_NOTICE_RECOVER_ROLLBACK" case ResultWarningAutoIndex: return "SQLITE_WARNING_AUTOINDEX" case ResultAuthUser: return "SQLITE_AUTH_USER" default: return fmt.Sprintf("ResultCode(%d)", int32(code)) } } // Message returns the English-language text that describes the result code. func (code ResultCode) Message() string { tls := libc.NewTLS() defer tls.Close() initlib(tls) cstr := lib.Xsqlite3_errstr(tls, int32(code)) return libc.GoString(cstr) } // ToError converts an error code into an error // for which ErrCode(code.ToError()) == code. // If the code indicates success, ToError returns nil. func (code ResultCode) ToError() error { if code.IsSuccess() { return nil } return sqliteError{code} } type sqliteError struct { code ResultCode } func (e sqliteError) Error() string { return e.code.Message() } type extendedError struct { code ResultCode offset int // negative if not present message string } func (e *extendedError) Error() string { m := e.code.Message() if e.message == "" || e.message == m { // Only use the connection's message if it's adding more information. // Sometimes the connection will return the code's message. return m } return m + ": " + e.message } func (e *extendedError) As(target any) bool { switch target := target.(type) { case *sqliteError: *target = sqliteError{e.code} return true case **extendedError: *target = e return true default: return false } } // ErrCode returns the error's SQLite error code // or [ResultError] if the error does not represent a SQLite error. // ErrCode returns [ResultOK] if and only if the error is nil. func ErrCode(err error) ResultCode { if err == nil { return ResultOK } if e := new(sqliteError); errors.As(err, e) { return e.code } return ResultError } // ErrorOffset returns the byte offset of the start of the SQL token // that the error references if known. func ErrorOffset(err error) (offset int, ok bool) { if e := (*extendedError)(nil); errors.As(err, &e) { return e.offset, e.offset >= 0 } return -1, false } zombiezen-go-sqlite-297af96/result_test.go000066400000000000000000000013731501417116500207000ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlite import "testing" func TestResultCodeMessage(t *testing.T) { t.Log(ResultOK.Message()) t.Log(ResultNoMem.Message()) } func TestErrCode(t *testing.T) { rawErr := ResultInterrupt.ToError() if got, want := ErrCode(rawErr), ResultInterrupt; got != want { t.Errorf("got err=%s, want %s", got, want) } wrappedErr := errWithMessage{err: rawErr, msg: "Doing something"} if got, want := ErrCode(wrappedErr), ResultInterrupt; got != want { t.Errorf("got err=%s, want %s", got, want) } } type errWithMessage struct { err error msg string } func (e errWithMessage) Unwrap() error { return e.err } func (e errWithMessage) Error() string { return e.msg + ": " + e.err.Error() } zombiezen-go-sqlite-297af96/session.go000066400000000000000000000673511501417116500200160ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "io" "runtime" "sync" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" lib "modernc.org/sqlite/lib" ) // A Session tracks database changes made by a Conn. // It is used to build changesets. // // For more details: https://www.sqlite.org/sessionintro.html type Session struct { tls *libc.TLS ptr uintptr } // CreateSession creates a new session object. // If db is "", then a default of "main" is used. // It is the caller's responsibility to call Delete when the session is // no longer needed. // // https://www.sqlite.org/session/sqlite3session_create.html func (c *Conn) CreateSession(db string) (*Session, error) { if c == nil { return nil, fmt.Errorf("sqlite: create session: nil connection") } var cdb uintptr if db == "" || db == "main" { cdb = mainCString } else { var err error cdb, err = libc.CString(db) if err != nil { return nil, fmt.Errorf("sqlite: create session: %w", err) } defer libc.Xfree(c.tls, cdb) } doublePtr, err := malloc(c.tls, ptrSize) if err != nil { return nil, fmt.Errorf("sqlite: create session: %w", err) } defer libc.Xfree(c.tls, doublePtr) res := ResultCode(lib.Xsqlite3session_create(c.tls, c.conn, cdb, doublePtr)) if err := res.ToError(); err != nil { return nil, fmt.Errorf("sqlite: create session: %w", err) } s := &Session{ tls: c.tls, ptr: *(*uintptr)(unsafe.Pointer(doublePtr)), } runtime.SetFinalizer(s, func(s *Session) { if s.ptr != 0 { panic("open *sqlite.Session garbage collected, call Delete method") } }) return s, nil } // Delete releases any resources associated with the session. // It must be called before closing the Conn the session is attached to. func (s *Session) Delete() { if s.ptr == 0 { panic("Session.Delete called twice on same session") } lib.Xsqlite3session_delete(s.tls, s.ptr) s.ptr = 0 s.tls = nil } // Enable enables recording of changes after a previous call to Disable. // New sessions start enabled. // // https://www.sqlite.org/session/sqlite3session_enable.html func (s *Session) Enable() { if s.ptr == 0 { panic("Session.Enable called on deleted session") } lib.Xsqlite3session_enable(s.tls, s.ptr, 1) } // Disable disables recording of changes. // // https://www.sqlite.org/session/sqlite3session_enable.html func (s *Session) Disable() { if s.ptr == 0 { panic("Session.Disable called on deleted session") } lib.Xsqlite3session_enable(s.tls, s.ptr, 0) } // Attach attaches a table to the session object. // Changes made to the table will be tracked by the session. // An empty tableName attaches all the tables in the database. func (s *Session) Attach(tableName string) error { if s.ptr == 0 { return fmt.Errorf("sqlite: attach table %q to session: session deleted", tableName) } var ctable uintptr if tableName != "" { var err error ctable, err = libc.CString(tableName) if err != nil { return fmt.Errorf("sqlite: attach table %q to session: %v", tableName, err) } defer libc.Xfree(s.tls, ctable) } res := ResultCode(lib.Xsqlite3session_attach(s.tls, s.ptr, ctable)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: attach table %q to session: %w", tableName, err) } return nil } // Diff appends the difference between two tables (srcDB and the session DB) to // the session. The two tables must have the same name and schema. // // https://www.sqlite.org/session/sqlite3session_diff.html func (s *Session) Diff(srcDB, tableName string) error { if s.ptr == 0 { return fmt.Errorf("sqlite: diff table %q: session deleted", tableName) } errMsgPtr, err := malloc(s.tls, ptrSize) if err != nil { return fmt.Errorf("sqlite: diff table %q: %v", tableName, err) } defer libc.Xfree(s.tls, errMsgPtr) csrcDB, err := libc.CString(srcDB) if err != nil { return fmt.Errorf("sqlite: diff table %q: %v", tableName, err) } defer libc.Xfree(s.tls, csrcDB) ctable, err := libc.CString(tableName) if err != nil { return fmt.Errorf("sqlite: diff table %q: %v", tableName, err) } defer libc.Xfree(s.tls, ctable) res := ResultCode(lib.Xsqlite3session_diff(s.tls, s.ptr, csrcDB, ctable, errMsgPtr)) if err := res.ToError(); err != nil { cerrMsg := *(*uintptr)(unsafe.Pointer(errMsgPtr)) if cerrMsg == 0 { return fmt.Errorf("sqlite: diff table %q: %w", tableName, err) } errMsg := libc.GoString(cerrMsg) lib.Xsqlite3_free(s.tls, cerrMsg) return fmt.Errorf("sqlite: diff table %q: %w (%s)", tableName, err, errMsg) } return nil } // WriteChangeset generates a changeset from a session. // // https://www.sqlite.org/session/sqlite3session_changeset.html func (s *Session) WriteChangeset(w io.Writer) error { if s.ptr == 0 { return fmt.Errorf("sqlite: write session changeset: session deleted") } xOutput, pOut := registerStreamWriter(w) defer unregisterStreamWriter(pOut) res := ResultCode(lib.Xsqlite3session_changeset_strm(s.tls, s.ptr, xOutput, pOut)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: write session changeset: %w", err) } return nil } // WritePatchset generates a patchset from a session. // // https://www.sqlite.org/session/sqlite3session_patchset.html func (s *Session) WritePatchset(w io.Writer) error { if s.ptr == 0 { return fmt.Errorf("sqlite: write session patchset: session deleted") } xOutput, pOut := registerStreamWriter(w) defer unregisterStreamWriter(pOut) res := ResultCode(lib.Xsqlite3session_patchset_strm(s.tls, s.ptr, xOutput, pOut)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: write session patchset: %w", err) } return nil } // ApplyChangeset applies a changeset to the database. // // If filterFn is not nil and the changeset includes changes for a table for // which the function reports false, then the changes are ignored. If filterFn // is nil, then all changes are applied. // // If a changeset will not apply cleanly, then conflictFn will be called to // resolve the conflict. See https://www.sqlite.org/session/sqlite3changeset_apply.html // for more details. func (c *Conn) ApplyChangeset(r io.Reader, filterFn func(tableName string) bool, conflictFn ConflictHandler) error { if c == nil { return fmt.Errorf("sqlite: apply changeset: nil connection") } if conflictFn == nil { return fmt.Errorf("sqlite: apply changeset: no conflict handler provided") } xInput, pIn := registerStreamReader(r) defer unregisterStreamReader(pIn) appliesIDMu.Lock() pCtx := appliesIDs.next() appliesIDMu.Unlock() applies.Store(pCtx, applyFuncs{ tls: c.tls, filterFn: filterFn, conflictFn: conflictFn, }) defer func() { applies.Delete(pCtx) appliesIDMu.Lock() appliesIDs.reclaim(pCtx) appliesIDMu.Unlock() }() xFilter := uintptr(0) if filterFn != nil { xFilter = cFuncPointer(changesetApplyFilter) } xConflict := cFuncPointer(changesetApplyConflict) res := ResultCode(lib.Xsqlite3changeset_apply_strm(c.tls, c.conn, xInput, pIn, xFilter, xConflict, pCtx)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: apply changeset: %w", err) } return nil } type applyFuncs struct { tls *libc.TLS filterFn func(tableName string) bool conflictFn ConflictHandler } var ( applies sync.Map // map[uintptr]applyFuncs appliesIDMu sync.Mutex appliesIDs idGen ) func changesetApplyFilter(tls *libc.TLS, pCtx uintptr, zTab uintptr) int32 { appliesValue, _ := applies.Load(pCtx) funcs := appliesValue.(applyFuncs) tableName := libc.GoString(zTab) if funcs.filterFn(tableName) { return 1 } else { return 0 } } func changesetApplyConflict(tls *libc.TLS, pCtx uintptr, eConflict int32, p uintptr) int32 { appliesValue, _ := applies.Load(pCtx) funcs := appliesValue.(applyFuncs) return int32(funcs.conflictFn(ConflictType(eConflict), &ChangesetIterator{ tls: tls, ptr: p, })) } // ApplyInverseChangeset applies the inverse of a changeset to the database. // See ApplyChangeset and InvertChangeset for more details. func (c *Conn) ApplyInverseChangeset(r io.Reader, filterFn func(tableName string) bool, conflictFn ConflictHandler) error { if c == nil { return fmt.Errorf("sqlite: apply changeset: nil connection") } pr, pw := io.Pipe() go func() { err := InvertChangeset(pw, pr) pw.CloseWithError(err) }() err := c.ApplyChangeset(pr, filterFn, conflictFn) io.Copy(io.Discard, pr) // wait for invert goroutine to finish return err } // InvertChangeset generates an inverted changeset. Applying an inverted // changeset to a database reverses the effects of applying the uninverted // changeset. // // This function currently assumes that the input is a valid changeset. // If it is not, the results are undefined. // // https://www.sqlite.org/session/sqlite3changeset_invert.html func InvertChangeset(w io.Writer, r io.Reader) error { tls := libc.NewTLS() defer tls.Close() initlib(tls) xInput, pIn := registerStreamReader(r) defer unregisterStreamReader(pIn) xOutput, pOut := registerStreamWriter(w) defer unregisterStreamWriter(pOut) res := ResultCode(lib.Xsqlite3changeset_invert_strm(tls, xInput, pIn, xOutput, pOut)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: invert changeset: %w", err) } return nil } // ChangesetIterator is an iterator over a changeset. type ChangesetIterator struct { tls *libc.TLS ptr uintptr ownTLS bool pIn uintptr // if non-zero, then must be unregistered at Finalize } // NewChangesetIterator returns a new iterator over the contents of the // changeset. The caller is responsible for calling Finalize on the returned // iterator. // // https://www.sqlite.org/session/sqlite3changeset_start.html func NewChangesetIterator(r io.Reader) (*ChangesetIterator, error) { tls := libc.NewTLS() initlib(tls) xInput, pIn := registerStreamReader(r) pp, err := malloc(tls, ptrSize) if err != nil { unregisterStreamReader(pIn) tls.Close() return nil, fmt.Errorf("sqlite: start changeset iterator: %v", err) } defer libc.Xfree(tls, pp) res := ResultCode(lib.Xsqlite3changeset_start_strm(tls, pp, xInput, pIn)) if err := res.ToError(); err != nil { unregisterStreamReader(pIn) tls.Close() return nil, fmt.Errorf("sqlite: start changeset iterator: %w", err) } iter := &ChangesetIterator{ tls: tls, ownTLS: true, ptr: *(*uintptr)(unsafe.Pointer(pp)), pIn: pIn, } runtime.SetFinalizer(iter, func(iter *ChangesetIterator) { if iter.ptr != 0 { panic("open *sqlite.ChangesetIterator garbage collected, call Finalize method") } }) return iter, nil } // Close releases any resources associated with the iterator created with // NewChangesetIterator. func (iter *ChangesetIterator) Close() error { if iter.ptr == 0 { return fmt.Errorf("sqlite: finalize changeset iterator: called twice on same iterator") } res := ResultCode(lib.Xsqlite3changeset_finalize(iter.tls, iter.ptr)) iter.ptr = 0 if iter.ownTLS { iter.tls.Close() } iter.tls = nil if iter.pIn != 0 { unregisterStreamReader(iter.pIn) } iter.pIn = 0 if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: finalize changeset iterator: %w", err) } return nil } // Next advances the iterator to the next change in the changeset. // It is an error to call Next on an iterator passed to an ApplyChangeset // conflict handler. // // https://www.sqlite.org/session/sqlite3changeset_next.html func (iter *ChangesetIterator) Next() (rowReturned bool, err error) { res := ResultCode(lib.Xsqlite3changeset_next(iter.tls, iter.ptr)) switch res { case ResultRow: return true, nil case ResultDone: return false, nil default: return false, fmt.Errorf("sqlite: iterate changeset: %w", res.ToError()) } } // ChangesetOperation holds information about a change in a changeset. type ChangesetOperation struct { // Type is one of OpInsert, OpDelete, or OpUpdate. Type OpType // TableName is the name of the table affected by the change. TableName string // NumColumns is the number of columns in the table affected by the change. NumColumns int // Indirect is true if the session object "indirect" flag was set when the // change was made or the change was made by an SQL trigger or foreign key // action instead of directly as a result of a users SQL statement. Indirect bool } // Operation obtains the current operation from the iterator. // // https://www.sqlite.org/session/sqlite3changeset_op.html func (iter *ChangesetIterator) Operation() (*ChangesetOperation, error) { if iter.ptr == 0 { return nil, fmt.Errorf("sqlite: changeset iterator operation: iterator finalized") } pzTab, err := malloc(iter.tls, ptrSize) if err != nil { return nil, fmt.Errorf("sqlite: changeset iterator operation: %v", err) } defer libc.Xfree(iter.tls, pzTab) pnCol, err := malloc(iter.tls, types.Size_t(unsafe.Sizeof(int32(0)))) if err != nil { return nil, fmt.Errorf("sqlite: changeset iterator operation: %v", err) } defer libc.Xfree(iter.tls, pnCol) pOp, err := malloc(iter.tls, types.Size_t(unsafe.Sizeof(int32(0)))) if err != nil { return nil, fmt.Errorf("sqlite: changeset iterator operation: %v", err) } defer libc.Xfree(iter.tls, pOp) pbIndirect, err := malloc(iter.tls, types.Size_t(unsafe.Sizeof(int32(0)))) if err != nil { return nil, fmt.Errorf("sqlite: changeset iterator operation: %v", err) } defer libc.Xfree(iter.tls, pbIndirect) res := ResultCode(lib.Xsqlite3changeset_op(iter.tls, iter.ptr, pzTab, pnCol, pOp, pbIndirect)) if err := res.ToError(); err != nil { return nil, fmt.Errorf("sqlite: changeset iterator operation: %w", err) } return &ChangesetOperation{ Type: OpType(*(*int32)(unsafe.Pointer(pOp))), TableName: libc.GoString(*(*uintptr)(unsafe.Pointer(pzTab))), NumColumns: int(*(*int32)(unsafe.Pointer(pnCol))), Indirect: *(*int32)(unsafe.Pointer(pbIndirect)) != 0, }, nil } // Old obtains the old row value from an iterator. Column indices start at 0. // The returned value is valid until the iterator is finalized. // // https://www.sqlite.org/session/sqlite3changeset_old.html func (iter *ChangesetIterator) Old(col int) (Value, error) { if iter.ptr == 0 { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: iterator finalized") } ppValue, err := malloc(iter.tls, ptrSize) if err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %v", err) } defer libc.Xfree(iter.tls, ppValue) res := ResultCode(lib.Xsqlite3changeset_old(iter.tls, iter.ptr, int32(col), ppValue)) if err := res.ToError(); err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %w", err) } return Value{ tls: iter.tls, ptrOrType: *(*uintptr)(unsafe.Pointer(ppValue)), }, nil } // New obtains the new row value from an iterator. Column indices start at 0. // The returned value is valid until the iterator is finalized. // // https://www.sqlite.org/session/sqlite3changeset_new.html func (iter *ChangesetIterator) New(col int) (Value, error) { if iter.ptr == 0 { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: iterator finalized") } ppValue, err := malloc(iter.tls, ptrSize) if err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %v", err) } defer libc.Xfree(iter.tls, ppValue) res := ResultCode(lib.Xsqlite3changeset_new(iter.tls, iter.ptr, int32(col), ppValue)) if err := res.ToError(); err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %w", err) } return Value{ tls: iter.tls, ptrOrType: *(*uintptr)(unsafe.Pointer(ppValue)), }, nil } // ConflictValue obtains the conflicting row value from an iterator. // Column indices start at 0. The returned value is valid until the iterator is // finalized. // // https://www.sqlite.org/session/sqlite3changeset_conflict.html func (iter *ChangesetIterator) ConflictValue(col int) (Value, error) { if iter.ptr == 0 { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: iterator finalized") } ppValue, err := malloc(iter.tls, ptrSize) if err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %v", err) } defer libc.Xfree(iter.tls, ppValue) res := ResultCode(lib.Xsqlite3changeset_conflict(iter.tls, iter.ptr, int32(col), ppValue)) if err := res.ToError(); err != nil { return Value{}, fmt.Errorf("sqlite: get changeset iterator value: %w", err) } return Value{ tls: iter.tls, ptrOrType: *(*uintptr)(unsafe.Pointer(ppValue)), }, nil } // ForeignKeyConflicts returns the number of foreign key constraint violations. // // https://www.sqlite.org/session/sqlite3changeset_fk_conflicts.html func (iter *ChangesetIterator) ForeignKeyConflicts() (int, error) { pnOut, err := malloc(iter.tls, types.Size_t(unsafe.Sizeof(int32(0)))) if err != nil { return 0, fmt.Errorf("sqlite: get number of foreign key conflicts: %v", err) } defer libc.Xfree(iter.tls, pnOut) res := ResultCode(lib.Xsqlite3changeset_fk_conflicts(iter.tls, iter.ptr, pnOut)) if err := res.ToError(); err != nil { return 0, fmt.Errorf("sqlite: get number of foreign key conflicts: %w", err) } return int(*(*int32)(unsafe.Pointer(pnOut))), nil } // PrimaryKey returns a map of columns that make up the primary key. // // https://www.sqlite.org/session/sqlite3changeset_pk.html func (iter *ChangesetIterator) PrimaryKey() ([]bool, error) { pabPK, err := malloc(iter.tls, ptrSize) if err != nil { return nil, fmt.Errorf("sqlite: get primary key columns: %v", err) } defer libc.Xfree(iter.tls, pabPK) pnCol, err := malloc(iter.tls, types.Size_t(unsafe.Sizeof(int32(0)))) if err != nil { return nil, fmt.Errorf("sqlite: get primary key columns: %v", err) } defer libc.Xfree(iter.tls, pnCol) res := ResultCode(lib.Xsqlite3changeset_pk(iter.tls, iter.ptr, pabPK, pnCol)) if err := res.ToError(); err != nil { return nil, fmt.Errorf("sqlite: get primary key columns: %w", err) } c := libc.GoBytes(*(*uintptr)(unsafe.Pointer(pabPK)), int(*(*int32)(unsafe.Pointer(pnCol)))) cols := make([]bool, len(c)) for i := range cols { cols[i] = c[i] != 0 } return cols, nil } // ConcatChangesets concatenates two changesets into a single changeset. // // https://www.sqlite.org/session/sqlite3changeset_concat.html func ConcatChangesets(w io.Writer, changeset1, changeset2 io.Reader) error { tls := libc.NewTLS() defer tls.Close() initlib(tls) xInput1, pIn1 := registerStreamReader(changeset1) defer unregisterStreamReader(pIn1) xInput2, pIn2 := registerStreamReader(changeset2) defer unregisterStreamReader(pIn2) xOutput, pOut := registerStreamWriter(w) defer unregisterStreamWriter(pOut) res := ResultCode(lib.Xsqlite3changeset_concat_strm(tls, xInput1, pIn1, xInput2, pIn2, xOutput, pOut)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: concatenate changesets: %w", err) } return nil } // A Changegroup is an object used to combine two or more changesets or // patchesets. The zero value is an empty changegroup. // // https://www.sqlite.org/session/changegroup.html type Changegroup struct { tls *libc.TLS ptr uintptr } // NewChangegroup returns a new changegroup. The caller is responsible for // calling Clear on the returned changegroup. // // https://www.sqlite.org/session/sqlite3changegroup_new.html // // Deprecated: Use new(sqlite.Changegroup) instead, which does not require // calling Clear until Add is called. func NewChangegroup() (*Changegroup, error) { cg := new(Changegroup) if err := cg.init(); err != nil { return nil, fmt.Errorf("sqlite: %w", err) } return cg, nil } func (cg *Changegroup) init() error { if cg.tls == nil { cg.tls = libc.NewTLS() } if cg.ptr == 0 { pp, err := malloc(cg.tls, ptrSize) if err != nil { cg.tls.Close() cg.tls = nil return fmt.Errorf("init changegroup: %v", err) } defer libc.Xfree(cg.tls, pp) initlib(cg.tls) res := ResultCode(lib.Xsqlite3changegroup_new(cg.tls, pp)) if err := res.ToError(); err != nil { cg.tls.Close() cg.tls = nil return fmt.Errorf("init changegroup: %w", err) } cg.ptr = *(*uintptr)(unsafe.Pointer(pp)) } return nil } // Clear empties the changegroup and releases any resources associated with // the changegroup. This method may be called multiple times. func (cg *Changegroup) Clear() { if cg == nil { return } if cg.ptr != 0 { lib.Xsqlite3changegroup_delete(cg.tls, cg.ptr) cg.ptr = 0 } if cg.tls != nil { cg.tls.Close() cg.tls = nil } } // Add adds all changes within the changeset (or patchset) read from r to // the changegroup. Once Add has been called, it is the caller's responsibility // to call Clear. // // https://www.sqlite.org/session/sqlite3changegroup_add.html func (cg *Changegroup) Add(r io.Reader) error { if err := cg.init(); err != nil { return fmt.Errorf("sqlite: add to changegroup: %w", err) } xInput, pIn := registerStreamReader(r) defer unregisterStreamReader(pIn) res := ResultCode(lib.Xsqlite3changegroup_add_strm(cg.tls, cg.ptr, xInput, pIn)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: add to changegroup: %w", err) } return nil } // WriteTo writes the current contents of the changegroup to w. // // https://www.sqlite.org/session/sqlite3changegroup_output.html func (cg *Changegroup) WriteTo(w io.Writer) (n int64, err error) { // We want to allow uninitialized changegroups to write output without // forcing the caller to call Clear. In theses cases, we initialize a new // changegroup that lasts for the length of the WriteTo call. if cg == nil { cg = new(Changegroup) } if cg.ptr == 0 { defer cg.Clear() } if err := cg.init(); err != nil { return 0, fmt.Errorf("sqlite: write changegroup: %w", err) } wc := &writeCounter{Writer: w} xOutput, pOut := registerStreamWriter(wc) defer unregisterStreamWriter(pOut) res := ResultCode(lib.Xsqlite3changegroup_output_strm(cg.tls, cg.ptr, xOutput, pOut)) if err := res.ToError(); err != nil { return wc.n, fmt.Errorf("sqlite: write changegroup: %w", err) } return wc.n, nil } // A ConflictHandler function determines the action to take to resolve a // conflict while applying a changeset. // // https://www.sqlite.org/session/sqlite3changeset_apply.html type ConflictHandler func(ConflictType, *ChangesetIterator) ConflictAction // ConflictType is an enumeration of changeset conflict types. // // https://www.sqlite.org/session/c_changeset_conflict.html type ConflictType int32 // Conflict types. const ( ChangesetData = ConflictType(lib.SQLITE_CHANGESET_DATA) ChangesetNotFound = ConflictType(lib.SQLITE_CHANGESET_NOTFOUND) ChangesetConflict = ConflictType(lib.SQLITE_CHANGESET_CONFLICT) ChangesetConstraint = ConflictType(lib.SQLITE_CHANGESET_CONSTRAINT) ChangesetForeignKey = ConflictType(lib.SQLITE_CHANGESET_FOREIGN_KEY) ) // String returns the C constant name of the conflict type. func (code ConflictType) String() string { switch code { case ChangesetData: return "SQLITE_CHANGESET_DATA" case ChangesetNotFound: return "SQLITE_CHANGESET_NOTFOUND" case ChangesetConflict: return "SQLITE_CHANGESET_CONFLICT" case ChangesetConstraint: return "SQLITE_CHANGESET_CONSTRAINT" case ChangesetForeignKey: return "SQLITE_CHANGESET_FOREIGN_KEY" default: return fmt.Sprintf("ConflictType(%d)", int32(code)) } } // ConflictAction is an enumeration of actions that can be taken in response to // a changeset conflict. The zero value is ChangesetOmit. // // https://www.sqlite.org/session/c_changeset_abort.html type ConflictAction int32 // Conflict actions. const ( // ChangesetOmit signals that no special action should be taken. The change // that caused the conflict will not be applied. The session module continues // to the next change in the changeset. ChangesetOmit = ConflictAction(lib.SQLITE_CHANGESET_OMIT) // ChangesetAbort signals that any changes applied so far should be rolled // back and the call to ApplyChangeset returns an error whose code // is ResultAbort. ChangesetAbort = ConflictAction(lib.SQLITE_CHANGESET_ABORT) // ChangesetReplace signals a different action depending on the conflict type. // // If the conflict type is ChangesetData, ChangesetReplace signals the // conflicting row should be updated or deleted. // // If the conflict type is ChangesetConflict, then ChangesetReplace signals // that the conflicting row should be removed from the database and a second // attempt to apply the change should be made. If this second attempt fails, // the original row is restored to the database before continuing. // // For all other conflict types, returning ChangesetReplace will cause // ApplyChangeset to roll back any changes applied so far and return an error // whose code is ResultMisuse. ChangesetReplace = ConflictAction(lib.SQLITE_CHANGESET_REPLACE) ) // String returns the C constant name of the conflict action. func (code ConflictAction) String() string { switch code { case ChangesetOmit: return "SQLITE_CHANGESET_OMIT" case ChangesetAbort: return "SQLITE_CHANGESET_ABORT" case ChangesetReplace: return "SQLITE_CHANGESET_REPLACE" default: return fmt.Sprintf("ConflictAction(%d)", int32(code)) } } var ( streamReaders sync.Map // map[uintptr]io.Reader streamReadersIDMu sync.Mutex streamReadersIDs idGen ) func registerStreamReader(r io.Reader) (xInput, pIn uintptr) { xInput = cFuncPointer(sessionStreamInput) streamReadersIDMu.Lock() pIn = streamReadersIDs.next() streamReadersIDMu.Unlock() streamReaders.Store(pIn, r) return } func unregisterStreamReader(pIn uintptr) { streamReaders.Delete(pIn) streamReadersIDMu.Lock() streamReadersIDs.reclaim(pIn) streamReadersIDMu.Unlock() } // sessionStreamInput is the callback returned by registerSessionReader used // for the session streaming APIs. // https://www.sqlite.org/session/sqlite3changegroup_add_strm.html func sessionStreamInput(tls *libc.TLS, pIn uintptr, pData uintptr, pnData uintptr) int32 { rval, _ := streamReaders.Load(pIn) r, _ := rval.(io.Reader) if r == nil { return lib.SQLITE_MISUSE } n := int(*(*int32)(unsafe.Pointer(pnData))) n, err := r.Read(libc.GoBytes(pData, n)) *(*int32)(unsafe.Pointer(pnData)) = int32(n) if n == 0 && err != io.EOF { // Readers should not return n == 0 && err == nil. However, as per io.Reader // docs, we can't treat it as an EOF condition. return lib.SQLITE_IOERR_READ } return lib.SQLITE_OK } var ( streamWriters sync.Map // map[uintptr]io.Writer streamWritersIDMu sync.Mutex streamWritersIDs idGen ) func registerStreamWriter(w io.Writer) (xOutput, pOut uintptr) { xOutput = cFuncPointer(sessionStreamOutput) streamWritersIDMu.Lock() pOut = streamWritersIDs.next() streamWritersIDMu.Unlock() streamWriters.Store(pOut, w) return } func unregisterStreamWriter(pOut uintptr) { streamWriters.Delete(pOut) streamWritersIDMu.Lock() streamWritersIDs.reclaim(pOut) streamWritersIDMu.Unlock() } // sessionStreamOutput is the callback returned by registerSessionWriter used // for the session streaming APIs. // https://www.sqlite.org/session/sqlite3changegroup_add_strm.html func sessionStreamOutput(tls *libc.TLS, pOut uintptr, pData uintptr, nData int32) int32 { wval, _ := streamWriters.Load(pOut) w, _ := wval.(io.Writer) if w == nil { return lib.SQLITE_MISUSE } _, err := w.Write(libc.GoBytes(pData, int(nData))) if err != nil { return lib.SQLITE_IOERR_WRITE } return lib.SQLITE_OK } type writeCounter struct { io.Writer n int64 } func (wc *writeCounter) Write(p []byte) (int, error) { n, err := wc.Writer.Write(p) wc.n += int64(n) return n, err } zombiezen-go-sqlite-297af96/session_test.go000066400000000000000000000175701501417116500210530ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package sqlite_test import ( "bytes" "reflect" "testing" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func initT(t *testing.T, conn *sqlite.Conn) { if _, err := conn.Prep(`INSERT INTO t (c1, c2, c3) VALUES ('1', '2', '3');`).Step(); err != nil { t.Fatal(err) } if _, err := conn.Prep(`INSERT INTO t (c1, c2, c3) VALUES ('4', '5', '6');`).Step(); err != nil { t.Fatal(err) } } func fillSession(t *testing.T) (*sqlite.Conn, *sqlite.Session) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } if _, err := conn.Prep("CREATE TABLE t (c1 PRIMARY KEY, c2, c3);").Step(); err != nil { t.Fatal(err) } initT(t, conn) // two rows that predate the session s, err := conn.CreateSession("") if err != nil { t.Fatal(err) } if err := s.Attach(""); err != nil { t.Fatal(err) } stmts := []string{ `UPDATE t SET c1='one' WHERE c1='1';`, `UPDATE t SET c2='two', c3='three' WHERE c1='one';`, `UPDATE t SET c1='noop' WHERE c2='2';`, `DELETE FROM t WHERE c1='4';`, `INSERT INTO t (c1, c2, c3) VALUES ('four', 'five', 'six');`, } for _, stmt := range stmts { if _, err := conn.Prep(stmt).Step(); err != nil { t.Fatal(err) } } if _, err := conn.Prep("BEGIN;").Step(); err != nil { t.Fatal(err) } stmt, err := conn.Prepare("INSERT INTO t (c1, c2, c3) VALUES (?,?,?);") if err != nil { t.Fatal(err) } for i := int64(2); i < 100; i++ { stmt.Reset() stmt.BindInt64(1, i) stmt.BindText(2, "column2") stmt.BindText(3, "column3") if _, err := stmt.Step(); err != nil { t.Fatal(err) } } if _, err := conn.Prep("COMMIT;").Step(); err != nil { t.Fatal(err) } return conn, s } func TestFillSession(t *testing.T) { conn, s := fillSession(t) s.Delete() conn.Close() } func TestChangeset(t *testing.T) { conn, s := fillSession(t) defer func() { s.Delete() if err := conn.Close(); err != nil { t.Error(err) } }() buf := new(bytes.Buffer) if err := s.WriteChangeset(buf); err != nil { t.Fatal(err) } b := buf.Bytes() if len(b) == 0 { t.Errorf("changeset has no length") } iter, err := sqlite.NewChangesetIterator(bytes.NewReader(b)) if err != nil { t.Fatal(err) } numChanges := 0 num3Cols := 0 opTypes := make(map[sqlite.OpType]int) for { hasRow, err := iter.Next() if err != nil { t.Fatal(err) } if !hasRow { break } op, err := iter.Operation() if err != nil { t.Fatalf("numChanges=%d, Op err: %v", numChanges, err) } if op.TableName != "t" { t.Errorf("table=%q, want t", op.TableName) } opTypes[op.Type]++ if op.NumColumns == 3 { num3Cols++ } numChanges++ } if numChanges != 102 { t.Errorf("numChanges=%d, want 102", numChanges) } if num3Cols != 102 { t.Errorf("num3Cols=%d, want 102", num3Cols) } if got := opTypes[sqlite.OpInsert]; got != 100 { t.Errorf("num inserts=%d, want 100", got) } if err := iter.Close(); err != nil { t.Fatal(err) } } func TestChangesetInvert(t *testing.T) { conn, s := fillSession(t) defer func() { s.Delete() if err := conn.Close(); err != nil { t.Error(err) } }() buf := new(bytes.Buffer) if err := s.WriteChangeset(buf); err != nil { t.Fatal(err) } b := buf.Bytes() buf = new(bytes.Buffer) if err := sqlite.InvertChangeset(buf, bytes.NewReader(b)); err != nil { t.Fatal(err) } invB := buf.Bytes() if len(invB) == 0 { t.Error("no inverted changeset") } if bytes.Equal(b, invB) { t.Error("inverted changeset is unchanged") } buf = new(bytes.Buffer) if err := sqlite.InvertChangeset(buf, bytes.NewReader(invB)); err != nil { t.Fatal(err) } invinvB := buf.Bytes() if !bytes.Equal(b, invinvB) { t.Error("inv(inv(b)) != b") } } func TestChangesetApply(t *testing.T) { conn, s := fillSession(t) defer func() { s.Delete() if err := conn.Close(); err != nil { t.Error(err) } }() buf := new(bytes.Buffer) if err := s.WriteChangeset(buf); err != nil { t.Fatal(err) } b := buf.Bytes() invBuf := new(bytes.Buffer) if err := sqlite.InvertChangeset(invBuf, bytes.NewReader(b)); err != nil { t.Fatal(err) } // Undo the entire session. conflictHandler := sqlite.ConflictHandler(func(typ sqlite.ConflictType, iter *sqlite.ChangesetIterator) sqlite.ConflictAction { return sqlite.ChangesetOmit }) if err := conn.ApplyChangeset(invBuf, nil, conflictHandler); err != nil { t.Fatal(err) } // Table t should now be equivalent to the first two statements: // INSERT INTO t (c1, c2, c3) VALUES ("1", "2", "3"); // INSERT INTO t (c1, c2, c3) VALUES ("4", "5", "6"); want := []string{"1,2,3", "4,5,6"} var got []string fn := func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnText(0)+","+stmt.ColumnText(1)+","+stmt.ColumnText(2)) return nil } if err := sqlitex.Exec(conn, "SELECT c1, c2, c3 FROM t ORDER BY c1;", fn); err != nil { t.Fatal(err) } if !reflect.DeepEqual(got, want) { t.Errorf("got=%v, want=%v", got, want) } } func TestPatchsetApply(t *testing.T) { conn, s := fillSession(t) defer func() { if s != nil { s.Delete() } if err := conn.Close(); err != nil { t.Error(err) } }() var rowCountBefore int fn := func(stmt *sqlite.Stmt) error { rowCountBefore = stmt.ColumnInt(0) return nil } if err := sqlitex.Exec(conn, "SELECT COUNT(*) FROM t;", fn); err != nil { t.Fatal(err) } buf := new(bytes.Buffer) if err := s.WritePatchset(buf); err != nil { t.Fatal(err) } b := buf.Bytes() s.Delete() s = nil if _, err := conn.Prep("DELETE FROM t;").Step(); err != nil { t.Fatal(err) } initT(t, conn) filterFnCalled := false filterFn := func(tableName string) bool { if tableName == "t" { filterFnCalled = true return true } else { t.Errorf("unexpected table in filter fn: %q", tableName) return false } } conflictFn := func(sqlite.ConflictType, *sqlite.ChangesetIterator) sqlite.ConflictAction { t.Error("conflict applying patchset") return sqlite.ChangesetAbort } if err := conn.ApplyChangeset(bytes.NewReader(b), filterFn, conflictFn); err != nil { t.Fatal(err) } if !filterFnCalled { t.Error("filter function not called") } var rowCountAfter int fn = func(stmt *sqlite.Stmt) error { rowCountAfter = stmt.ColumnInt(0) return nil } if err := sqlitex.Exec(conn, "SELECT COUNT(*) FROM t;", fn); err != nil { t.Fatal(err) } if rowCountBefore != rowCountAfter { t.Errorf("row count is %d, want %d", rowCountAfter, rowCountBefore) } // Second application of patchset should fail. haveConflict := false conflictFn = func(ct sqlite.ConflictType, iter *sqlite.ChangesetIterator) sqlite.ConflictAction { if ct == sqlite.ChangesetConflict { haveConflict = true } else { t.Errorf("unexpected conflict type: %v", ct) } op, err := iter.Operation() if err != nil { t.Errorf("conflict iter.Op() error: %v", err) return sqlite.ChangesetAbort } if op.Type != sqlite.OpInsert { t.Errorf("unexpected conflict op type: %v", op.Type) } return sqlite.ChangesetAbort } err := conn.ApplyChangeset(bytes.NewReader(b), nil, conflictFn) if code := sqlite.ErrCode(err); code != sqlite.ResultAbort { t.Errorf("conflicting changeset Apply error is %v, want %v", err, sqlite.ResultAbort) } if !haveConflict { t.Error("no conflict found") } } zombiezen-go-sqlite-297af96/shell/000077500000000000000000000000001501417116500170775ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/shell/example_test.go000066400000000000000000000010131501417116500221130ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package shell_test import ( "fmt" "os" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/shell" ) // This is a small program that emulates the behavior of the sqlite3 CLI. // A path to a database can be passed on the command-line. func Example() { dbName := ":memory:" if len(os.Args) > 1 { dbName = os.Args[1] } conn, err := sqlite.OpenConn(dbName) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } shell.Run(conn) conn.Close() } zombiezen-go-sqlite-297af96/shell/shell.go000066400000000000000000000060461501417116500205430ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Package shell provides a minimal SQLite REPL, similar to the built-in one. // This is useful for providing a REPL with custom functions. package shell import ( "fmt" "io" "os" "strings" "unicode" "github.com/chzyer/readline" "modernc.org/libc" lib "modernc.org/sqlite/lib" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) const ( prompt = "sqlite> " continuationPrompt = " ...> " ) // Run runs an interactive shell on the process's standard I/O. func Run(conn *sqlite.Conn) { tls := libc.NewTLS() defer tls.Close() rl, err := readline.NewEx(&readline.Config{ Prompt: prompt, }) if err != nil { fmt.Fprintln(os.Stderr, err) return } defer rl.Close() if readline.DefaultIsTerminal() { fmt.Printf("SQLite version %s\n", sqlite.Version) } var sql string for { if len(sql) > 0 { rl.SetPrompt(continuationPrompt) } else { rl.SetPrompt(prompt) } line, err := rl.Readline() if err == io.EOF { break } if err != nil { fmt.Fprintln(os.Stderr, err) return } if len(sql) == 0 && strings.HasPrefix(line, ".") { wordEnd := strings.IndexFunc(line, unicode.IsSpace) if wordEnd == -1 { wordEnd = len(line) } switch word := line[1:wordEnd]; word { case "schema": err := sqlitex.ExecuteTransient(conn, `SELECT sql FROM sqlite_master;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Println(stmt.ColumnText(0) + ";") return nil }, }) if err != nil { fmt.Fprintln(os.Stderr, err) } case "quit": return default: fmt.Fprintf(os.Stderr, "unknown command .%s\n", word) } continue } line = strings.TrimLeftFunc(line, unicode.IsSpace) if line == "" { continue } sql += line + "\n" if !strings.Contains(line, ";") { continue } for isCompleteStmt(tls, sql) { sql = strings.TrimLeftFunc(sql, unicode.IsSpace) if sql == "" { break } stmt, trailingBytes, err := conn.PrepareTransient(sql) sql = sql[len(sql)-trailingBytes:] if err != nil { fmt.Fprintln(os.Stderr, err) continue } for { hasData, err := stmt.Step() if err != nil { fmt.Fprintln(os.Stderr, err) break } if !hasData { break } row := new(strings.Builder) for i, n := 0, stmt.ColumnCount(); i < n; i++ { if i > 0 { row.WriteString("|") } switch stmt.ColumnType(i) { case sqlite.TypeInteger: fmt.Fprint(row, stmt.ColumnInt64(i)) case sqlite.TypeFloat: fmt.Fprint(row, stmt.ColumnFloat(i)) case sqlite.TypeBlob: buf := make([]byte, stmt.ColumnLen(i)) stmt.ColumnBytes(i, buf) row.Write(buf) case sqlite.TypeText: row.WriteString(stmt.ColumnText(i)) } } fmt.Println(row) } stmt.Finalize() } } } func isCompleteStmt(tls *libc.TLS, s string) bool { if s == "" { return true } c, err := libc.CString(s + ";") if err != nil { panic(err) } defer libc.Xfree(tls, c) return lib.Xsqlite3_complete(tls, c) != 0 } zombiezen-go-sqlite-297af96/sqlite.go000066400000000000000000001230301501417116500176170ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite import ( "bytes" "fmt" "sync" "time" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" lib "modernc.org/sqlite/lib" ) // Version is the SQLite version in the format "X.Y.Z" where X is the major // version number (always 3), Y is the minor version number, and Z is the // release number. const Version = lib.SQLITE_VERSION // VersionNumber is an integer with the value (X*1000000 + Y*1000 + Z) where X // is the major version number (always 3), Y is the minor version number, and Z // is the release number. const VersionNumber = lib.SQLITE_VERSION_NUMBER var initOnce sync.Once func initlib(tls *libc.TLS) { initOnce.Do(func() { lib.Xsqlite3_initialize(tls) }) } // Conn is an open connection to an SQLite3 database. // // A Conn can only be used by one goroutine at a time. type Conn struct { tls *libc.TLS conn uintptr stmts map[string]*Stmt // query -> prepared statement closed bool cancelCh chan struct{} doneCh <-chan struct{} unlockNote uintptr file string line int } const ptrSize = types.Size_t(unsafe.Sizeof(uintptr(0))) // OpenConn opens a single SQLite database connection with the given flags. // No flags or a value of 0 defaults to the following: // // - [OpenReadWrite] // - [OpenCreate] // - [OpenWAL] // - [OpenURI] // // The OpenWAL flag is an extension specific to this library: // it runs "PRAGMA journal_mode=wal;" before returning. // While convenient, this constitutes a write transaction // and may fail if the database is contended. // A default timeout of a few seconds is used // so that OpenConn does not block indefinitely. // If you want to have more control over the timeout on setting WAL, // do not pass OpenWAL and execute "PRAGMA journal_mode=wal;" yourself. // // https://www.sqlite.org/c3ref/open.html func OpenConn(path string, flags ...OpenFlags) (*Conn, error) { var openFlags OpenFlags for _, f := range flags { openFlags |= f } if openFlags == 0 { openFlags = OpenReadWrite | OpenCreate | OpenWAL | OpenURI } c, err := openConn(path, openFlags&^(OpenWAL|OpenFullMutex)|OpenNoMutex) if err != nil { return nil, err } // Disable double-quoted string literals. varArgs := libc.NewVaList(int32(0), uintptr(0)) if varArgs == 0 { c.Close() return nil, fmt.Errorf("sqlite: open %q: cannot allocate memory", path) } defer libc.Xfree(c.tls, varArgs) res := ResultCode(lib.Xsqlite3_db_config( c.tls, c.conn, lib.SQLITE_DBCONFIG_DQS_DDL, varArgs, )) if err := res.ToError(); err != nil { // Making error opaque because it's not part of the primary connection // opening and reflects an internal error. c.Close() return nil, fmt.Errorf("sqlite: open %q: disable double-quoted string literals: %v", path, err) } res = ResultCode(lib.Xsqlite3_db_config( c.tls, c.conn, lib.SQLITE_DBCONFIG_DQS_DML, varArgs, )) if err := res.ToError(); err != nil { // Making error opaque because it's not part of the primary connection // opening and reflects an internal error. c.Close() return nil, fmt.Errorf("sqlite: open %q: disable double-quoted string literals: %v", path, err) } if openFlags&OpenWAL != 0 { // Set timeout for enabling WAL. // See https://github.com/crawshaw/sqlite/pull/113 for details. c.SetBusyTimeout(10 * time.Second) stmt, _, err := c.PrepareTransient("PRAGMA journal_mode=wal;") if err != nil { c.Close() return nil, fmt.Errorf("sqlite: open %q: %w", path, err) } _, err = stmt.Step() stmt.Finalize() if err != nil { c.Close() return nil, fmt.Errorf("sqlite: open %q: enable wal: %w", path, err) } } c.SetBlockOnBusy() return c, nil } var allConns struct { mu sync.RWMutex table map[uintptr]*Conn } func openConn(path string, openFlags OpenFlags) (_ *Conn, err error) { tls := libc.NewTLS() defer func() { if err != nil { tls.Close() } }() initlib(tls) unlockNote, err := allocUnlockNote(tls) if err != nil { return nil, fmt.Errorf("sqlite: open %q: %w", path, err) } defer func() { if err != nil { libc.Xfree(tls, unlockNote) } }() cpath, err := libc.CString(path) if err != nil { return nil, fmt.Errorf("sqlite: open %q: %w", path, err) } defer libc.Xfree(tls, cpath) connPtr, err := malloc(tls, ptrSize) if err != nil { return nil, fmt.Errorf("sqlite: open %q: %w", path, err) } defer libc.Xfree(tls, connPtr) res := ResultCode(lib.Xsqlite3_open_v2(tls, cpath, connPtr, int32(openFlags), 0)) c := &Conn{ tls: tls, conn: *(*uintptr)(unsafe.Pointer(connPtr)), stmts: make(map[string]*Stmt), unlockNote: unlockNote, } if c.conn == 0 { // Not enough memory to allocate the sqlite3 object. return nil, fmt.Errorf("sqlite: open %q: %w", path, res.ToError()) } if res != ResultOK { // sqlite3_open_v2 may still return a sqlite3* just so we can extract the error. extres := ResultCode(lib.Xsqlite3_extended_errcode(tls, c.conn)) if extres != 0 { res = extres } lib.Xsqlite3_close_v2(tls, c.conn) return nil, fmt.Errorf("sqlite: open %q: %w", path, res.ToError()) } lib.Xsqlite3_extended_result_codes(tls, c.conn, 1) allConns.mu.Lock() if allConns.table == nil { allConns.table = make(map[uintptr]*Conn) } allConns.table[c.conn] = c allConns.mu.Unlock() return c, nil } // Close closes the database connection using sqlite3_close and finalizes // persistent prepared statements. https://www.sqlite.org/c3ref/close.html func (c *Conn) Close() error { if c == nil { return fmt.Errorf("sqlite: close: nil connection") } if c.closed { return fmt.Errorf("sqlite: close: already closed") } c.cancelInterrupt() c.closed = true for _, stmt := range c.stmts { stmt.Finalize() } res := ResultCode(lib.Xsqlite3_close(c.tls, c.conn)) libc.Xfree(c.tls, c.unlockNote) c.unlockNote = 0 c.tls.Close() c.tls = nil c.releaseAuthorizer() busyHandlers.Delete(c.conn) allConns.mu.Lock() delete(allConns.table, c.conn) allConns.mu.Unlock() if !res.IsSuccess() { return fmt.Errorf("sqlite: close: %w", res.ToError()) } return nil } // AutocommitEnabled reports whether conn is in autocommit mode. // https://sqlite.org/c3ref/get_autocommit.html func (c *Conn) AutocommitEnabled() bool { if c == nil { return false } return lib.Xsqlite3_get_autocommit(c.tls, c.conn) != 0 } // CheckReset reports whether any statement on this connection is in the process // of returning results. func (c *Conn) CheckReset() string { if c != nil { for _, stmt := range c.stmts { if stmt.lastHasRow { return stmt.query } } } return "" } // SetInterrupt assigns a channel to control connection execution lifetime. // // When doneCh is closed, the connection uses sqlite3_interrupt to // stop long-running queries and cancels any *Stmt.Step calls that // are blocked waiting for the database write lock. // // Subsequent uses of the connection will return SQLITE_INTERRUPT // errors until doneCh is reset with a subsequent call to SetInterrupt. // // Any busy statements at the time SetInterrupt is called will be reset. // // SetInterrupt returns the old doneCh assigned to the connection. func (c *Conn) SetInterrupt(doneCh <-chan struct{}) (oldDoneCh <-chan struct{}) { if c == nil { return nil } if c.closed { panic("sqlite.Conn is closed") } oldDoneCh = c.doneCh c.cancelInterrupt() c.doneCh = doneCh for _, stmt := range c.stmts { if stmt.lastHasRow { stmt.Reset() } } if doneCh == nil { return oldDoneCh } cancelCh := make(chan struct{}) c.cancelCh = cancelCh go func() { select { case <-doneCh: lib.Xsqlite3_interrupt(c.tls, c.conn) fireUnlockNote(c.tls, c.unlockNote) <-cancelCh cancelCh <- struct{}{} case <-cancelCh: cancelCh <- struct{}{} } }() return oldDoneCh } // SetBusyTimeout sets a busy handler that sleeps for up to d to acquire a lock. // Passing a non-positive value will turn off all busy handlers. // // By default, connections are opened with SetBlockOnBusy, // with the assumption that programs use SetInterrupt to control timeouts. // // https://www.sqlite.org/c3ref/busy_timeout.html func (c *Conn) SetBusyTimeout(d time.Duration) { if c != nil { lib.Xsqlite3_busy_timeout(c.tls, c.conn, int32(d/time.Millisecond)) busyHandlers.Delete(c.conn) } } // SetBlockOnBusy sets a busy handler that waits to acquire a lock // until the connection is interrupted (see SetInterrupt). // // By default, connections are opened with SetBlockOnBusy, // with the assumption that programs use SetInterrupt to control timeouts. // // https://www.sqlite.org/c3ref/busy_handler.html func (c *Conn) SetBlockOnBusy() { if c == nil { return } c.setBusyHandler(func(count int) bool { if count >= len(busyDelays) { count = len(busyDelays) - 1 } t := time.NewTimer(busyDelays[count]) defer t.Stop() select { case <-t.C: return true case <-c.doneCh: // ^ Assuming that doneCh won't be set by SetInterrupt concurrently // with other operations. return false } }) } var busyDelays = [...]time.Duration{ 1 * time.Millisecond, 2 * time.Millisecond, 5 * time.Millisecond, 10 * time.Millisecond, 15 * time.Millisecond, 20 * time.Millisecond, 25 * time.Millisecond, 25 * time.Millisecond, 25 * time.Millisecond, 50 * time.Millisecond, 50 * time.Millisecond, 100 * time.Millisecond, } var busyHandlers sync.Map // sqlite3* -> func(int) bool func (c *Conn) setBusyHandler(handler func(count int) bool) { if c == nil { return } if handler == nil { lib.Xsqlite3_busy_handler(c.tls, c.conn, 0, 0) busyHandlers.Delete(c.conn) return } busyHandlers.Store(c.conn, handler) xBusy := cFuncPointer(busyHandlerCallback) lib.Xsqlite3_busy_handler(c.tls, c.conn, xBusy, c.conn) } func busyHandlerCallback(tls *libc.TLS, pArg uintptr, count int32) int32 { val, _ := busyHandlers.Load(pArg) if val == nil { return 0 } f := val.(func(int) bool) if !f(int(count)) { return 0 } return 1 } func (c *Conn) interrupted() error { select { case <-c.doneCh: return ResultInterrupt.ToError() default: return nil } } func (c *Conn) cancelInterrupt() { if c.cancelCh != nil { c.cancelCh <- struct{}{} <-c.cancelCh c.cancelCh = nil } } // Prep returns a persistent SQL statement. // // Any error in preparation will panic. // // Persistent prepared statements are cached by the query // string in a Conn. If Finalize is not called, then subsequent // calls to Prepare will return the same statement. // // https://www.sqlite.org/c3ref/prepare.html func (c *Conn) Prep(query string) *Stmt { stmt, err := c.Prepare(query) if err != nil { if ErrCode(err) == ResultInterrupt { return &Stmt{ conn: c, query: query, colNames: make(map[string]int), prepInterrupt: true, } } panic(err) } return stmt } // Prepare prepares a persistent SQL statement. // // Persistent prepared statements are cached by the query // string in a Conn. If Finalize is not called, then subsequent // calls to Prepare will return the same statement. // // If the query has any unprocessed trailing bytes, Prepare // returns an error. // // https://www.sqlite.org/c3ref/prepare.html func (c *Conn) Prepare(query string) (*Stmt, error) { if c == nil { return nil, fmt.Errorf("sqlite: prepare %q: nil connection", query) } if stmt := c.stmts[query]; stmt != nil { if err := stmt.Reset(); err != nil { return nil, err } if err := stmt.ClearBindings(); err != nil { return nil, err } return stmt, nil } stmt, trailingBytes, err := c.prepare(query, lib.SQLITE_PREPARE_PERSISTENT) if err != nil { return nil, fmt.Errorf("sqlite: prepare %q: %w", query, err) } if trailingBytes != 0 { stmt.Finalize() return nil, fmt.Errorf("sqlite: prepare %q: statement has trailing bytes", query) } c.stmts[query] = stmt return stmt, nil } // PrepareTransient prepares an SQL statement that is not cached by // the Conn. Subsequent calls with the same query will create new Stmts. // Finalize must be called by the caller once done with the Stmt. // // The number of trailing bytes not consumed from query is returned. // // To run a sequence of queries once as part of a script, // the sqlitex package provides an ExecScript function built on this. // // https://www.sqlite.org/c3ref/prepare.html func (c *Conn) PrepareTransient(query string) (stmt *Stmt, trailingBytes int, err error) { if c == nil { return nil, 0, fmt.Errorf("sqlite: prepare: nil connection") } // TODO(soon) // if stmt != nil { // runtime.SetFinalizer(stmt, func(stmt *Stmt) { // if stmt.conn != nil { // panic("open *sqlite.Stmt \"" + query + "\" garbage collected, call Finalize") // } // }) // } stmt, trailingBytes, err = c.prepare(query, 0) if err != nil { err = fmt.Errorf("sqlite: prepare: %w", err) } return stmt, trailingBytes, err } func (c *Conn) prepare(query string, flags uint32) (*Stmt, int, error) { if err := c.interrupted(); err != nil { return nil, 0, err } cquery, err := libc.CString(query) if err != nil { return nil, 0, err } defer libc.Xfree(c.tls, cquery) ctrailingPtr, err := malloc(c.tls, ptrSize) if err != nil { return nil, 0, err } defer libc.Xfree(c.tls, ctrailingPtr) stmtPtr, err := malloc(c.tls, ptrSize) if err != nil { return nil, 0, err } defer libc.Xfree(c.tls, stmtPtr) res := ResultCode(lib.Xsqlite3_prepare_v3(c.tls, c.conn, cquery, -1, flags, stmtPtr, ctrailingPtr)) if err := c.extreserr(res); err != nil { if offset, ok := ErrorOffset(err); ok && offset <= len(query) { line, col := linecol(query[:offset]) err = fmt.Errorf("%d:%d: %w", line, col, err) } return nil, 0, err } ctrailing := *(*uintptr)(unsafe.Pointer(ctrailingPtr)) trailingBytes := len(query) - int(ctrailing-cquery) stmt := &Stmt{ conn: c, query: query, stmt: *(*uintptr)(unsafe.Pointer(stmtPtr)), } stmt.bindNames = make([]string, lib.Xsqlite3_bind_parameter_count(c.tls, stmt.stmt)) for i := range stmt.bindNames { cname := lib.Xsqlite3_bind_parameter_name(c.tls, stmt.stmt, int32(i+1)) if cname != 0 { stmt.bindNames[i] = libc.GoString(cname) } } colCount := int(lib.Xsqlite3_column_count(c.tls, stmt.stmt)) stmt.colNames = make(map[string]int, colCount) for i := 0; i < colCount; i++ { cname := lib.Xsqlite3_column_name(c.tls, stmt.stmt, int32(i)) if cname != 0 { stmt.colNames[libc.GoString(cname)] = i } } return stmt, trailingBytes, nil } // Changes reports the number of rows affected by the most recent statement. // // https://www.sqlite.org/c3ref/changes.html func (c *Conn) Changes() int { if c == nil { return 0 } return int(lib.Xsqlite3_changes(c.tls, c.conn)) } // LastInsertRowID reports the rowid of the most recently successful INSERT. // // https://www.sqlite.org/c3ref/last_insert_rowid.html func (c *Conn) LastInsertRowID() int64 { if c == nil { return 0 } return lib.Xsqlite3_last_insert_rowid(c.tls, c.conn) } // Serialize serializes the database with the given name (e.g. "main" or "temp"). func (c *Conn) Serialize(dbName string) ([]byte, error) { if c == nil { return nil, fmt.Errorf("sqlite: serialize %q: nil connection", dbName) } zSchema, cleanup, err := cDBName(dbName) if err != nil { return nil, fmt.Errorf("sqlite: serialize %q: %v", dbName, err) } defer cleanup() piSize := lib.Xsqlite3_malloc(c.tls, int32(unsafe.Sizeof(int64(0)))) if piSize == 0 { return nil, fmt.Errorf("sqlite: serialize %q: memory allocation failure", dbName) } defer lib.Xsqlite3_free(c.tls, piSize) // Optimization: avoid copying if possible. p := lib.Xsqlite3_serialize(c.tls, c.conn, zSchema, piSize, lib.SQLITE_SERIALIZE_NOCOPY) if p == 0 { // Optimization impossible. Have SQLite allocate memory. p = lib.Xsqlite3_serialize(c.tls, c.conn, zSchema, piSize, 0) if p == 0 { return nil, fmt.Errorf("sqlite: serialize %q: unable to serialize", dbName) } defer lib.Xsqlite3_free(c.tls, p) } // Copy data into a Go byte slice. n := *(*int64)(unsafe.Pointer(piSize)) goCopy := make([]byte, n) copy(goCopy, libc.GoBytes(p, int(n))) return goCopy, nil } // Deserialize disconnects the database with the given name (e.g. "main") // and reopens it as an in-memory database based on the serialized data. // The database name must already exist. // It is not possible to deserialize into the TEMP database. func (c *Conn) Deserialize(dbName string, data []byte) error { if c == nil { return fmt.Errorf("sqlite: deserialize to %q: nil connection", dbName) } zSchema, cleanup, err := cDBName(dbName) if err != nil { return fmt.Errorf("sqlite: deserialize to %q: %v", dbName, err) } defer cleanup() n := int64(len(data)) pData := lib.Xsqlite3_malloc64(c.tls, uint64(n)) if pData == 0 { return fmt.Errorf("sqlite: deserialize to %q: memory allocation failure", dbName) } copy(libc.GoBytes(pData, len(data)), data) res := ResultCode(lib.Xsqlite3_deserialize(c.tls, c.conn, zSchema, pData, n, n, lib.SQLITE_DESERIALIZE_FREEONCLOSE|lib.SQLITE_DESERIALIZE_RESIZEABLE)) if !res.IsSuccess() { return fmt.Errorf("sqlite: deserialize to %q: %w", dbName, res.ToError()) } return nil } // extreserr asks SQLite for a string explaining the error. // Only called for errors that are probably program bugs. func (c *Conn) extreserr(res ResultCode) error { if res.IsSuccess() { return nil } return &extendedError{ code: res, offset: int(lib.Xsqlite3_error_offset(c.tls, c.conn)), message: libc.GoString(lib.Xsqlite3_errmsg(c.tls, c.conn)), } } // linecol computes the 1-based line number and column // of a character placed after s. func linecol(s string) (line, col int) { line, col = 1, 1 for _, c := range s { // This is intentionally a bit naive with regards to control characters. // If the user wants greater control, switch c { case '\n': line++ col = 1 case '\t': const tabWidth = 8 locationInTab := (col - 1) % tabWidth col += tabWidth - locationInTab default: col++ } } return } // Stmt is an SQLite3 prepared statement. // // A Stmt is attached to a particular Conn // (and that Conn can only be used by a single goroutine). // // When a Stmt is no longer needed it should be cleaned up // by calling the Finalize method. type Stmt struct { conn *Conn stmt uintptr query string bindNames []string colNames map[string]int bindErr error prepInterrupt bool // set if Prep was interrupted lastHasRow bool // last bool returned by Step } func (stmt *Stmt) interrupted() error { if stmt.prepInterrupt { return ResultInterrupt.ToError() } return stmt.conn.interrupted() } // Finalize deletes a prepared statement. // // Be sure to always call Finalize when done with // a statement created using PrepareTransient. // // Do not call Finalize on a prepared statement that // you intend to prepare again in the future. // // https://www.sqlite.org/c3ref/finalize.html func (stmt *Stmt) Finalize() error { if ptr := stmt.conn.stmts[stmt.query]; ptr == stmt { delete(stmt.conn.stmts, stmt.query) } res := ResultCode(lib.Xsqlite3_finalize(stmt.conn.tls, stmt.stmt)) stmt.conn = nil if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: finalize: %w", err) } return nil } // Reset resets a prepared statement so it can be executed again. // // Note that any parameter values bound to the statement are retained. // To clear bound values, call ClearBindings. // // https://www.sqlite.org/c3ref/reset.html func (stmt *Stmt) Reset() error { stmt.lastHasRow = false var res ResultCode for { res = ResultCode(lib.Xsqlite3_reset(stmt.conn.tls, stmt.stmt)) if res != ResultLockedSharedCache { break } // An SQLITE_LOCKED_SHAREDCACHE error has been seen from sqlite3_reset // in the wild, but so far has eluded exact test case replication. // TODO: write a test for this. if res := waitForUnlockNotify(stmt.conn.tls, stmt.conn.conn, stmt.conn.unlockNote); res != ResultOK { return fmt.Errorf("sqlite: reset: %w", stmt.conn.extreserr(res)) } } if err := stmt.conn.extreserr(res); err != nil { return fmt.Errorf("sqlite: reset: %w", err) } return nil } // ClearBindings clears all bound parameter values on a statement. // // https://www.sqlite.org/c3ref/clear_bindings.html func (stmt *Stmt) ClearBindings() error { if err := stmt.interrupted(); err != nil { return fmt.Errorf("sqlite: clear bindings: %w", err) } res := ResultCode(lib.Xsqlite3_clear_bindings(stmt.conn.tls, stmt.stmt)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: clear bindings: %w", err) } return nil } // Step moves through the statement cursor using sqlite3_step. // // If a row of data is available, rowReturned is reported as true. // If the statement has reached the end of the available data then // rowReturned is false. Thus the status codes SQLITE_ROW and // SQLITE_DONE are reported by the rowReturned bool, and all other // non-OK status codes are reported as an error. // // If an error value is returned, then the statement has been reset. // // https://www.sqlite.org/c3ref/step.html // // # Shared cache // // As multiple writers are common in multi-threaded programs, // this Step method uses sqlite3_unlock_notify to handle any // SQLITE_LOCKED errors. // // Without the shared cache, SQLite will block for // several seconds while trying to acquire the write lock. // With the shared cache, it returns SQLITE_LOCKED immediately // if the write lock is held by another connection in this process. // Dealing with this correctly makes for an unpleasant programming // experience, so this package does it automatically by blocking // Step until the write lock is relinquished. // // This means Step can block for a very long time. // Use SetInterrupt to control how long Step will block. // // For far more details, see: // // http://www.sqlite.org/unlock_notify.html func (stmt *Stmt) Step() (rowReturned bool, err error) { if stmt.bindErr != nil { err = stmt.bindErr stmt.bindErr = nil stmt.Reset() return false, fmt.Errorf("sqlite: step: %w", err) } rowReturned, err = stmt.step() stmt.lastHasRow = rowReturned if err != nil { lib.Xsqlite3_reset(stmt.conn.tls, stmt.stmt) return rowReturned, fmt.Errorf("sqlite: step: %w", err) } return rowReturned, nil } func (stmt *Stmt) step() (bool, error) { for { if err := stmt.interrupted(); err != nil { return false, err } switch res := ResultCode(lib.Xsqlite3_step(stmt.conn.tls, stmt.stmt)); res.ToPrimary() { case ResultLocked: if res != ResultLockedSharedCache { // don't call waitForUnlockNotify as it might deadlock, see: // https://github.com/crawshaw/sqlite/issues/6 return false, stmt.conn.extreserr(res) } if res := waitForUnlockNotify(stmt.conn.tls, stmt.conn.conn, stmt.conn.unlockNote); !res.IsSuccess() { return false, stmt.conn.extreserr(res) } lib.Xsqlite3_reset(stmt.conn.tls, stmt.stmt) // loop case ResultRow: return true, nil case ResultDone: return false, nil case ResultInterrupt: // TODO: embed some of these errors into the stmt for zero-alloc errors? return false, res.ToError() default: return false, stmt.conn.extreserr(res) } } } func (stmt *Stmt) handleBindErr(prefix string, res ResultCode) { if stmt.bindErr == nil && !res.IsSuccess() { stmt.bindErr = fmt.Errorf("%s: %w", prefix, res.ToError()) } } // DataCount returns the number of columns in the current row of the result // set of prepared statement. // // https://sqlite.org/c3ref/data_count.html func (stmt *Stmt) DataCount() int { return int(lib.Xsqlite3_data_count(stmt.conn.tls, stmt.stmt)) } // ColumnCount returns the number of columns in the result set returned by the // prepared statement. // // https://sqlite.org/c3ref/column_count.html func (stmt *Stmt) ColumnCount() int { return int(lib.Xsqlite3_column_count(stmt.conn.tls, stmt.stmt)) } // ColumnName returns the name assigned to a particular column in the result // set of a SELECT statement. // // https://sqlite.org/c3ref/column_name.html func (stmt *Stmt) ColumnName(col int) string { for name, namedCol := range stmt.colNames { if namedCol == col { return name } } return "" } // BindParamCount reports the number of parameters in stmt. // // https://www.sqlite.org/c3ref/bind_parameter_count.html func (stmt *Stmt) BindParamCount() int { if stmt.stmt == 0 { return 0 } return len(stmt.bindNames) } // BindParamName returns the name of parameter or the empty string if the // parameter is nameless or i is out of range. // // Parameters indices start at 1. // // https://www.sqlite.org/c3ref/bind_parameter_name.html func (stmt *Stmt) BindParamName(i int) string { i-- // map from 1-based to 0-based if i < 0 || i >= len(stmt.bindNames) { return "" } return stmt.bindNames[i] } // BindInt64 binds value to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindInt64(param int, value int64) { if stmt.stmt == 0 { return } res := ResultCode(lib.Xsqlite3_bind_int64(stmt.conn.tls, stmt.stmt, int32(param), value)) stmt.handleBindErr("bind int64", res) } // BindBool binds value (as an integer 0 or 1) to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindBool(param int, value bool) { if stmt.stmt == 0 { return } v := int64(0) if value { v = 1 } res := ResultCode(lib.Xsqlite3_bind_int64(stmt.conn.tls, stmt.stmt, int32(param), v)) stmt.handleBindErr("bind bool", res) } var emptyCString = mustCString("") const sqliteStatic uintptr = 0 // BindBytes binds value to a numbered stmt parameter. // // In-memory copies of value are made using this interface. // For large blobs, consider using the streaming Blob object. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindBytes(param int, value []byte) { if stmt.stmt == 0 { return } if len(value) == 0 { res := ResultCode(lib.Xsqlite3_bind_blob(stmt.conn.tls, stmt.stmt, int32(param), emptyCString, 0, sqliteStatic)) stmt.handleBindErr("bind bytes", res) return } v, err := malloc(stmt.conn.tls, types.Size_t(len(value))) if err != nil { if stmt.bindErr == nil { stmt.bindErr = fmt.Errorf("bind bytes: %w", err) } return } for i, b := range value { *(*byte)(unsafe.Pointer(v + uintptr(i))) = b } res := ResultCode(lib.Xsqlite3_bind_blob(stmt.conn.tls, stmt.stmt, int32(param), v, int32(len(value)), freeFuncPtr)) stmt.handleBindErr("bind bytes", res) } var freeFuncPtr = cFuncPointer(libc.Xfree) // BindText binds value to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindText(param int, value string) { if stmt.stmt == 0 { return } allocSize := types.Size_t(len(value)) if allocSize == 0 { allocSize = 1 } v, err := malloc(stmt.conn.tls, allocSize) if err != nil { if stmt.bindErr == nil { stmt.bindErr = fmt.Errorf("bind text: %w", err) } return } for i := 0; i < len(value); i++ { *(*byte)(unsafe.Pointer(v + uintptr(i))) = value[i] } res := ResultCode(lib.Xsqlite3_bind_text(stmt.conn.tls, stmt.stmt, int32(param), v, int32(len(value)), freeFuncPtr)) stmt.handleBindErr("bind text", res) } // BindFloat binds value to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindFloat(param int, value float64) { if stmt.stmt == 0 { return } res := ResultCode(lib.Xsqlite3_bind_double(stmt.conn.tls, stmt.stmt, int32(param), value)) stmt.handleBindErr("bind float", res) } // BindNull binds an SQL NULL value to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindNull(param int) { if stmt.stmt == 0 { return } res := ResultCode(lib.Xsqlite3_bind_null(stmt.conn.tls, stmt.stmt, int32(param))) stmt.handleBindErr("bind null", res) } // BindZeroBlob binds a blob of zeros of length len to a numbered stmt parameter. // // Parameter indices start at 1. // // https://www.sqlite.org/c3ref/bind_blob.html func (stmt *Stmt) BindZeroBlob(param int, len int64) { if stmt.stmt == 0 { return } res := ResultCode(lib.Xsqlite3_bind_zeroblob64(stmt.conn.tls, stmt.stmt, int32(param), uint64(len))) stmt.handleBindErr("bind zero blob", res) } func (stmt *Stmt) findBindName(prefix string, param string) int { for i, name := range stmt.bindNames { if name == param { return i + 1 // 1-based indices } } if stmt.bindErr == nil { stmt.bindErr = fmt.Errorf("%s: unknown parameter: %s", prefix, param) } return 0 } // SetInt64 binds an int64 to a parameter using a column name. func (stmt *Stmt) SetInt64(param string, value int64) { stmt.BindInt64(stmt.findBindName("SetInt64", param), value) } // SetBool binds a value (as a 0 or 1) to a parameter using a column name. func (stmt *Stmt) SetBool(param string, value bool) { stmt.BindBool(stmt.findBindName("SetBool", param), value) } // SetBytes binds bytes to a parameter using a column name. // An invalid parameter name will cause the call to Step to return an error. func (stmt *Stmt) SetBytes(param string, value []byte) { stmt.BindBytes(stmt.findBindName("SetBytes", param), value) } // SetText binds text to a parameter using a column name. // An invalid parameter name will cause the call to Step to return an error. func (stmt *Stmt) SetText(param string, value string) { stmt.BindText(stmt.findBindName("SetText", param), value) } // SetFloat binds a float64 to a parameter using a column name. // An invalid parameter name will cause the call to Step to return an error. func (stmt *Stmt) SetFloat(param string, value float64) { stmt.BindFloat(stmt.findBindName("SetFloat", param), value) } // SetNull binds a null to a parameter using a column name. // An invalid parameter name will cause the call to Step to return an error. func (stmt *Stmt) SetNull(param string) { stmt.BindNull(stmt.findBindName("SetNull", param)) } // SetZeroBlob binds a zero blob of length len to a parameter using a column name. // An invalid parameter name will cause the call to Step to return an error. func (stmt *Stmt) SetZeroBlob(param string, len int64) { stmt.BindZeroBlob(stmt.findBindName("SetZeroBlob", param), len) } // ColumnInt returns a query result value as an int. // // Note: this method calls sqlite3_column_int64 and then converts the // resulting 64-bits to an int. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnInt(col int) int { return int(stmt.ColumnInt64(col)) } // ColumnInt32 returns a query result value as an int32. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnInt32(col int) int32 { return lib.Xsqlite3_column_int(stmt.conn.tls, stmt.stmt, int32(col)) } // ColumnInt64 returns a query result value as an int64. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnInt64(col int) int64 { return lib.Xsqlite3_column_int64(stmt.conn.tls, stmt.stmt, int32(col)) } // ColumnBool reports whether a query result value is non-zero. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnBool(col int) bool { return stmt.ColumnInt64(col) != 0 } // ColumnBytes reads a query result into buf. // It reports the number of bytes read. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnBytes(col int, buf []byte) int { return copy(buf, stmt.columnBytes(col)) } // ColumnReader creates a byte reader for a query result column. // // The reader directly references C-managed memory that stops // being valid as soon as the statement row resets. func (stmt *Stmt) ColumnReader(col int) *bytes.Reader { // Load the C memory directly into the Reader. // There is no exported method that lets it escape. return bytes.NewReader(stmt.columnBytes(col)) } func (stmt *Stmt) columnBytes(col int) []byte { p := lib.Xsqlite3_column_blob(stmt.conn.tls, stmt.stmt, int32(col)) if p == 0 { return nil } n := stmt.ColumnLen(col) return libc.GoBytes(p, n) } // ColumnType are codes for each of the SQLite fundamental datatypes: // // - 64-bit signed integer // - 64-bit IEEE floating point number // - string // - BLOB // - NULL // // https://www.sqlite.org/c3ref/c_blob.html type ColumnType int // Data types. const ( TypeInteger ColumnType = lib.SQLITE_INTEGER TypeFloat ColumnType = lib.SQLITE_FLOAT TypeText ColumnType = lib.SQLITE_TEXT TypeBlob ColumnType = lib.SQLITE_BLOB TypeNull ColumnType = lib.SQLITE_NULL ) // String returns the SQLite constant name of the type. func (t ColumnType) String() string { switch t { case TypeInteger: return "SQLITE_INTEGER" case TypeFloat: return "SQLITE_FLOAT" case TypeText: return "SQLITE_TEXT" case TypeBlob: return "SQLITE_BLOB" case TypeNull: return "SQLITE_NULL" default: return "" } } // ColumnType returns the datatype code for the initial data // type of the result column. The returned value is one of: // // - SQLITE_INTEGER // - SQLITE_FLOAT // - SQLITE_TEXT // - SQLITE_BLOB // - SQLITE_NULL // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnType(col int) ColumnType { return ColumnType(lib.Xsqlite3_column_type(stmt.conn.tls, stmt.stmt, int32(col))) } // ColumnIsNull reports whether the result column holds NULL. // Column indices start at 0. func (stmt *Stmt) ColumnIsNull(col int) bool { return stmt.ColumnType(col) == TypeNull } // ColumnText returns a query result as a string. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnText(col int) string { n := stmt.ColumnLen(col) return goStringN(lib.Xsqlite3_column_text(stmt.conn.tls, stmt.stmt, int32(col)), n) } // ColumnFloat returns a query result as a float64. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnFloat(col int) float64 { return lib.Xsqlite3_column_double(stmt.conn.tls, stmt.stmt, int32(col)) } // ColumnLen returns the number of bytes in a query result. // // Column indices start at 0. // // https://www.sqlite.org/c3ref/column_blob.html func (stmt *Stmt) ColumnLen(col int) int { return int(lib.Xsqlite3_column_bytes(stmt.conn.tls, stmt.stmt, int32(col))) } func (stmt *Stmt) ColumnDatabaseName(col int) string { return libc.GoString(lib.Xsqlite3_column_database_name(stmt.conn.tls, stmt.stmt, int32(col))) } func (stmt *Stmt) ColumnTableName(col int) string { return libc.GoString(lib.Xsqlite3_column_table_name(stmt.conn.tls, stmt.stmt, int32(col))) } // ColumnIndex returns the index of the column with the given name. // // If there is no column with the given name ColumnIndex returns -1. func (stmt *Stmt) ColumnIndex(colName string) int { col, found := stmt.colNames[colName] if !found { return -1 } return col } // GetInt64 returns a query result value for colName as an int64. func (stmt *Stmt) GetInt64(colName string) int64 { col, found := stmt.colNames[colName] if !found { return 0 } return stmt.ColumnInt64(col) } // GetBool reports whether the query result value for colName is non-zero. func (stmt *Stmt) GetBool(colName string) bool { return stmt.GetInt64(colName) != 0 } // GetBytes reads a query result for colName into buf. // It reports the number of bytes read. func (stmt *Stmt) GetBytes(colName string, buf []byte) int { col, found := stmt.colNames[colName] if !found { return 0 } return stmt.ColumnBytes(col, buf) } // GetReader creates a byte reader for colName. // // The reader directly references C-managed memory that stops // being valid as soon as the statement row resets. func (stmt *Stmt) GetReader(colName string) *bytes.Reader { col, found := stmt.colNames[colName] if !found { return bytes.NewReader(nil) } return stmt.ColumnReader(col) } // GetText returns a query result value for colName as a string. func (stmt *Stmt) GetText(colName string) string { col, found := stmt.colNames[colName] if !found { return "" } return stmt.ColumnText(col) } // GetFloat returns a query result value for colName as a float64. func (stmt *Stmt) GetFloat(colName string) float64 { col, found := stmt.colNames[colName] if !found { return 0 } return stmt.ColumnFloat(col) } // GetLen returns the number of bytes in a query result for colName. func (stmt *Stmt) GetLen(colName string) int { col, found := stmt.colNames[colName] if !found { return 0 } return stmt.ColumnLen(col) } // IsNull reports whether a query result value for colName is NULL. func (stmt *Stmt) IsNull(colName string) bool { col, found := stmt.colNames[colName] if !found { return true } return stmt.ColumnIsNull(col) } func malloc(tls *libc.TLS, n types.Size_t) (uintptr, error) { p := libc.Xmalloc(tls, n) if p == 0 { return 0, fmt.Errorf("out of memory") } return p, nil } func mustCString(s string) uintptr { p, err := libc.CString(s) if err != nil { panic(err) } return p } func goStringN(s uintptr, n int) string { if s == 0 { return "" } b := unsafe.Slice((*byte)(unsafe.Pointer(s)), n) return string(b) } // cFuncPointer converts a function defined by a function declaration to a C pointer. // The result of using cFuncPointer on closures is undefined. func cFuncPointer[T any](f T) uintptr { // This assumes the memory representation described in https://golang.org/s/go11func. // // cFuncPointer does its conversion by doing the following in order: // 1) Create a Go struct containing a pointer to a pointer to // the function. It is assumed that the pointer to the function will be // stored in the read-only data section and thus will not move. // 2) Convert the pointer to the Go struct to a pointer to uintptr through // unsafe.Pointer. This is permitted via Rule #1 of unsafe.Pointer. // 3) Dereference the pointer to uintptr to obtain the function value as a // uintptr. This is safe as long as function values are passed as pointers. return *(*uintptr)(unsafe.Pointer(&struct{ f T }{f})) } // Limit is a category of performance limits. // // https://sqlite.org/c3ref/c_limit_attached.html type Limit int32 // Limit categories. const ( LimitLength Limit = lib.SQLITE_LIMIT_LENGTH LimitSQLLength Limit = lib.SQLITE_LIMIT_SQL_LENGTH LimitColumn Limit = lib.SQLITE_LIMIT_COLUMN LimitExprDepth Limit = lib.SQLITE_LIMIT_EXPR_DEPTH LimitCompoundSelect Limit = lib.SQLITE_LIMIT_COMPOUND_SELECT LimitVDBEOp Limit = lib.SQLITE_LIMIT_VDBE_OP LimitFunctionArg Limit = lib.SQLITE_LIMIT_FUNCTION_ARG LimitAttached Limit = lib.SQLITE_LIMIT_ATTACHED LimitLikePatternLength Limit = lib.SQLITE_LIMIT_LIKE_PATTERN_LENGTH LimitVariableNumber Limit = lib.SQLITE_LIMIT_VARIABLE_NUMBER LimitTriggerDepth Limit = lib.SQLITE_LIMIT_TRIGGER_DEPTH LimitWorkerThreads Limit = lib.SQLITE_LIMIT_WORKER_THREADS ) // String returns the limit's C constant name. func (limit Limit) String() string { switch limit { case LimitLength: return "SQLITE_LIMIT_LENGTH" case LimitSQLLength: return "SQLITE_LIMIT_SQL_LENGTH" case LimitColumn: return "SQLITE_LIMIT_COLUMN" case LimitExprDepth: return "SQLITE_LIMIT_EXPR_DEPTH" case LimitCompoundSelect: return "SQLITE_LIMIT_COMPOUND_SELECT" case LimitVDBEOp: return "SQLITE_LIMIT_VDBE_OP" case LimitFunctionArg: return "SQLITE_LIMIT_FUNCTION_ARG" case LimitAttached: return "SQLITE_LIMIT_ATTACHED" case LimitLikePatternLength: return "SQLITE_LIMIT_LIKE_PATTERN_LENGTH" case LimitVariableNumber: return "SQLITE_LIMIT_VARIABLE_NUMBER" case LimitTriggerDepth: return "SQLITE_LIMIT_TRIGGER_DEPTH" case LimitWorkerThreads: return "SQLITE_LIMIT_WORKER_THREADS" default: return fmt.Sprintf("Limit(%d)", int32(limit)) } } // Limit sets a runtime limit on the connection. The the previous value of the // limit is returned. Pass a negative value to check the limit without changing // it. // // https://sqlite.org/c3ref/limit.html func (c *Conn) Limit(id Limit, value int32) int32 { if c == nil { return 0 } return lib.Xsqlite3_limit(c.tls, c.conn, int32(id), int32(value)) } // SetDefensive sets the "defensive" flag for a database connection. When the // defensive flag is enabled, language features that allow ordinary SQL to // deliberately corrupt the database file are disabled. The disabled features // include but are not limited to the following: // // - The PRAGMA writable_schema=ON statement. // - The PRAGMA journal_mode=OFF statement. // - Writes to the sqlite_dbpage virtual table. // - Direct writes to shadow tables. func (c *Conn) SetDefensive(enabled bool) error { if c == nil { return fmt.Errorf("sqlite: set defensive=%t: nil connection", enabled) } enabledInt := int32(0) if enabled { enabledInt = 1 } varArgs := libc.NewVaList(enabledInt) if varArgs == 0 { return fmt.Errorf("sqlite: set defensive=%t: cannot allocate memory", enabled) } defer libc.Xfree(c.tls, varArgs) res := ResultCode(lib.Xsqlite3_db_config( c.tls, c.conn, lib.SQLITE_DBCONFIG_DEFENSIVE, varArgs, )) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: set defensive=%t: %w", enabled, err) } return nil } zombiezen-go-sqlite-297af96/sqlite_test.go000066400000000000000000000525401501417116500206650ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlite_test import ( "bytes" "context" "fmt" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" "io" "modernc.org/libc" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func TestConn(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, _, err := c.PrepareTransient("CREATE TABLE bartable (foo1 string, foo2 integer);") if err != nil { t.Fatal(err) } hasRow, err := stmt.Step() if err != nil { t.Fatal(err) } if hasRow { t.Errorf("CREATE TABLE reports having a row") } if err := stmt.Finalize(); err != nil { t.Error(err) } fooVals := []string{ "bar", "baz", "bop", } for i, val := range fooVals { stmt, err := c.Prepare("INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);") if err != nil { t.Fatal(err) } stmt.SetText("$f1", val) stmt.SetInt64("$f2", int64(i)) hasRow, err = stmt.Step() if err != nil { t.Errorf("INSERT %q: %v", val, err) } if hasRow { t.Errorf("INSERT %q: has row", val) } } stmt, err = c.Prepare("SELECT foo1, foo2 FROM bartable;") if err != nil { t.Fatal(err) } gotVals := []string{} gotInts := []int64{} for { hasRow, err := stmt.Step() if err != nil { t.Errorf("SELECT: %v", err) } if !hasRow { break } val := stmt.ColumnText(0) if getVal := stmt.GetText("foo1"); getVal != val { t.Errorf(`GetText("foo1")=%q, want %q`, getVal, val) } intVal := stmt.ColumnInt64(1) if getIntVal := stmt.GetInt64("foo2"); getIntVal != intVal { t.Errorf(`GetText("foo2")=%q, want %q`, getIntVal, intVal) } typ := stmt.ColumnType(0) if typ != sqlite.TypeText { t.Errorf(`ColumnType(0)=%q, want %q`, typ, sqlite.TypeText) } intTyp := stmt.ColumnType(1) if intTyp != sqlite.TypeInteger { t.Errorf(`ColumnType(1)=%q, want %q`, intTyp, sqlite.TypeInteger) } gotVals = append(gotVals, val) gotInts = append(gotInts, intVal) } if !reflect.DeepEqual(fooVals, gotVals) { t.Errorf("SELECT foo1: got %v, want %v", gotVals, fooVals) } wantInts := []int64{0, 1, 2} if !reflect.DeepEqual(wantInts, gotInts) { t.Errorf("SELECT foo2: got %v, want %v", gotInts, wantInts) } if err := stmt.Finalize(); err != nil { t.Error(err) } stmt, err = c.Prepare(`SELECT "foo" = 'foo';`) if err == nil { stmt.Finalize() t.Error("Double-quoted string literals are permitted") } } func TestEarlyInterrupt(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() ctx, cancel := context.WithCancel(context.Background()) c.SetInterrupt(ctx.Done()) stmt, _, err := c.PrepareTransient("CREATE TABLE bartable (foo1 string, foo2 integer);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt.Finalize() cancel() stmt, err = c.Prepare("INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);") if err == nil { t.Fatal("Prepare err=nil, want prepare to fail") } if code := sqlite.ErrCode(err); code != sqlite.ResultInterrupt { t.Fatalf("Prepare err=%s, want SQLITE_INTERRUPT", code) } } func TestStmtInterrupt(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() stmt := sqlite.InterruptedStmt(conn, "CREATE TABLE intt (c);") _, err = stmt.Step() if err == nil { t.Fatal("interrupted stmt Step succeeded") } if got := sqlite.ErrCode(err); got != sqlite.ResultInterrupt { t.Errorf("Step err=%v, want SQLITE_INTERRUPT", got) } } func TestInterruptStepReset(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecScript(c, `CREATE TABLE resetint (c); INSERT INTO resetint (c) VALUES (1); INSERT INTO resetint (c) VALUES (2); INSERT INTO resetint (c) VALUES (3);`) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) c.SetInterrupt(ctx.Done()) stmt := c.Prep("SELECT * FROM resetint;") if _, err := stmt.Step(); err != nil { t.Fatal(err) } cancel() // next Step needs to reset stmt if _, err := stmt.Step(); sqlite.ErrCode(err) != sqlite.ResultInterrupt { t.Fatalf("want SQLITE_INTERRUPT, got %v", err) } c.SetInterrupt(nil) stmt = c.Prep("SELECT c FROM resetint ORDER BY c;") if _, err := stmt.Step(); err != nil { t.Fatalf("statement after interrupt reset failed: %v", err) } stmt.Reset() } func TestInterruptReset(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecScript(c, `CREATE TABLE resetint (c); INSERT INTO resetint (c) VALUES (1); INSERT INTO resetint (c) VALUES (2); INSERT INTO resetint (c) VALUES (3);`) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) c.SetInterrupt(ctx.Done()) stmt := c.Prep("SELECT * FROM resetint;") if _, err := stmt.Step(); err != nil { t.Fatal(err) } cancel() c.SetInterrupt(nil) stmt = c.Prep("SELECT c FROM resetint ORDER BY c;") if _, err := stmt.Step(); err != nil { t.Fatalf("statement after interrupt reset failed: %v", err) } stmt.Reset() } func TestTrailingBytes(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() stmt, trailingBytes, err := conn.PrepareTransient("BEGIN; -- 56") if err != nil { t.Error(err) } stmt.Finalize() const want = 6 if trailingBytes != want { t.Errorf("trailingBytes=%d, want %d", trailingBytes, want) } } func TestTrailingBytesError(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() if _, err := conn.Prepare("BEGIN; -- 56"); err == nil { t.Error("expecting error on trailing bytes") } } func TestBadParam(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, err := c.Prepare("CREATE TABLE IF NOT EXISTS badparam (a, b, c);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt, err = c.Prepare("INSERT INTO badparam (a, b, c) VALUES ($a, $b, $c);") if err != nil { t.Fatal(err) } stmt.SetText("$a", "col_a") stmt.SetText("$b", "col_b") stmt.SetText("$badparam", "notaval") stmt.SetText("$c", "col_c") _, err = stmt.Step() if err == nil { t.Fatal("expecting error from bad param name, got no error") } if got := err.Error(); !strings.Contains(got, "$badparam") { t.Errorf(`error does not mention "$badparam": %v`, got) } stmt.Finalize() } func TestParallel(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt := c.Prep("CREATE TABLE testparallel (c);") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("INSERT INTO testparallel (c) VALUES ($c);") stmt.SetText("$c", "text") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt.Reset() if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("SELECT * from testparallel;") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt2 := c.Prep("SELECT count(*) from testparallel;") if hasRow, err := stmt2.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("expecting count row") } if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("expecting second row") } } func TestBindBytes(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() val := make([]byte, 32) copy(val[5:], []byte("hello world")) stmt := c.Prep("CREATE TABLE IF NOT EXISTS bindbytes (c);") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("INSERT INTO bindbytes (c) VALUES ($bytes);") stmt.SetBytes("$bytes", val) if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("SELECT count(*) FROM bindbytes WHERE c = $bytes;") stmt.SetBytes("$bytes", val) if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("SetBytes: result has no row") } if got := stmt.ColumnInt(0); got != 1 { t.Errorf("SetBytes: count is %d, want 1", got) } stmt.Reset() stmt.SetBytes("$bytes", val) if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("SetBytes: result has no row") } if got := stmt.ColumnInt(0); got != 1 { t.Errorf("SetBytes: count is %d, want 1", got) } blob, err := c.OpenBlob("", "bindbytes", "c", 1, false) if err != nil { t.Fatalf("SetBytes: OpenBlob: %v", err) } defer func() { if err := blob.Close(); err != nil { t.Error(err) } }() storedVal, err := io.ReadAll(io.Reader(blob)) if err != nil { t.Fatalf("SetBytes: Read: %v", err) } if !bytes.Equal(val, storedVal) { t.Fatalf("SetBytes: want: %x, got: %x", val, storedVal) } } func TestBindText(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() const val = "column_value" stmt := c.Prep("CREATE TABLE IF NOT EXISTS bindtext (c);") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("INSERT INTO bindtext (c) VALUES ($text);") stmt.SetText("$text", val) if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("SELECT count(*) FROM bindtext WHERE c = $text;") stmt.SetText("$text", val) if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("SetText: result has no row") } if got := stmt.ColumnInt(0); got != 1 { t.Errorf("SetText: count is %d, want 1", got) } stmt.Reset() stmt.SetText("$text", val) if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("SetText: result has no row") } if got := stmt.ColumnInt(0); got != 1 { t.Errorf("SetText: count is %d, want 1", got) } } func TestExtendedCodes(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt := c.Prep("CREATE TABLE IF NOT EXISTS extcodes (c UNIQUE);") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt = c.Prep("INSERT INTO extcodes (c) VALUES ($c);") stmt.SetText("$c", "value1") if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt.Reset() stmt.SetText("$c", "value1") _, err = stmt.Step() if err == nil { t.Fatal("expected UNIQUE error, got nothing") } if got, want := sqlite.ErrCode(err), sqlite.ResultConstraintUnique; got != want { t.Errorf("got err=%s, want %s", got, want) } } func TestSyntaxError(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() stmt, _, err := conn.PrepareTransient(" \nSELECT );") if err == nil { stmt.Finalize() t.Fatal("No error returned") } msg := err.Error() t.Log("Message:", msg) if want := "2:8"; !strings.Contains(msg, want) { t.Errorf("err.Error() = %q; want to contain %q", msg, want) } got, ok := sqlite.ErrorOffset(err) if want := 9; got != want || ok == false { t.Errorf("ErrorOffset(err) = %d, %t; want %d, true", got, ok, want) } } func TestJournalMode(t *testing.T) { dir, err := os.MkdirTemp("", "crawshaw.io") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) tests := []struct { db string mode string flags sqlite.OpenFlags }{ { "test-delete.db", "delete", sqlite.OpenReadWrite | sqlite.OpenCreate, }, { "test-wal.db", "wal", sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, }, { "test-default-wal.db", "wal", 0, }, // memory databases can't have wal, only journal_mode=memory { ":memory:", "memory", 0, }, // temp databases can't have wal, only journal_mode=delete { "", "delete", 0, }, } for _, test := range tests { if test.db != ":memory:" && test.db != "" { test.db = filepath.Join(dir, test.db) } c, err := sqlite.OpenConn(test.db, test.flags) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt := c.Prep("PRAGMA journal_mode;") if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if !hasRow { t.Error("PRAGMA journal_mode: has no row") } if got := stmt.GetText("journal_mode"); got != test.mode { t.Errorf("journal_mode not set properly, got: %s, want: %s", got, test.mode) } } } func TestBusyTimeout(t *testing.T) { dir, err := os.MkdirTemp("", "crawshaw.io") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) db := filepath.Join(dir, "busytest.db") flags := sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL conn0, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer func() { if err := conn0.Close(); err != nil { t.Error(err) } }() conn1, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer func() { if err := conn1.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecScript(conn0, ` CREATE TABLE t (c); INSERT INTO t (c) VALUES (1); `) if err != nil { t.Fatal(err) } c0Lock := func() { if _, err := conn0.Prep("BEGIN;").Step(); err != nil { t.Fatal(err) } if _, err := conn0.Prep("INSERT INTO t (c) VALUES (2);").Step(); err != nil { t.Fatal(err) } } c0Unlock := func() { if _, err := conn0.Prep("COMMIT;").Step(); err != nil { t.Fatal(err) } } c0Lock() done := make(chan struct{}) go func() { _, err = conn1.Prep("INSERT INTO t (c) VALUES (3);").Step() if err != nil { t.Errorf("insert failed: %v", err) } close(done) }() time.Sleep(10 * time.Millisecond) select { case <-done: t.Errorf("done before unlock") default: } c0Unlock() <-done c0Lock() done = make(chan struct{}) go func() { conn1.SetBusyTimeout(5 * time.Millisecond) _, err = conn1.Prep("INSERT INTO t (c) VALUES (4);").Step() if sqlite.ErrCode(err) != sqlite.ResultBusy { t.Errorf("want SQLITE_BUSY got %v", err) } close(done) }() select { case <-done: case <-time.After(1 * time.Second): t.Errorf("short busy timeout got stuck") } c0Unlock() <-done } func TestBlockOnBusy(t *testing.T) { dir := t.TempDir() db := filepath.Join(dir, "busytest.db") const flags = sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL conn0, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer func() { if err := conn0.Close(); err != nil { t.Error(err) } }() conn1, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer func() { if err := conn1.Close(); err != nil { t.Error(err) } }() if _, err := conn0.Prep("BEGIN EXCLUSIVE;").Step(); err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) conn1.SetInterrupt(ctx.Done()) _, err = conn1.Prep("BEGIN EXCLUSIVE;").Step() cancel() if code := sqlite.ErrCode(err).ToPrimary(); code != sqlite.ResultBusy { t.Errorf("Concurrent transaction error: %v (code=%v); want code=%v", err, code, sqlite.ResultBusy) } } func TestColumnIndex(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, err := c.Prepare("CREATE TABLE IF NOT EXISTS columnindex (a, b, c);") if err != nil { t.Fatal(err) } if _, err := stmt.Step(); err != nil { t.Fatal(err) } stmt, err = c.Prepare("SELECT b, 1 AS d, a, c FROM columnindex") if err != nil { t.Fatal(err) } cols := []struct { name string idx int }{ {"a", 2}, {"b", 0}, {"c", 3}, {"d", 1}, {"badcol", -1}, } for _, col := range cols { if got := stmt.ColumnIndex(col.name); got != col.idx { t.Errorf("expected column %s to have index %d, got %d", col.name, col.idx, got) } } stmt.Finalize() } func TestBindParamName(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() stmt, err := c.Prepare("SELECT :foo, :bar;") if err != nil { t.Fatal(err) } defer stmt.Finalize() var got []string for i, n := 1, stmt.BindParamCount(); i <= n; i++ { got = append(got, stmt.BindParamName(i)) } // We don't care what indices SQLite picked, so sort returned names. sort.Strings(got) want := []string{":bar", ":foo"} if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { t.Errorf("names = %q; want %q", got, want) } } // Just to verify that the JSON1 extension is automatically loaded. func TestJSON1Extension(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() got, err := sqlitex.ResultText(c.Prep(`SELECT json(' { "this" : "is", "a": [ "test" ] } ');`)) if err != nil { t.Fatal(err) } if want := `{"this":"is","a":["test"]}`; got != want { t.Errorf("json(...) = %q; want %q", got, want) } } func TestLimit(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() c.Limit(sqlite.LimitSQLLength, 1) stmt, err := c.Prepare("SELECT 1;") if err == nil { stmt.Finalize() t.Fatal("Prepare did not return an error") } if got, want := sqlite.ErrCode(err), sqlite.ResultTooBig; got != want { t.Errorf("sqlite.ErrCode(err) = %v; want %v", got, want) } } func TestSetDefensive(t *testing.T) { c, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecTransient(c, `PRAGMA writable_schema=ON;`, nil) if err != nil { t.Fatal(err) } if err := c.SetDefensive(true); err != nil { t.Error("SetDefensive:", err) } err = sqlitex.ExecTransient(c, `INSERT INTO sqlite_schema (type, name, tbl_name, sql) `+ `VALUES ('table','foo','foo','CREATE TABLE foo (id integer primary key)');`, nil, ) if err == nil { t.Fatal("Inserting into sqlite_schema did not return an error") } else { t.Log("Insert sqlite_schema:", err) } } func TestSerialize(t *testing.T) { c, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecTransient(c, `CREATE TABLE foo (msg TEXT NOT NULL);`, nil) if err != nil { t.Fatal(err) } err = sqlitex.ExecTransient(c, `INSERT INTO foo VALUES ('Hello, World!');`, nil) if err != nil { t.Fatal(err) } data, err := c.Serialize("main") if err != nil { t.Fatal("Serialize:", err) } err = sqlitex.ExecTransient(c, `ATTACH DATABASE ':memory:' AS a;`, nil) if err != nil { t.Fatal(err) } if err := c.Deserialize("a", data); err != nil { t.Error("Deserialize:", err) } const want = "Hello, World!" var nResults int err = sqlitex.ExecTransient(c, `SELECT msg FROM a.foo;`, func(stmt *sqlite.Stmt) error { nResults++ if got := stmt.ColumnText(0); got != want { t.Errorf("msg = %q; want %q", got, want) } return nil }) if err != nil { t.Error(err) } if nResults != 1 { t.Errorf("COUNT(*) = %d; want 1", nResults) } } func TestForeignKey(t *testing.T) { c, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() err = sqlitex.ExecuteTransient(c, `PRAGMA foreign_keys = on;`, nil) if err != nil { t.Fatal(err) } err = sqlitex.ExecuteScript(c, `CREATE TABLE artist( artistid INTEGER PRIMARY KEY, artistname TEXT ); CREATE TABLE track( trackid INTEGER, trackname TEXT, trackartist INTEGER, FOREIGN KEY(trackartist) REFERENCES artist(artistid) );`, nil) if err != nil { t.Fatal(err) } err = sqlitex.ExecuteTransient(c, `INSERT INTO track VALUES(14, 'Mr. Bojangles', 3);`, nil) if err == nil { t.Fatal("No error from breaking foreign key") } else { t.Log("Got (intentional) error:", err) } } func TestMain(m *testing.M) { _ = libc.Environ() // Forces libc.SetEnviron; fixes memory accounting balance for environ(7). libc.MemAuditStart() rc := m.Run() if err := libc.MemAuditReport(); err != nil { fmt.Fprintln(os.Stderr, err) rc = 1 } os.Exit(rc) } zombiezen-go-sqlite-297af96/sqlitefile/000077500000000000000000000000001501417116500201315ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/sqlitefile/buffer.go000066400000000000000000000173761501417116500217470ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitefile import ( "errors" "io" "zombiezen.com/go/sqlite" ) // A Buffer is a variable-sized bytes buffer backed by SQLite blobs. // // The bytes are broken into pages, with the first and last pages // stored in memory, and intermediate pages loaded into blobs. // Unlike a single SQLite blob, a Buffer can grow beyond its initial size. // The blobs are allocated in a temporary table. // // A Buffer is very similar to a [bytes.Buffer]. type Buffer struct { err error conn *sqlite.Conn // cap(rbuf) == cap(wbuf) == blobs[N].Size() rbuf []byte // read buffer roff int // read head position in roff blobs []tblob // blobs storing data between rbuf and wbuf wbuf []byte // write buffer freelist []tblob } type tblob struct { blob *sqlite.Blob rowid int64 } // NewBuffer creates a Buffer with 16KB pages. func NewBuffer(conn *sqlite.Conn) (*Buffer, error) { return NewBufferSize(conn, 16*1024) } // NewBufferSize creates a Buffer with a specified page size. func NewBufferSize(conn *sqlite.Conn, pageSize int) (*Buffer, error) { bb := &Buffer{ conn: conn, rbuf: make([]byte, 0, pageSize), wbuf: make([]byte, 0, pageSize), } stmt := conn.Prep("CREATE TEMP TABLE IF NOT EXISTS BlobBuffer (blob BLOB);") if _, err := stmt.Step(); err != nil { return nil, err } return bb, nil } func (bb *Buffer) alloc() (tblob, error) { if len(bb.freelist) > 0 { b := bb.freelist[len(bb.freelist)-1] bb.freelist = bb.freelist[:len(bb.freelist)-1] return b, nil } stmt := bb.conn.Prep("INSERT INTO BlobBuffer (blob) VALUES ($blob);") stmt.SetZeroBlob("$blob", int64(len(bb.rbuf))) if _, err := stmt.Step(); err != nil { return tblob{}, err } rowid := bb.conn.LastInsertRowID() blob, err := bb.conn.OpenBlob("temp", "BlobBuffer", "blob", rowid, true) if err != nil { return tblob{}, err } return tblob{ blob: blob, rowid: rowid, }, nil } func (bb *Buffer) free(b tblob) { bb.freelist = append(bb.freelist, b) } func (bb *Buffer) wbufEnsureSpace() error { if len(bb.wbuf) < cap(bb.wbuf) { return nil } // Flush the write buffer. if len(bb.blobs) == 0 && bb.roff == len(bb.rbuf) { // Short cut. The write buffer is full, but // there are no on-disk blobs and the read // buffer is empty. So push these bytes // directly to the front of the Buffer. bb.rbuf, bb.wbuf = bb.wbuf, bb.rbuf[:0] bb.roff = 0 } else { tblob, err := bb.alloc() if err != nil { bb.err = err return err } if _, err := tblob.blob.Seek(0, io.SeekStart); err != nil { bb.err = err return err } if _, err := tblob.blob.Write(bb.wbuf); err != nil { bb.err = err return err } bb.blobs = append(bb.blobs, tblob) bb.wbuf = bb.wbuf[:0] } return nil } // WriteByte appends a byte to the buffer, growing it as needed. func (bb *Buffer) WriteByte(c byte) error { if bb.err != nil { return bb.err } if err := bb.wbufEnsureSpace(); err != nil { return err } bb.wbuf = append(bb.wbuf, c) return nil } func (bb *Buffer) UnreadByte() error { if bb.err != nil { return bb.err } if bb.roff == 0 { return errors.New("sqlitex.Buffer: UnreadByte: no byte to unread") } bb.roff-- return nil } // Write appends bytes to the buffer, growing it as needed. func (bb *Buffer) Write(p []byte) (n int, err error) { if bb.err != nil { return 0, bb.err } for len(p) > 0 { if err := bb.wbufEnsureSpace(); err != nil { return n, err } // TODO: shortcut for writing large p directly into a new blob nn := len(p) if rem := cap(bb.wbuf) - len(bb.wbuf); nn > rem { nn = rem } bb.wbuf = append(bb.wbuf, p[:nn]...) // never grows wbuf n += nn p = p[nn:] } return n, nil } // WriteString appends a string to the buffer, growing it as needed. func (bb *Buffer) WriteString(p string) (n int, err error) { if bb.err != nil { return 0, bb.err } for len(p) > 0 { if err := bb.wbufEnsureSpace(); err != nil { return n, err } // TODO: shortcut for writing large p directly into a new blob nn := len(p) if rem := cap(bb.wbuf) - len(bb.wbuf); nn > rem { nn = rem } bb.wbuf = append(bb.wbuf, p[:nn]...) // never grows wbuf n += nn p = p[nn:] } return n, nil } func (bb *Buffer) rbufFill() error { if bb.roff < len(bb.rbuf) { return nil } // Read buffer is empty. Fill it. if len(bb.blobs) > 0 { // Read the first blob entirely into the read buffer. // TODO: shortcut for if len(p) >= blob.Size() bb.roff = 0 bb.rbuf = bb.rbuf[:cap(bb.rbuf)] tblob := bb.blobs[0] bb.blobs = bb.blobs[1:] if _, err := tblob.blob.Seek(0, io.SeekStart); err != nil { bb.err = err return err } if _, err := io.ReadFull(tblob.blob, bb.rbuf); err != nil { bb.err = err return err } bb.free(tblob) return nil } if len(bb.wbuf) > 0 { // No blobs. Swap the write buffer bytes here directly. bb.rbuf, bb.wbuf = bb.wbuf, bb.rbuf[:0] bb.roff = 0 } if bb.roff == len(bb.rbuf) { return io.EOF } return nil } // ReadByte reads a byte from the beginning of the buffer, // or returns io.EOF if the buffer is empty. func (bb *Buffer) ReadByte() (byte, error) { if bb.err != nil { return 0, bb.err } if err := bb.rbufFill(); err != nil { return 0, err } c := bb.rbuf[bb.roff] bb.roff++ return c, nil } // Read reads data from the beginning of the buffer. func (bb *Buffer) Read(p []byte) (n int, err error) { if bb.err != nil { return 0, bb.err } if err := bb.rbufFill(); err != nil { return 0, err } if bb.roff == len(bb.rbuf) { return 0, io.EOF } n = copy(p, bb.rbuf[bb.roff:]) bb.roff += n return n, nil } // Len returns the number of unread bytes written to the buffer. func (bb *Buffer) Len() (n int64) { n = int64(len(bb.rbuf) - bb.roff) n += int64(cap(bb.rbuf) * len(bb.blobs)) n += int64(len(bb.wbuf)) return n } // Cap returns the number of bytes that have been allocated for this buffer, // both in memory as well as in the database. func (bb *Buffer) Cap() (n int64) { pageSize := int64(cap(bb.rbuf)) return (2 + int64(len(bb.blobs)+len(bb.freelist))) * pageSize } // Reset empties the buffer, // but retains the blobs written to the database for future writes. func (bb *Buffer) Reset() { bb.rbuf = bb.rbuf[:0] bb.wbuf = bb.wbuf[:0] bb.roff = 0 bb.freelist = append(bb.freelist, bb.blobs...) bb.blobs = nil } // Close releases all resources associated with the file, // removing the storage from the database. func (bb *Buffer) Close() error { close := func(tblob tblob) { err := tblob.blob.Close() if bb.err == nil { bb.err = err } } for _, tblob := range bb.blobs { close(tblob) } for _, tblob := range bb.freelist { close(tblob) } stmt := bb.conn.Prep("DELETE FROM BlobBuffer WHERE rowid = $rowid;") del := func(tblob tblob) { stmt.Reset() stmt.SetInt64("$rowid", tblob.rowid) if _, err := stmt.Step(); err != nil && bb.err == nil { bb.err = err } } for _, tblob := range bb.blobs { del(tblob) } for _, tblob := range bb.freelist { del(tblob) } bb.blobs = nil bb.freelist = nil return bb.err } zombiezen-go-sqlite-297af96/sqlitefile/buffer_test.go000066400000000000000000000073271501417116500230010ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitefile import ( "bytes" "context" "io" "testing" "crawshaw.io/iox/ioxtest" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) var _ interface { io.Reader io.Writer io.StringWriter io.ByteScanner io.ByteWriter io.Closer } = (*Buffer)(nil) func TestBuffer(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() data := make([]byte, 64*1024+1) for i := 0; i < 64; i++ { data[i*1024] = byte(i) data[i*1024+1023] = byte(i) } buf, err := NewBufferSize(conn, 1024) if err != nil { t.Fatal(err) } if n, err := buf.Write(data); err != nil { t.Fatal(err) } else if n != len(data) { t.Errorf("buf.Write(data) n=%d, want len(data)=%d", n, len(data)) } if l := int(buf.Len()); l != len(data) { t.Errorf("buf.Len()=%d, want %d", l, len(data)) } got1 := make([]byte, 1024) if n, err := buf.Read(got1); err != nil { t.Fatal(err) } else if n != len(got1) { t.Errorf("buf.Read(got1) n=%d, want len(got1)=%d", n, len(got1)) } if !bytes.Equal(got1, data[:len(got1)]) { t.Errorf("got1 does not match, [0]=%d, [1]=%d, [1023]=%d", got1[0], got1[1], got1[1023]) } if l := int(buf.Len()); l != len(data)-len(got1) { t.Errorf("buf.Len()=%d, want %d", l, len(data)-len(got1)) } b := new(bytes.Buffer) b.Write(got1) if _, err := io.Copy(b, buf); err != nil { t.Errorf("io.Copy err=%v", err) } if !bytes.Equal(b.Bytes(), data) { t.Errorf("b.Bytes and data do not match") } buf.Reset() if _, err := buf.Write(got1); err != nil { t.Fatal(err) } b.Reset() if _, err := io.Copy(b, buf); err != nil { t.Fatal(err) } if !bytes.Equal(b.Bytes(), got1) { t.Errorf("b.Bytes and got1 do not match") } if err := buf.Close(); err != nil { t.Fatal(err) } } func TestConcurrentBuffer(t *testing.T) { // Make sure the shared cache table lock does not // apply to blob buffers (because we use temp tables). dbpool, err := sqlitex.NewPool("file::memory:?mode=memory", sqlitex.PoolOptions{ PoolSize: 2, }) if err != nil { t.Fatal(err) } defer dbpool.Close() conn1, err := dbpool.Take(context.Background()) if err != nil { t.Fatal(err) } defer dbpool.Put(conn1) conn2, err := dbpool.Take(context.Background()) if err != nil { t.Fatal(err) } defer dbpool.Put(conn2) b1a, err := NewBuffer(conn1) if err != nil { t.Fatal(err) } defer b1a.Close() b1b, err := NewBuffer(conn1) if err != nil { t.Fatal(err) } defer b1b.Close() b2, err := NewBuffer(conn2) if err != nil { t.Fatal(err) } defer b2.Close() } func TestBufferRand(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() buf, err := NewBufferSize(conn, 1<<18) if err != nil { t.Fatal(err) } ft := ioxtest.Tester{ F1: buf, F2: new(bytes.Buffer), T: t, } ft.Run() if err := buf.Close(); err != nil { t.Fatal(err) } } zombiezen-go-sqlite-297af96/sqlitefile/file.go000066400000000000000000000166121501417116500214050ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC // Package sqlitefile provides bytes buffers backed by a temporary SQLite table. package sqlitefile import ( "fmt" "io" "zombiezen.com/go/sqlite" ) // File is a readable, writable, and seekable series of temporary SQLite blobs. type File struct { err error conn *sqlite.Conn blobs []*sqlite.Blob rowids []int64 off bbpos len bbpos } // NewFile creates a new [File] with a reasonable initial capacity. func NewFile(conn *sqlite.Conn) (*File, error) { return NewFileSize(conn, 16*1024) } // NewFileSize creates a new [File] with the given number of bytes of capacity. func NewFileSize(conn *sqlite.Conn, initSize int) (*File, error) { bb := &File{conn: conn} stmt := conn.Prep("CREATE TEMP TABLE IF NOT EXISTS BlobBuffer (blob BLOB);") if _, err := stmt.Step(); err != nil { return nil, err } if err := bb.addblob(int64(initSize)); err != nil { return nil, err } return bb, nil } func (bb *File) addblob(size int64) error { stmt := bb.conn.Prep("INSERT INTO BlobBuffer (blob) VALUES ($blob);") stmt.SetZeroBlob("$blob", size) if _, err := stmt.Step(); err != nil { return err } rowid := bb.conn.LastInsertRowID() blob, err := bb.conn.OpenBlob("temp", "BlobBuffer", "blob", rowid, true) if err != nil { return err } bb.blobs = append(bb.blobs, blob) bb.rowids = append(bb.rowids, rowid) return nil } // grow adds an sqlite blob if the buffer is out of space. func (bb *File) grow() error { lastSize := bb.blobs[len(bb.blobs)-1].Size() size := lastSize * 2 if err := bb.addblob(size); err != nil { return err } return nil } // rem reports the remaining available bytes in the pointed-to blob func (bb *File) rem(pos bbpos) int64 { return bb.blobs[pos.index].Size() - pos.pos } func (bb *File) eq(p1, p2 bbpos) bool { if p1 == p2 { return true } if p1.index == p2.index+1 && bb.rem(p1) == 0 && p2.pos == 0 { return true } if p2.index == p1.index+1 && bb.rem(p2) == 0 && p1.pos == 0 { return true } return false } func (bb *File) gt(p1, p2 bbpos) bool { if bb.eq(p1, p2) { return false } if p1.index != p2.index { return p1.index > p2.index } return p1.pos > p2.pos } func (bb *File) zero(p1, p2 bbpos) error { var zeros [4096]byte for bb.gt(p2, p1) { w := bb.rem(p1) if w == 0 { p1.index++ p1.pos = 0 w = bb.rem(p1) } if p1.index == p2.index { w = p2.pos } if w > int64(len(zeros)) { w = int64(len(zeros)) } if _, err := bb.blobs[p1.index].Seek(p1.pos, io.SeekStart); err != nil { return err } nn, err := bb.blobs[p1.index].Write(zeros[:w]) if err != nil { return err } p1.pos += int64(nn) } return nil } // Write writes p to the file. // See [io.Writer] for details. func (bb *File) Write(p []byte) (n int, err error) { if bb.err != nil { return 0, bb.err } if bb.gt(bb.off, bb.len) { if err := bb.zero(bb.len, bb.off); err != nil { bb.err = err return 0, err } } for len(p) > 0 { w := bb.rem(bb.off) if w == 0 { if bb.off.index == len(bb.blobs)-1 { if bb.err = bb.grow(); bb.err != nil { return n, bb.err } } bb.off.index++ bb.off.pos = 0 w = bb.rem(bb.off) } if int64(len(p)) < w { w = int64(len(p)) } if _, err := bb.blobs[bb.off.index].Seek(bb.off.pos, io.SeekStart); err != nil { bb.err = err break } nn, err := bb.blobs[bb.off.index].Write(p[:w]) n += nn p = p[nn:] bb.off.pos += int64(nn) if bb.gt(bb.off, bb.len) { bb.len = bb.off } if err != nil { bb.err = err break } } return n, bb.err } // Read reads data into p. // See [io.Reader] for details. func (bb *File) Read(p []byte) (n int, err error) { if bb.err != nil { return 0, bb.err } for len(p) > 0 && bb.gt(bb.len, bb.off) { if bb.rem(bb.off) == 0 { bb.off.index++ bb.off.pos = 0 } var bsize int64 if bb.len.index == bb.off.index { bsize = bb.len.pos } else { bsize = bb.blobs[bb.off.index].Size() } w := bsize - bb.off.pos if int64(len(p)) < w { w = int64(len(p)) } if _, err := bb.blobs[bb.off.index].Seek(bb.off.pos, io.SeekStart); err != nil { bb.err = err return n, err } nn, err := io.ReadFull(bb.blobs[bb.off.index], p[:w]) n += nn p = p[nn:] bb.off.pos += int64(nn) if err != nil { bb.err = err return n, err } } if n == 0 && (bb.eq(bb.off, bb.len) || bb.gt(bb.off, bb.len)) { return 0, io.EOF } return n, nil } // Seek changes the read/write position in the file. // See [io.Seeker] for details. func (bb *File) Seek(offset int64, whence int) (int64, error) { if bb.err != nil { return 0, bb.err } const ( SeekStart = 0 SeekCurrent = 1 SeekEnd = 2 ) switch whence { case SeekStart: // use offset directly case SeekCurrent: for i := 0; i < bb.off.index; i++ { offset += bb.blobs[i].Size() } offset += bb.off.pos case SeekEnd: offset += bb.Len() } if offset < 0 { return -1, fmt.Errorf("sqlitex.File: attempting to seek before beginning of blob (%d)", offset) } rem := offset bb.off.index = 0 for i := 0; rem > bb.blobs[i].Size(); i++ { bb.off.index = i + 1 rem -= bb.blobs[i].Size() if i == len(bb.blobs)-1 { if err := bb.grow(); err != nil { return offset - rem, err } } } bb.off.pos = rem return offset, nil } // Truncate changes the size of the file. func (bb *File) Truncate(size int64) error { for { for i := 0; i < len(bb.blobs); i++ { bsize := bb.blobs[i].Size() if bsize > size { newlen := bbpos{index: i, pos: size} if err := bb.zero(bb.len, newlen); err != nil { return err } bb.len = newlen return nil } size -= bsize } if err := bb.grow(); err != nil { return err } } } // Len returns the size of the file in bytes. func (bb *File) Len() (n int64) { for i := 0; i < bb.len.index; i++ { n += bb.blobs[i].Size() } n += bb.len.pos return n } // Cap returns the allocated capacity of the file in bytes. func (bb *File) Cap() (n int64) { for i := 0; i < len(bb.blobs); i++ { n += bb.blobs[i].Size() } return n } // Close releases all resources associated with the file, // removing the storage from the database. func (bb *File) Close() error { if bb.err != nil { return bb.err } for _, blob := range bb.blobs { err := blob.Close() if bb.err == nil { bb.err = err } } stmt := bb.conn.Prep("DELETE FROM BlobBuffer WHERE rowid = $rowid;") for _, rowid := range bb.rowids { stmt.Reset() stmt.SetInt64("$rowid", rowid) if _, err := stmt.Step(); err != nil && bb.err == nil { bb.err = err } } bb.blobs = nil bb.rowids = nil return bb.err } type bbpos struct { index int // bb.blobs[index] pos int64 // point inside that blob } zombiezen-go-sqlite-297af96/sqlitefile/file_test.go000066400000000000000000000025621501417116500224430ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitefile import ( "io" "os" "testing" "crawshaw.io/iox/ioxtest" "zombiezen.com/go/sqlite" ) var _ interface { io.Reader io.Writer io.Seeker io.Closer } = (*File)(nil) func TestFileRand(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() f1, err := NewFile(conn) if err != nil { t.Fatal(err) } f2, err := os.CreateTemp("", "sqlitex") if err != nil { t.Fatal(err) } ft := &ioxtest.Tester{F1: f1, F2: f2, T: t} ft.Run() } zombiezen-go-sqlite-297af96/sqlitemigration/000077500000000000000000000000001501417116500212035ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/sqlitemigration/example_test.go000066400000000000000000000052041501417116500242250ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlitemigration_test import ( "context" "errors" "fmt" "log" "os" "path/filepath" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitemigration" "zombiezen.com/go/sqlite/sqlitex" ) func Example() { schema := sqlitemigration.Schema{ // Each element of the Migrations slice is applied in sequence. When you // want to change the schema, add a new SQL script to this list. // // Existing databases will pick up at the same position in the Migrations // slice as they last left off. Migrations: []string{ "CREATE TABLE foo ( id INTEGER NOT NULL PRIMARY KEY );", "ALTER TABLE foo ADD COLUMN name TEXT;", }, // The RepeatableMigration is run after all other Migrations if any // migration was run. It is useful for creating triggers and views. RepeatableMigration: "DROP VIEW IF EXISTS bar;\n" + "CREATE VIEW bar ( id, name ) AS SELECT id, name FROM foo;\n", } // Set up a temporary directory to store the database. dir, err := os.MkdirTemp("", "sqlitemigration") if err != nil { // handle error log.Fatal(err) } defer os.RemoveAll(dir) // Open a pool. This does not block, and will start running any migrations // asynchronously. pool := sqlitemigration.NewPool(filepath.Join(dir, "foo.db"), schema, sqlitemigration.Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { // Enable foreign keys. See https://sqlite.org/foreignkeys.html return sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = ON;", nil) }, OnError: func(e error) { log.Println(e) }, }) defer pool.Close() // Get a connection. This blocks until the migration completes. conn, err := pool.Get(context.TODO()) if err != nil { // handle error } defer pool.Put(conn) // Print the list of schema objects created. const listSchemaQuery = `SELECT "type", "name" FROM sqlite_master ORDER BY 1, 2;` err = sqlitex.ExecuteTransient(conn, listSchemaQuery, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Printf("%-5s %s\n", stmt.ColumnText(0), stmt.ColumnText(1)) return nil }, }) if err != nil { // handle error } // Output: // table foo // view bar } // This example constructs a schema from a set of SQL files in a directory named // schema01.sql, schema02.sql, etc. func ExampleSchema() { var schema sqlitemigration.Schema for i := 1; ; i++ { migration, err := os.ReadFile(fmt.Sprintf("schema%02d.sql", i)) if errors.Is(err, os.ErrNotExist) { break } if err != nil { // handle error... } schema.Migrations = append(schema.Migrations, string(migration)) } } zombiezen-go-sqlite-297af96/sqlitemigration/sqlitemigration.go000066400000000000000000000340721501417116500247530ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC // Package sqlitemigration provides a connection pool type that guarantees a // series of SQL scripts has been run once successfully before making // connections available to the application. This is frequently useful for // ensuring tables are created. package sqlitemigration import ( "context" "errors" "fmt" "sync" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // Schema defines the migrations for the application. type Schema struct { // Migrations is a list of SQL scripts to run. // Each script is wrapped in a transaction which is rolled back on any error. Migrations []string // MigrationOptions specifies options for each migration. // len(MigrationOptions) must not be greater than len(Migrations). MigrationOptions []*MigrationOptions // AppID is saved to the database file to identify the application. // It's an optional measure to prevent opening database files for a different application. // It must not change between runs of the same program. // // A common way of setting this is with a compile-time constant that was randomly generated. // `head -c 4 /dev/urandom | xxd -p` can generate such an ID. AppID int32 // RepeatableMigration is a SQL script to run if any migrations ran. // The script is run as part of the final migration's transaction. RepeatableMigration string } // MigrationOptions holds optional parameters for a migration. type MigrationOptions struct { // If DisableForeignKeys is true, then before starting the migration's // transaction, "PRAGMA foreign_keys = off;" will be executed. After the // migration's transaction completes, then the "PRAGMA foreign_keys" setting // will be restored to the value it was before executing // "PRAGMA foreign_keys = off;". DisableForeignKeys bool } // Options specifies optional behaviors for the pool. type Options struct { // Flags is interpreted the same way as the argument to [sqlitex.PoolOptions]. Flags sqlite.OpenFlags // PoolSize sets an explicit size to the pool. If less than 1, a reasonable // default is used. PoolSize int // OnStartMigrate is called after the pool has successfully opened a // connection to the database but before any migrations have been run. OnStartMigrate SignalFunc // OnReady is called after the pool has connected to the database and run any // necessary migrations. OnReady SignalFunc // OnError is called when the pool encounters errors while applying the // migration. This is typically used for logging errors. OnError ReportFunc // PrepareConn is called for each connection in the pool to set up functions // and other connection-specific state. PrepareConn ConnPrepareFunc } // Pool is a pool of SQLite connections. type Pool struct { retry chan struct{} opts Options cancel context.CancelFunc ready <-chan struct{} // protects the following fields pool *sqlitex.Pool err error closedMu sync.RWMutex closed bool } // NewPool opens a new pool of SQLite connections. func NewPool(uri string, schema Schema, opts Options) *Pool { ready := make(chan struct{}) retry := make(chan struct{}, 1) ctx, cancel := context.WithCancel(context.Background()) p := &Pool{ retry: retry, opts: opts, cancel: cancel, ready: ready, } go func() { defer close(ready) defer cancel() p.pool, p.err = p.open(ctx, uri, schema) if p.err != nil { opts.OnError.call(p.err) } }() return p } // Close closes all connections in the Pool, potentially interrupting // a migration. func (p *Pool) Close() error { p.closedMu.Lock() if p.closed { p.closedMu.Unlock() return errors.New("close sqlite pool: already closed") } p.closed = true p.closedMu.Unlock() p.cancel() <-p.ready if p.pool == nil { return nil } return p.pool.Close() } // Get obtains an SQLite connection from the pool, // waiting until the initial migration is complete. // Get is identical to [Pool.Take]. // // If no connection is available, // Get will block until at least one connection is returned with [Pool.Put], // or until either the Pool is closed or the context is canceled. // If no connection can be obtained // or an error occurs while preparing the connection, // an error is returned. // // The provided context is also used to control the execution lifetime of the connection. // See [sqlite.Conn.SetInterrupt] for details. // // Applications must ensure that all non-nil Conns returned from Get // are returned to the same Pool with [Pool.Put]. func (p *Pool) Get(ctx context.Context) (*sqlite.Conn, error) { return p.Take(ctx) } // Take obtains an SQLite connection from the pool, // waiting until the initial migration is complete. // // If no connection is available, // Take will block until at least one connection is returned with [Pool.Put], // or until either the Pool is closed or the context is canceled. // If no connection can be obtained // or an error occurs while preparing the connection, // an error is returned. // // The provided context is also used to control the execution lifetime of the connection. // See [sqlite.Conn.SetInterrupt] for details. // // Applications must ensure that all non-nil Conns returned from Take // are returned to the same Pool with [Pool.Put]. func (p *Pool) Take(ctx context.Context) (*sqlite.Conn, error) { tick := time.NewTicker(5 * time.Second) for ready := false; !ready; { // Inform Pool.open to keep trying. select { case p.retry <- struct{}{}: default: } select { case <-tick.C: // Another try. case <-p.ready: ready = true case <-ctx.Done(): tick.Stop() return nil, fmt.Errorf("get sqlite connection: %w", ctx.Err()) } } tick.Stop() if p.err != nil { return nil, fmt.Errorf("get sqlite connection: %w", p.err) } conn, err := p.pool.Take(ctx) if err != nil { return nil, fmt.Errorf("get sqlite connection: %w", err) } return conn, nil } // Put puts an SQLite connection back into the pool. // See sqlitex.Pool for details. func (p *Pool) Put(conn *sqlite.Conn) { select { case <-p.ready: default: panic("Pool.Put before pool is ready") } if p.err != nil { panic("Pool.Put on failed pool") } p.pool.Put(conn) } // CheckHealth returns an error if the migration has not completed. // Closed pools may report healthy. func (p *Pool) CheckHealth() error { p.closedMu.RLock() closed := p.closed p.closedMu.RUnlock() if closed { return errors.New("sqlite pool health: closed") } select { case <-p.ready: if p.err != nil { return fmt.Errorf("sqlite pool health: %w", p.err) } return nil default: return errors.New("sqlite pool health: not ready") } } func (p *Pool) open(ctx context.Context, uri string, schema Schema) (*sqlitex.Pool, error) { for first := true; ; first = false { if !first { select { case <-p.retry: case <-ctx.Done(): return nil, errors.New("closed before successful migration") } } pool, err := sqlitex.NewPool(uri, sqlitex.PoolOptions{ Flags: p.opts.Flags, PoolSize: p.opts.PoolSize, PrepareConn: p.opts.PrepareConn, }) if err != nil { p.opts.OnError.call(err) continue } conn, err := pool.Take(ctx) if isDone(err) { pool.Close() return nil, errors.New("closed before successful migration") } if err != nil { if closeErr := pool.Close(); closeErr != nil { p.opts.OnError.call(fmt.Errorf("close after failed connection preparation: %w", closeErr)) } return nil, err } err = migrateDB(ctx, conn, schema, p.opts.OnStartMigrate) pool.Put(conn) if err != nil { if closeErr := pool.Close(); closeErr != nil { p.opts.OnError.call(fmt.Errorf("close after failed migration: %w", closeErr)) } return nil, err } p.opts.OnReady.call() return pool, nil } } // Migrate performs any unapplied migrations in the schema on the database. func Migrate(ctx context.Context, conn *sqlite.Conn, schema Schema) error { return migrateDB(ctx, conn, schema, nil) } func migrateDB(ctx context.Context, conn *sqlite.Conn, schema Schema, onStart SignalFunc) error { defer conn.SetInterrupt(conn.SetInterrupt(ctx.Done())) schemaVersion, err := ensureAppID(conn, schema.AppID) if err != nil { return fmt.Errorf("migrate database: %w", err) } onStart.call() var foreignKeysEnabled bool err = sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys;", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { foreignKeysEnabled = stmt.ColumnBool(0) return nil }, }) if err != nil { return fmt.Errorf("migrate database: %w", err) } beginStmt, _, err := conn.PrepareTransient("BEGIN IMMEDIATE;") if err != nil { return fmt.Errorf("migrate database: %w", err) } defer beginStmt.Finalize() commitStmt, _, err := conn.PrepareTransient("COMMIT;") if err != nil { return fmt.Errorf("migrate database: %w", err) } defer commitStmt.Finalize() for ; int(schemaVersion) < len(schema.Migrations); schemaVersion++ { migration := schema.Migrations[schemaVersion] disableFKs := foreignKeysEnabled && int(schemaVersion) < len(schema.MigrationOptions) && schema.MigrationOptions[schemaVersion] != nil && schema.MigrationOptions[schemaVersion].DisableForeignKeys if disableFKs { // Do not try to optimize by preparing this PRAGMA statement ahead of time. if err := sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = off;", nil); err != nil { return fmt.Errorf("migrate database: disable foreign keys: %w", err) } } if err := stepAndReset(beginStmt); err != nil { return fmt.Errorf("migrate database: apply migrations[%d]: %w", schemaVersion, err) } actualSchemaVersion, err := userVersion(conn) if err != nil { rollback(conn) return fmt.Errorf("migrate database: %w", err) } if actualSchemaVersion != schemaVersion { // A different process migrated while we were not inside a transaction. rollback(conn) schemaVersion = actualSchemaVersion - 1 continue } err = sqlitex.ExecScript(conn, fmt.Sprintf("%s;\nPRAGMA user_version = %d;\n", migration, schemaVersion+1)) if err != nil { rollback(conn) return fmt.Errorf("migrate database: apply migrations[%d]: %w", schemaVersion, err) } if int(schemaVersion) == len(schema.Migrations)-1 && schema.RepeatableMigration != "" { if err := sqlitex.ExecScript(conn, schema.RepeatableMigration); err != nil { rollback(conn) return fmt.Errorf("migrate database: apply repeatable migration: %w", err) } } if err := stepAndReset(commitStmt); err != nil { rollback(conn) return fmt.Errorf("migrate database: apply migrations[%d]: %w", schemaVersion, err) } if disableFKs { if err := sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = on;", nil); err != nil { return fmt.Errorf("migrate database: reenable foreign keys: %w", err) } } } return nil } func userVersion(conn *sqlite.Conn) (int32, error) { var version int32 err := sqlitex.ExecuteTransient(conn, "PRAGMA user_version;", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { version = stmt.ColumnInt32(0) return nil }, }) if err != nil { return 0, fmt.Errorf("get database user_version: %w", err) } return version, nil } func rollback(conn *sqlite.Conn) { if conn.AutocommitEnabled() { return } sqlitex.ExecuteTransient(conn, "ROLLBACK;", nil) } func ensureAppID(conn *sqlite.Conn, wantAppID int32) (schemaVersion int32, err error) { // This transaction will later be upgraded to a write transaction. If at the point of upgrading // to a write transaction, the database is locked, SQLite will fail immediately with // SQLITE_BUSY and the busy timeout will have no effect, causing the pool to fail. // // If we use an immediate transaction, telling SQLite this is a write transaction, SQLite // will attempt to lock the database immediately. If a lock cannot be acquired, the busy // timeout is used allowing the transaction to wait until it can get a lock, thus allowing the // pool to start successfully. end, err := sqlitex.ImmediateTransaction(conn) if err != nil { return 0, err } defer end(&err) var hasSchema bool err = sqlitex.ExecuteTransient(conn, "VALUES ((SELECT COUNT(*) FROM sqlite_master) > 0);", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { hasSchema = stmt.ColumnInt(0) != 0 return nil }, }) if err != nil { return 0, err } var dbAppID int32 err = sqlitex.ExecuteTransient(conn, "PRAGMA application_id;", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { dbAppID = stmt.ColumnInt32(0) return nil }, }) if err != nil { return 0, err } if dbAppID != wantAppID && !(dbAppID == 0 && !hasSchema) { return 0, fmt.Errorf("database application_id = %#x (expected %#x)", dbAppID, wantAppID) } schemaVersion, err = userVersion(conn) if err != nil { return 0, err } // Using Sprintf because PRAGMAs don't permit arbitrary expressions, and thus // don't permit using parameter substitution. err = sqlitex.ExecuteTransient(conn, fmt.Sprintf("PRAGMA application_id = %d;", wantAppID), nil) if err != nil { return 0, err } return schemaVersion, nil } // A SignalFunc is called at most once when a particular event in a Pool's // lifecycle occurs. type SignalFunc func() func (f SignalFunc) call() { if f == nil { return } f() } // A ReportFunc is called for transient errors the pool encounters while // running the migrations. It must be safe to call from multiple goroutines. type ReportFunc func(error) func (f ReportFunc) call(err error) { if f == nil { return } f(err) } // A ConnPrepareFunc is called for each connection in a pool // to set up connection-specific state. // It must be safe to call from multiple goroutines. // // If the ConnPrepareFunc returns an error, // then it will be called the next time the connection is about to be used. // Once ConnPrepareFunc returns nil for a given connection, // it will not be called on that connection again. type ConnPrepareFunc = sqlitex.ConnPrepareFunc func stepAndReset(stmt *sqlite.Stmt) error { if _, err := stmt.Step(); err != nil { return err } return stmt.Reset() } // isDone reports whether an error indicates cancellation or deadline exceeded // from a [context.Context] func isDone(err error) bool { return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) } zombiezen-go-sqlite-297af96/sqlitemigration/sqlitemigration_test.go000066400000000000000000000630451501417116500260140ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlitemigration import ( "context" "fmt" "path/filepath" "sync" "testing" "github.com/google/go-cmp/cmp" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func TestPool(t *testing.T) { ctx := context.Background() t.Run("NoMigrations", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, } state := new(eventRecorder) pool := NewPool(filepath.Join(t.TempDir(), "no-migrations.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, OnStartMigrate: state.startMigrateFunc(), OnReady: state.readyFunc(), }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } if err := pool.CheckHealth(); err == nil { t.Error("after Close, CheckHealth() = ; want error") } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } if state.migrationStarted != 1 { t.Errorf("OnStartMigrate called %d times; want 1", state.migrationStarted) } if state.ready != 1 { t.Errorf("OnReady called %d times; want 1", state.ready) } if err := pool.CheckHealth(); err != nil { t.Errorf("after successful Get, CheckHealth = %v; want ", err) } called := false err = sqlitex.ExecTransient(conn, "PRAGMA application_id;", func(stmt *sqlite.Stmt) error { called = true if got, want := stmt.ColumnInt32(0), int32(0xedbeef); got != want { t.Errorf("application_id = %#x; want %#x", got, want) } return nil }) if err != nil { t.Errorf("PRAGMA application_id: %v", err) } else if !called { t.Error("PRAGMA application_id not called") } pool.Put(conn) }) t.Run("DoesNotMigrateDifferentDatabase", func(t *testing.T) { // Create another.db with a single table. // Don't set application ID. dir := t.TempDir() err := withTestConn(dir, "another.db", func(conn *sqlite.Conn) error { err := sqlitex.ExecTransient(conn, `create table foo ( id integer primary key not null );`, nil) if err != nil { return fmt.Errorf("create table: %v", err) } return nil }) if err != nil { t.Fatal(err) } // Try to open the pool. schema := Schema{ AppID: 0xedbeef, } state := new(eventRecorder) pool := NewPool(filepath.Join(dir, "another.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, OnStartMigrate: state.startMigrateFunc(), OnReady: state.readyFunc(), }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } if err := pool.CheckHealth(); err == nil { t.Error("after Close, CheckHealth() = ; want error") } }() conn, err := pool.Get(ctx) t.Logf("pool.Get error: %v", err) if err == nil { pool.Put(conn) return } if state.migrationStarted != 0 { t.Errorf("OnStartMigrate called %d times; want 0", state.migrationStarted) } if state.ready != 0 { t.Errorf("OnReady called %d times; want 0", state.ready) } if err := pool.CheckHealth(); err == nil { t.Errorf("CheckHealth = ; want error") } // Verify that application ID is not set. err = withTestConn(dir, "another.db", func(conn *sqlite.Conn) error { called := false err = sqlitex.ExecTransient(conn, "PRAGMA application_id;", func(stmt *sqlite.Stmt) error { called = true if got, want := stmt.ColumnInt32(0), int32(0); got != want { t.Errorf("application_id = %#x; want %#x", got, want) } return nil }) if err != nil { t.Errorf("PRAGMA application_id: %v", err) } else if !called { t.Error("PRAGMA application_id not called") } return nil }) if err != nil { t.Fatal(err) } }) t.Run("ZeroAppID", func(t *testing.T) { const dbName = "zeroid.db" schema1 := Schema{ AppID: 0, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, dbName), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, dbName), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Error(err) } else if got != 42 { t.Errorf("select id = %d; want 42", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("OneMigration", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } state := new(eventRecorder) pool := NewPool(filepath.Join(t.TempDir(), "one-migration.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, OnStartMigrate: state.startMigrateFunc(), OnReady: state.readyFunc(), }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } if err := pool.CheckHealth(); err == nil { t.Error("after Close, CheckHealth() = ; want error") } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) if state.migrationStarted != 1 { t.Errorf("OnStartMigrate called %d times; want 1", state.migrationStarted) } if state.ready != 1 { t.Errorf("OnReady called %d times; want 1", state.ready) } if err := pool.CheckHealth(); err != nil { t.Errorf("after successful Get, CheckHealth = %v; want ", err) } err = sqlitex.ExecTransient(conn, "insert into foo values (42);", nil) if err != nil { t.Error(err) } }) t.Run("TwoMigrations", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, } state := new(eventRecorder) pool := NewPool(filepath.Join(t.TempDir(), "two-migrations.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, OnStartMigrate: state.startMigrateFunc(), OnReady: state.readyFunc(), }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) if state.migrationStarted != 1 { t.Errorf("OnStartMigrate called %d times; want 1", state.migrationStarted) } if state.ready != 1 { t.Errorf("OnReady called %d times; want 1", state.ready) } if err := pool.CheckHealth(); err != nil { t.Errorf("after successful Get, CheckHealth = %v; want ", err) } var got int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Error(err) } else if got != 42 { t.Errorf("select id = %d; want 42", got) } }) t.Run("PartialMigration", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null ); insert into foo values (1);`, `insert into foo values (42); insert into bar values (57);`, }, } state := new(eventRecorder) dir := t.TempDir() pool := NewPool(filepath.Join(dir, "partial-migration.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, OnStartMigrate: state.startMigrateFunc(), OnReady: state.readyFunc(), }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } if err := pool.CheckHealth(); err == nil { t.Error("after Close, CheckHealth() = ; want error") } }() conn, err := pool.Get(ctx) t.Logf("pool.Get error: %v", err) if err == nil { pool.Put(conn) return } if state.migrationStarted != 1 { t.Errorf("OnStartMigrate called %d times; want 1", state.migrationStarted) } if state.ready != 0 { t.Errorf("OnReady called %d times; want 0", state.ready) } if err := pool.CheckHealth(); err == nil { t.Error("CheckHealth() = ; want error") } // Verify that the first migration is applied and that none of the second // migration is applied. err = withTestConn(dir, "partial-migration.db", func(conn *sqlite.Conn) error { var got int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { return err } if got != 1 { t.Errorf("select id = %d; want 1", got) } return nil }) if err != nil { t.Fatal(err) } }) t.Run("MigrationsDontRepeat", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "migrations-dont-repeat.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } err = sqlitex.ExecTransient(conn, "insert into foo values (42);", nil) if err != nil { t.Error(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "migrations-dont-repeat.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } err = sqlitex.ExecTransient(conn, "insert into foo values (56);", nil) if err != nil { t.Error(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("IncrementalMigration", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "incremental-migration.db"), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "incremental-migration.db"), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Error(err) } else if got != 42 { t.Errorf("select id = %d; want 42", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("Repeatable/IncrementalMigration", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, RepeatableMigration: `insert into foo values (333);`, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "repeatable-incremental.db"), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "repeatable-incremental.db"), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got []int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnInt(0)) return nil }) if err != nil { t.Error(err) } else if !cmp.Equal(got, []int{42, 333}) { t.Errorf("select id = %v; want [42 333]", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("Repeatable/IncrementalMigrationEmpty", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, "", }, RepeatableMigration: `insert into foo values (333);`, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "repeatable-incremental.db"), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "repeatable-incremental.db"), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got []int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnInt(0)) return nil }) if err != nil { t.Error(err) } else if !cmp.Equal(got, []int{333}) { t.Errorf("select id = %v; want [333]", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("Repeatable/IncrementalMigrationFailure", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, RepeatableMigration: `insert into bar values (333);`, } // Run 1 dbPath := filepath.Join(t.TempDir(), "repeatable-fail.db") pool := NewPool(dbPath, schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(dbPath, schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err == nil { pool.Put(conn) t.Error("Second migration did not fail") } if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } err = withTestConn(filepath.Dir(dbPath), filepath.Base(dbPath), func(conn *sqlite.Conn) error { var got []int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnInt(0)) return nil }) if err != nil { return err } if len(got) != 0 { return fmt.Errorf("select id = %v; want []", got) } return nil }) if err != nil { t.Fatal(err) } }) t.Run("Repeatable/SameVersion", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, RepeatableMigration: `insert into foo values (333);`, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "repeatable-sameversion.db"), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "repeatable-sameversion.db"), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got []int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = append(got, stmt.ColumnInt(0)) return nil }) if err != nil { t.Error(err) } else if len(got) > 0 { t.Errorf("select id = %v; want []", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("FutureVersion", func(t *testing.T) { schema1 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, `insert into foo values (42);`, }, } schema2 := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } // Run 1 dir := t.TempDir() pool := NewPool(filepath.Join(dir, "future-version.db"), schema1, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err := pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } // Run 2 pool = NewPool(filepath.Join(dir, "future-version.db"), schema2, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, }) conn, err = pool.Get(ctx) if err != nil { pool.Close() t.Fatal(err) } var got int err = sqlitex.ExecTransient(conn, "select id from foo order by id;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Error(err) } else if got != 42 { t.Errorf("select id = %d; want 42", got) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }) t.Run("CustomFunctionInMigration", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null ); insert into foo (id) values (theAnswer());`, }, } pool := NewPool(filepath.Join(t.TempDir(), "custom-schema-function.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { return conn.CreateFunction("theAnswer", &sqlite.FunctionImpl{ NArgs: 0, Deterministic: true, Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { return sqlite.IntegerValue(42), nil }, }) }, }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) var got int err = sqlitex.ExecTransient(conn, "select id from foo limit 1;", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Fatal(err) } if got != 42 { t.Errorf("got %d; want 42", got) } }) t.Run("CustomFunctionInGet", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, } pool := NewPool(filepath.Join(t.TempDir(), "custom-get-function.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { return conn.CreateFunction("theAnswer", &sqlite.FunctionImpl{ NArgs: 0, Deterministic: true, Scalar: func(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) { return sqlite.IntegerValue(42), nil }, }) }, }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) var got int err = sqlitex.ExecTransient(conn, "select theAnswer();", func(stmt *sqlite.Stmt) error { got = stmt.ColumnInt(0) return nil }) if err != nil { t.Fatal(err) } if got != 42 { t.Errorf("got %d; want 42", got) } }) t.Run("DisableForeignKeys", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( foreign_keys_enabled bool ); insert into foo values ((select * from pragma_foreign_keys()));`, }, MigrationOptions: []*MigrationOptions{ {DisableForeignKeys: true}, }, } pool := NewPool(filepath.Join(t.TempDir(), "disable-foreign-keys.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { return sqlitex.ExecTransient(conn, "PRAGMA foreign_keys = on;", nil) }, }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) duringMigration, err := sqlitex.ResultBool(conn.Prep("select foreign_keys_enabled from foo;")) if err != nil { t.Error(err) } else if duringMigration { t.Error("Foreign keys were enabled during migration") } afterMigration, err := sqlitex.ResultBool(conn.Prep("PRAGMA foreign_keys;")) if err != nil { t.Error(err) } else if !afterMigration { t.Error("Foreign keys were left disabled after migration") } }) t.Run("NoTouchForeignKeys", func(t *testing.T) { schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( foreign_keys_enabled bool ); insert into foo values ((select * from pragma_foreign_keys()));`, }, } pool := NewPool(filepath.Join(t.TempDir(), "no-touch-foreign-keys.db"), schema, Options{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate, PrepareConn: func(conn *sqlite.Conn) error { return sqlitex.ExecuteTransient(conn, "PRAGMA foreign_keys = on;", nil) }, }) defer func() { if err := pool.Close(); err != nil { t.Error("pool.Close:", err) } }() conn, err := pool.Get(ctx) if err != nil { t.Fatal(err) } defer pool.Put(conn) duringMigration, err := sqlitex.ResultBool(conn.Prep("select foreign_keys_enabled from foo;")) if err != nil { t.Error(err) } else if !duringMigration { t.Error("Foreign keys were disabled during migration") } afterMigration, err := sqlitex.ResultBool(conn.Prep("PRAGMA foreign_keys;")) if err != nil { t.Error(err) } else if !afterMigration { t.Error("Foreign keys were disabled after migration") } }) } func TestMigrate(t *testing.T) { t.Run("NoTouchForeignKeys", func(t *testing.T) { conn, err := sqlite.OpenConn(filepath.Join(t.TempDir(), "no-touch-foreign-keys.db"), sqlite.OpenReadWrite, sqlite.OpenCreate) if err != nil { t.Fatal(err) } defer conn.Close() err = sqlitex.ExecuteTransient(conn, `PRAGMA foreign_keys = on;`, nil) if err != nil { t.Fatal(err) } schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( foreign_keys_enabled bool ); insert into foo values ((select * from pragma_foreign_keys()));`, }, } if err := Migrate(context.Background(), conn, schema); err != nil { t.Error(err) } duringMigration, err := sqlitex.ResultBool(conn.Prep("select foreign_keys_enabled from foo;")) if err != nil { t.Error(err) } else if !duringMigration { t.Error("Foreign keys were disabled during migration") } afterMigration, err := sqlitex.ResultBool(conn.Prep("PRAGMA foreign_keys;")) if err != nil { t.Error(err) } else if !afterMigration { t.Error("Foreign keys were disabled after migration") } }) t.Run("Concurrent", func(t *testing.T) { ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "concurrent.db") schema := Schema{ AppID: 0xedbeef, Migrations: []string{ `create table foo ( id integer primary key not null );`, }, } // Attempt to perform migrations while writing. var wg sync.WaitGroup defer wg.Wait() const numConcurrent = 5 wg.Add(numConcurrent) for i := 0; i < numConcurrent; i++ { go func(i int) { defer wg.Done() conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate) if err != nil { t.Error(err) return } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() if err := Migrate(ctx, conn, schema); err != nil { t.Error("Migrate:", err) } }(i) } // Migrate and issue writes on one connection. conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite, sqlite.OpenCreate) if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() if err := Migrate(ctx, conn, schema); err != nil { t.Fatal("Migrate:", err) } for i := 0; i < 150; i++ { if err := sqlitex.Execute(conn, "insert into foo values (?)", &sqlitex.ExecOptions{ Args: []any{i}, }); err != nil { t.Error("insert query:", err) } } }) } // withTestConn makes an independent connection to the given database. func withTestConn(dir, name string, f func(*sqlite.Conn) error) error { conn, err := sqlite.OpenConn(filepath.Join(dir, name), sqlite.OpenReadWrite|sqlite.OpenCreate) if err != nil { return err } defer conn.Close() if err := f(conn); err != nil { return err } return nil } type eventRecorder struct { migrationStarted int ready int } func (rec *eventRecorder) startMigrateFunc() SignalFunc { return func() { rec.migrationStarted++ } } func (rec *eventRecorder) readyFunc() SignalFunc { return func() { rec.ready++ } } zombiezen-go-sqlite-297af96/sqlitex/000077500000000000000000000000001501417116500174615ustar00rootroot00000000000000zombiezen-go-sqlite-297af96/sqlitex/doc.go000066400000000000000000000012121501417116500205510ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC /* Package sqlitex provides utilities for working with SQLite. # Statements To execute a statement from a string, pass an [ExecOptions] struct to one of the following functions: - [Execute] - [ExecuteScript] - [ExecuteTransient] To execute a statement from a file (typically using [embed]), pass an [ExecOptions] struct to one of the following functions: - [ExecuteFS] - [ExecuteScriptFS] - [ExecuteTransientFS] - [PrepareTransientFS] # Transactions and Savepoints - [Save] - [Transaction] - [ExclusiveTransaction] - [ImmediateTransaction] */ package sqlitex zombiezen-go-sqlite-297af96/sqlitex/example_test.go000066400000000000000000000027031501417116500225040ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlitex_test import ( "context" "fmt" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func ExampleExecute() { conn, err := sqlite.OpenConn(":memory:") if err != nil { // handle err } if err := sqlitex.Execute(conn, "CREATE TABLE t (a, b, c, d);", nil); err != nil { // handle err } err = sqlitex.Execute(conn, "INSERT INTO t (a, b, c, d) VALUES (?, ?, ?, ?);", &sqlitex.ExecOptions{ Args: []any{"a1", 1, 42, 1}, }) if err != nil { // handle err } var a []string var b []int64 err = sqlitex.Execute(conn, "SELECT a, b FROM t WHERE c = ? AND d = ?;", &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { a = append(a, stmt.ColumnText(0)) b = append(b, stmt.ColumnInt64(1)) return nil }, Args: []any{42, 1}, }) if err != nil { // handle err } fmt.Println(a, b) // Output: // [a1] [1] } func ExampleSave() { doWork := func(conn *sqlite.Conn) (err error) { defer sqlitex.Save(conn)(&err) // ... do work in the transaction return nil } _ = doWork } func ExamplePool() { // Open a pool. dbpool, err := sqlitex.NewPool("foo.db", sqlitex.PoolOptions{}) if err != nil { // handle err } defer func() { if err := dbpool.Close(); err != nil { // handle err } }() // While handling a request: ctx := context.TODO() conn, err := dbpool.Take(ctx) if err != nil { // handle err } defer dbpool.Put(conn) } zombiezen-go-sqlite-297af96/sqlitex/exec.go000066400000000000000000000341431501417116500207410ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "fmt" "io" "io/fs" "reflect" "strings" "zombiezen.com/go/sqlite" ) // ExecOptions is the set of optional arguments executing a statement. type ExecOptions struct { // Args is the set of positional arguments to bind to the statement. // The first element in the slice is ?1. // See https://sqlite.org/lang_expr.html for more details. // // Basic reflection on Args is used to map: // // integers to BindInt64 // floats to BindFloat // []byte to BindBytes // string to BindText // bool to BindBool // // All other kinds are printed using fmt.Sprint(v) and passed to BindText. Args []any // Named is the set of named arguments to bind to the statement. Keys must // start with ':', '@', or '$'. See https://sqlite.org/lang_expr.html for more // details. // // Basic reflection on Named is used to map: // // integers to BindInt64 // floats to BindFloat // []byte to BindBytes // string to BindText // bool to BindBool // // All other kinds are printed using fmt.Sprint(v) and passed to BindText. Named map[string]any // ResultFunc is called for each result row. // If ResultFunc returns an error then iteration ceases // and the execution function returns the error value. ResultFunc func(stmt *sqlite.Stmt) error } // Exec executes an SQLite query. // // For each result row, the resultFn is called. // Result values can be read by resultFn using stmt.Column* methods. // If resultFn returns an error then iteration ceases and Exec returns // the error value. // // Any args provided to Exec are bound to numbered parameters of the // query using the [sqlite.Stmt] Bind* methods. Basic reflection on args is used // to map: // // integers to BindInt64 // floats to BindFloat // []byte to BindBytes // string to BindText // bool to BindBool // // All other kinds are printed using fmt.Sprintf("%v", v) and passed // to BindText. // // Exec is implemented using the Stmt prepare mechanism which allows // better interactions with Go's type system and avoids pitfalls of // passing a Go closure to cgo. // // As Exec is implemented using Conn.Prepare, subsequent calls to Exec // with the same statement will reuse the cached statement object. // // Deprecated: Use [Execute]. // Exec skips some argument checks for compatibility with crawshaw.io/sqlite. func Exec(conn *sqlite.Conn, query string, resultFn func(stmt *sqlite.Stmt) error, args ...any) error { stmt, err := conn.Prepare(query) if err != nil { return err } err = exec(stmt, 0, &ExecOptions{ Args: args, ResultFunc: resultFn, }) resetErr := stmt.Reset() if err == nil { err = resetErr } return err } // Execute executes an SQLite query. // // As Execute is implemented using [sqlite.Conn.Prepare], // subsequent calls to Execute with the same statement // will reuse the cached statement object. func Execute(conn *sqlite.Conn, query string, opts *ExecOptions) error { stmt, err := conn.Prepare(query) if err != nil { return err } err = exec(stmt, forbidMissing|forbidExtra, opts) resetErr := stmt.Reset() if err == nil { err = resetErr } return err } // ExecFS is an alias for [ExecuteFS]. // // Deprecated: Call [ExecuteFS] directly. func ExecFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) error { return ExecuteFS(conn, fsys, filename, opts) } // ExecuteFS executes the single statement in the given SQL file. // ExecuteFS is implemented using [sqlite.Conn.Prepare], // so subsequent calls to ExecuteFS with the same statement // will reuse the cached statement object. func ExecuteFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) error { query, err := readString(fsys, filename) if err != nil { return fmt.Errorf("sqlitex: execute: %w", err) } stmt, err := conn.Prepare(strings.TrimSpace(query)) if err != nil { return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } err = exec(stmt, forbidMissing|forbidExtra, opts) resetErr := stmt.Reset() if err != nil { // Don't strip the error query: we already do this inside exec. return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } if resetErr != nil { return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } return nil } // ExecTransient executes an SQLite query without caching the underlying query. // The interface is exactly the same as [Exec]. // It is the spiritual equivalent of sqlite3_exec. // // Deprecated: Use [ExecuteTransient]. // ExecTransient skips some argument checks for compatibility with crawshaw.io/sqlite. func ExecTransient(conn *sqlite.Conn, query string, resultFn func(stmt *sqlite.Stmt) error, args ...any) (err error) { var stmt *sqlite.Stmt var trailingBytes int stmt, trailingBytes, err = conn.PrepareTransient(query) if err != nil { return err } defer func() { ferr := stmt.Finalize() if err == nil { err = ferr } }() if trailingBytes != 0 { return fmt.Errorf("sqlitex: execute: query %q has trailing bytes", query) } return exec(stmt, 0, &ExecOptions{ Args: args, ResultFunc: resultFn, }) } // ExecuteTransient executes an SQLite query without caching the underlying query. // It is the spiritual equivalent of sqlite3_exec: // https://www.sqlite.org/c3ref/exec.html func ExecuteTransient(conn *sqlite.Conn, query string, opts *ExecOptions) (err error) { var stmt *sqlite.Stmt var trailingBytes int stmt, trailingBytes, err = conn.PrepareTransient(query) if err != nil { return err } defer func() { ferr := stmt.Finalize() if err == nil { err = ferr } }() if trailingBytes != 0 { return fmt.Errorf("sqlitex: execute: query %q has trailing bytes", query) } return exec(stmt, forbidMissing|forbidExtra, opts) } // ExecTransientFS is an alias for [ExecuteTransientFS]. // // Deprecated: Call [ExecuteTransientFS] directly. func ExecTransientFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) error { return ExecuteTransientFS(conn, fsys, filename, opts) } // ExecuteTransientFS executes the single statement in the given SQL file without // caching the underlying query. func ExecuteTransientFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) error { query, err := readString(fsys, filename) if err != nil { return fmt.Errorf("sqlitex: execute: %w", err) } stmt, _, err := conn.PrepareTransient(strings.TrimSpace(query)) if err != nil { return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } defer stmt.Finalize() err = exec(stmt, forbidMissing|forbidExtra, opts) resetErr := stmt.Reset() if err != nil { // Don't strip the error query: we already do this inside exec. return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } if resetErr != nil { return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } return nil } // PrepareTransientFS prepares an SQL statement from a file // that is not cached by the Conn. // Subsequent calls with the same query will create new Stmts. // The caller is responsible for calling [sqlite.Stmt.Finalize] on the returned Stmt // when the Stmt is no longer needed. func PrepareTransientFS(conn *sqlite.Conn, fsys fs.FS, filename string) (*sqlite.Stmt, error) { query, err := readString(fsys, filename) if err != nil { return nil, fmt.Errorf("sqlitex: prepare: %w", err) } stmt, _, err := conn.PrepareTransient(strings.TrimSpace(query)) if err != nil { return nil, fmt.Errorf("sqlitex: prepare %s: %w", filename, err) } return stmt, nil } const ( forbidMissing = 1 << iota forbidExtra ) func exec(stmt *sqlite.Stmt, flags uint8, opts *ExecOptions) (err error) { paramCount := stmt.BindParamCount() provided := newBitset(paramCount) if opts != nil { if len(opts.Args) > paramCount { return fmt.Errorf("sqlitex: %w (len(Args) > BindParamCount(); %d > %d)", sqlite.ResultRange.ToError(), len(opts.Args), paramCount) } for i, arg := range opts.Args { provided.set(i) setArg(stmt, i+1, reflect.ValueOf(arg)) } if err := setNamed(stmt, provided, flags, opts.Named); err != nil { return err } } if flags&forbidMissing != 0 && !provided.hasAll(paramCount) { i := provided.firstMissing() + 1 name := stmt.BindParamName(i) if name == "" { name = fmt.Sprintf("?%d", i) } return fmt.Errorf("sqlitex: missing argument for %s", name) } for { hasRow, err := stmt.Step() if err != nil { return err } if !hasRow { break } if opts != nil && opts.ResultFunc != nil { if err := opts.ResultFunc(stmt); err != nil { return err } } } return nil } func setArg(stmt *sqlite.Stmt, i int, v reflect.Value) { switch v.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: stmt.BindInt64(i, v.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: stmt.BindInt64(i, int64(v.Uint())) case reflect.Float32, reflect.Float64: stmt.BindFloat(i, v.Float()) case reflect.String: stmt.BindText(i, v.String()) case reflect.Bool: stmt.BindBool(i, v.Bool()) case reflect.Invalid: stmt.BindNull(i) default: if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 { stmt.BindBytes(i, v.Bytes()) } else { stmt.BindText(i, fmt.Sprint(v.Interface())) } } } func setNamed(stmt *sqlite.Stmt, provided bitset, flags uint8, args map[string]any) error { if len(args) == 0 { return nil } var unused map[string]struct{} if flags&forbidExtra != 0 { unused = make(map[string]struct{}, len(args)) for k := range args { unused[k] = struct{}{} } } for i, count := 1, stmt.BindParamCount(); i <= count; i++ { name := stmt.BindParamName(i) if name == "" { continue } arg, present := args[name] if !present { if flags&forbidMissing != 0 { // TODO(maybe): Check provided as well? return fmt.Errorf("missing parameter %s", name) } continue } delete(unused, name) provided.set(i - 1) setArg(stmt, i, reflect.ValueOf(arg)) } if len(unused) > 0 { return fmt.Errorf("%w: unknown argument %s", sqlite.ResultRange.ToError(), minStringInSet(unused)) } return nil } // ExecScript executes a script of SQL statements. // It is the same as calling [ExecuteScript] without options. func ExecScript(conn *sqlite.Conn, queries string) (err error) { return ExecuteScript(conn, queries, nil) } // ExecuteScript executes a script of SQL statements. // The script is wrapped in a SAVEPOINT transaction, // which is rolled back on any error. // // opts.ResultFunc is ignored. func ExecuteScript(conn *sqlite.Conn, queries string, opts *ExecOptions) (err error) { defer Save(conn)(&err) unused := make(map[string]struct{}) if opts != nil { for k := range opts.Named { unused[k] = struct{}{} } } for { queries = strings.TrimSpace(queries) if queries == "" { break } var stmt *sqlite.Stmt var trailingBytes int stmt, trailingBytes, err = conn.PrepareTransient(queries) if err != nil { return err } for i, n := 1, stmt.BindParamCount(); i <= n; i++ { if name := stmt.BindParamName(i); name != "" { delete(unused, name) } } usedBytes := len(queries) - trailingBytes queries = queries[usedBytes:] err = exec(stmt, forbidMissing, opts) stmt.Finalize() if err != nil { return err } } if len(unused) > 0 { return fmt.Errorf("%w: unknown argument %s", sqlite.ResultRange.ToError(), minStringInSet(unused)) } return nil } // ExecScriptFS is an alias for [ExecuteScriptFS]. // // Deprecated: Call [ExecuteScriptFS] directly. func ExecScriptFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) (err error) { return ExecuteScriptFS(conn, fsys, filename, opts) } // ExecuteScriptFS executes a script of SQL statements from a file. // The script is wrapped in a SAVEPOINT transaction, // which is rolled back on any error. func ExecuteScriptFS(conn *sqlite.Conn, fsys fs.FS, filename string, opts *ExecOptions) (err error) { queries, err := readString(fsys, filename) if err != nil { return fmt.Errorf("sqlitex: execute script: %w", err) } if err := ExecuteScript(conn, queries, opts); err != nil { return fmt.Errorf("sqlitex: execute %s: %w", filename, err) } return nil } type bitset []uint64 func newBitset(n int) bitset { return make([]uint64, (n+63)/64) } // hasAll reports whether the bitset is a superset of [0, n). func (bs bitset) hasAll(n int) bool { nbytes := (n + 63) / 64 if len(bs) < nbytes { return false } fullBytes := n / 64 for _, b := range bs[:fullBytes] { if b != ^uint64(0) { return false } } if fullBytes == nbytes { return true } mask := uint64(1)<<(n%64) - 1 return bs[nbytes-1]&mask == mask } func (bs bitset) firstMissing() int { for i, b := range bs { if b == ^uint64(0) { continue } for j := 0; j < 64; j++ { if b&(1<= 0; i-- { fmt.Fprintf(sb, "%08b", bs[i]) } return sb.String() } func minStringInSet(set map[string]struct{}) string { min := "" for k := range set { if min == "" || k < min { min = k } } return min } func readString(fsys fs.FS, filename string) (string, error) { f, err := fsys.Open(filename) if err != nil { return "", err } content := new(strings.Builder) _, err = io.Copy(content, f) f.Close() if err != nil { return "", fmt.Errorf("%s: %w", filename, err) } return content.String(), nil } zombiezen-go-sqlite-297af96/sqlitex/exec_test.go000066400000000000000000000222211501417116500217720ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "fmt" "reflect" "testing" "zombiezen.com/go/sqlite" ) func TestExec(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err := ExecTransient(conn, "CREATE TABLE t (a TEXT, b INTEGER);", nil); err != nil { t.Fatal(err) } if err := Exec(conn, "INSERT INTO t (a, b) VALUES (?, ?);", nil, "a1", 1); err != nil { t.Error(err) } if err := Exec(conn, "INSERT INTO t (a, b) VALUES (?, ?);", nil, "a2", 2); err != nil { t.Error(err) } var a []string var b []int64 fn := func(stmt *sqlite.Stmt) error { a = append(a, stmt.ColumnText(0)) b = append(b, stmt.ColumnInt64(1)) return nil } if err := ExecTransient(conn, "SELECT a, b FROM t;", fn); err != nil { t.Fatal(err) } if want := []string{"a1", "a2"}; !reflect.DeepEqual(a, want) { t.Errorf("a=%v, want %v", a, want) } if want := []int64{1, 2}; !reflect.DeepEqual(b, want) { t.Errorf("b=%v, want %v", b, want) } } func TestExecNil(t *testing.T) { err := Execute(nil, `SELECT 1;`, &ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { t.Error("ResultFunc called") return nil }, }) if err == nil { t.Error("No error returned") } else { t.Log("Execute message:", err) } err = ExecuteTransient(nil, `SELECT 1;`, &ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { t.Error("ResultFunc called") return nil }, }) if err == nil { t.Error("No error returned") } else { t.Log("ExecuteTransient message:", err) } } func TestExecBadSyntax(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() err = ExecuteTransient(conn, " \nSELECT );", &ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { t.Error("ResultFunc called") return nil }, }) if err == nil { t.Fatal("No error returned") } t.Log("Message:", err) got, ok := sqlite.ErrorOffset(err) if want := 9; got != want || ok == false { t.Errorf("sqlite.ErrorOffset(err) = %d, %t; want %d, true", got, ok, want) } } func TestExecErr(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() err = Exec(conn, "INVALID SQL STMT", nil) if err == nil { t.Error("invalid SQL did not return an error code") } if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { t.Errorf("INVALID err code=%s, want %s", got, want) } if err := Exec(conn, "CREATE TABLE t (c1, c2);", nil); err != nil { t.Error(err) } if err := Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 1, 1); err != nil { t.Error(err) } if err := Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 2, 2); err != nil { t.Error(err) } err = Exec(conn, "INSERT INTO t (c1, c2) VALUES (?, ?);", nil, 1, 1, 1) if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { t.Errorf("INSERT err code=%s, want %s", got, want) } calls := 0 customErr := fmt.Errorf("custom err") fn := func(stmt *sqlite.Stmt) error { calls++ return customErr } err = Exec(conn, "SELECT c1 FROM t;", fn) if err != customErr { t.Errorf("SELECT want err=customErr, got: %v", err) } if calls != 1 { t.Errorf("SELECT want truncated callback calls, got calls=%d", calls) } } func TestExecArgsErrors(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() t.Run("TooManyPositional", func(t *testing.T) { err := Exec(conn, `SELECT ?;`, nil, 1, 2) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { t.Errorf("code = %v; want %v", got, want) } }) t.Run("Missing", func(t *testing.T) { // Compatibility: crawshaw.io/sqlite does not check for this condition. err := Exec(conn, `SELECT ?;`, nil) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultOK; got != want { t.Errorf("code = %v; want %v", got, want) } }) } func TestExecuteArgsErrors(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() t.Run("TooManyPositional", func(t *testing.T) { err := Execute(conn, `SELECT ?;`, &ExecOptions{ Args: []any{1, 2}, }) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { t.Errorf("code = %v; want %v", got, want) } }) t.Run("ExtraNamed", func(t *testing.T) { err := Execute(conn, `SELECT :foo;`, &ExecOptions{ Named: map[string]any{ ":foo": 42, ":bar": "hi", }, }) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { t.Errorf("code = %v; want %v", got, want) } }) t.Run("Missing", func(t *testing.T) { err := Execute(conn, `SELECT ?;`, &ExecOptions{ Args: []any{}, }) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { t.Errorf("code = %v; want %v", got, want) } }) t.Run("MissingNamed", func(t *testing.T) { err := Execute(conn, `SELECT :foo;`, nil) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { t.Errorf("code = %v; want %v", got, want) } }) } func TestExecScript(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() script := ` CREATE TABLE t (a TEXT, b INTEGER); INSERT INTO t (a, b) VALUES ('a1', 1); INSERT INTO t (a, b) VALUES ('a2', 2); ` if err := ExecScript(conn, script); err != nil { t.Error(err) } sum := 0 fn := func(stmt *sqlite.Stmt) error { sum = stmt.ColumnInt(0) return nil } if err := Exec(conn, "SELECT sum(b) FROM t;", fn); err != nil { t.Fatal(err) } if sum != 3 { t.Errorf("sum=%d, want 3", sum) } } func TestExecuteScript(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() script := ` CREATE TABLE t (a TEXT, b INTEGER); INSERT INTO t (a, b) VALUES ('a1', :a1); INSERT INTO t (a, b) VALUES ('a2', :a2); ` err = ExecuteScript(conn, script, &ExecOptions{ Named: map[string]any{ ":a1": 1, ":a2": 2, }, }) if err != nil { t.Error(err) } sum := 0 fn := func(stmt *sqlite.Stmt) error { sum = stmt.ColumnInt(0) return nil } if err := Exec(conn, "SELECT sum(b) FROM t;", fn); err != nil { t.Fatal(err) } if sum != 3 { t.Errorf("sum=%d, want 3", sum) } t.Run("ExtraNamed", func(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() script := ` CREATE TABLE t (a TEXT, b INTEGER); INSERT INTO t (a, b) VALUES ('a1', :a1); INSERT INTO t (a, b) VALUES ('a2', :a2); ` err = ExecuteScript(conn, script, &ExecOptions{ Named: map[string]any{ ":a1": 1, ":a2": 2, ":a3": 3, }, }) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultRange; got != want { t.Errorf("code = %v; want %v", got, want) } }) t.Run("MissingNamed", func(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() script := ` CREATE TABLE t (a TEXT, b INTEGER); INSERT INTO t (a, b) VALUES ('a1', :a1); INSERT INTO t (a, b) VALUES ('a2', :a2); ` err = ExecuteScript(conn, script, &ExecOptions{ Named: map[string]any{ ":a1": 1, }, }) t.Log(err) if got, want := sqlite.ErrCode(err), sqlite.ResultError; got != want { t.Errorf("code = %v; want %v", got, want) } }) } func TestBitsetHasAll(t *testing.T) { tests := []struct { bs bitset n int want bool }{ { bs: bitset{}, n: 0, want: true, }, { bs: bitset{0}, n: 1, want: false, }, { bs: bitset{0x0000000000000001}, n: 1, want: true, }, { bs: bitset{0x8000000000000001}, n: 1, want: true, }, { bs: bitset{0x0000000000000001}, n: 2, want: false, }, { bs: bitset{0xffffffffffffffff}, n: 64, want: true, }, { bs: bitset{0xffffffffffffffff}, n: 65, want: false, }, { bs: bitset{0xffffffffffffffff, 0x0000000000000000}, n: 65, want: false, }, { bs: bitset{0xffffffffffffffff, 0x0000000000000001}, n: 65, want: true, }, { bs: bitset{0x7fffffffffffffff, 0x0000000000000001}, n: 65, want: false, }, } for _, test := range tests { if got := test.bs.hasAll(test.n); got != test.want { t.Errorf("%v.hasAll(%d) = %t; want %t", test.bs, test.n, got, test.want) } } } zombiezen-go-sqlite-297af96/sqlitex/pool.go000066400000000000000000000152721501417116500207700ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "context" "fmt" "sync" "zombiezen.com/go/sqlite" ) // PoolOptions is the set of optional arguments to [NewPool]. type PoolOptions struct { // Flags is interpreted the same way as the argument to [sqlite.Open]. // A Flags value of 0 defaults to: // // - SQLITE_OPEN_READWRITE // - SQLITE_OPEN_CREATE // - SQLITE_OPEN_WAL // - SQLITE_OPEN_URI Flags sqlite.OpenFlags // PoolSize sets an explicit size to the pool. // If less than 1, a reasonable default is used. PoolSize int // PrepareConn is called for each connection in the pool to set up functions // and other connection-specific state. PrepareConn ConnPrepareFunc } // Pool is a pool of SQLite connections. // It is safe for use by multiple goroutines concurrently. type Pool struct { free chan *sqlite.Conn closed chan struct{} prepare ConnPrepareFunc mu sync.Mutex all map[*sqlite.Conn]context.CancelFunc inited map[*sqlite.Conn]struct{} } // Open opens a fixed-size pool of SQLite connections. // // Deprecated: [NewPool] supports more options. func Open(uri string, flags sqlite.OpenFlags, poolSize int) (pool *Pool, err error) { return NewPool(uri, PoolOptions{ Flags: flags, PoolSize: poolSize, }) } // NewPool opens a fixed-size pool of SQLite connections. func NewPool(uri string, opts PoolOptions) (pool *Pool, err error) { if uri == ":memory:" { return nil, strerror{msg: `sqlite: ":memory:" does not work with multiple connections, use "file::memory:?mode=memory&cache=shared"`} } poolSize := opts.PoolSize if poolSize < 1 { poolSize = 10 } p := &Pool{ free: make(chan *sqlite.Conn, poolSize), closed: make(chan struct{}), prepare: opts.PrepareConn, } defer func() { // If an error occurred, call Close outside the lock so this doesn't deadlock. if err != nil { p.Close() } }() flags := opts.Flags if flags == 0 { flags = sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL | sqlite.OpenURI } // TODO(maybe) // sqlitex_pool is also defined in package sqlite // const sqlitex_pool = sqlite.OpenFlags(0x01000000) // flags |= sqlitex_pool p.all = make(map[*sqlite.Conn]context.CancelFunc) for i := 0; i < poolSize; i++ { conn, err := sqlite.OpenConn(uri, flags) if err != nil { return nil, err } p.free <- conn p.all[conn] = func() {} } return p, nil } // Get returns an SQLite connection from the Pool. // // Deprecated: Use [Pool.Take] instead. // Get is an alias for backward compatibility. func (p *Pool) Get(ctx context.Context) *sqlite.Conn { if ctx == nil { ctx = context.Background() } conn, _ := p.Take(ctx) return conn } // Take returns an SQLite connection from the Pool. // // If no connection is available, // Take will block until at least one connection is returned with [Pool.Put], // or until either the Pool is closed or the context is canceled. // If no connection can be obtained // or an error occurs while preparing the connection, // an error is returned. // // The provided context is also used to control the execution lifetime of the connection. // See [sqlite.Conn.SetInterrupt] for details. // // Applications must ensure that all non-nil Conns returned from Take // are returned to the same Pool with [Pool.Put]. func (p *Pool) Take(ctx context.Context) (*sqlite.Conn, error) { select { case conn := <-p.free: ctx, cancel := context.WithCancel(ctx) conn.SetInterrupt(ctx.Done()) p.mu.Lock() p.all[conn] = cancel inited := true if p.prepare != nil { _, inited = p.inited[conn] } p.mu.Unlock() if !inited { if err := p.prepare(conn); err != nil { p.put(conn) return nil, fmt.Errorf("get sqlite connection: %w", err) } p.mu.Lock() if p.inited == nil { p.inited = make(map[*sqlite.Conn]struct{}) } p.inited[conn] = struct{}{} p.mu.Unlock() } return conn, nil case <-ctx.Done(): return nil, fmt.Errorf("get sqlite connection: %w", ctx.Err()) case <-p.closed: return nil, fmt.Errorf("get sqlite connection: pool closed") } } // Put puts an SQLite connection back into the Pool. // // Put will panic if the conn was not originally created by p. // Put(nil) is a no-op. // // Applications must ensure that all non-nil Conns returned from [Pool.Get] // are returned to the same Pool with Put. func (p *Pool) Put(conn *sqlite.Conn) { if conn == nil { // See https://github.com/zombiezen/go-sqlite/issues/17 return } query := conn.CheckReset() if query != "" { panic(fmt.Sprintf( "connection returned to pool has active statement: %q", query)) } p.put(conn) } func (p *Pool) put(conn *sqlite.Conn) { p.mu.Lock() cancel, found := p.all[conn] if found { p.all[conn] = func() {} } p.mu.Unlock() if !found { panic("sqlite.Pool.Put: connection not created by this pool") } conn.SetInterrupt(nil) cancel() p.free <- conn } // Close interrupts and closes all the connections in the Pool, // blocking until all connections are returned to the Pool. func (p *Pool) Close() (err error) { close(p.closed) p.mu.Lock() n := len(p.all) cancelList := make([]context.CancelFunc, 0, n) for conn, cancel := range p.all { cancelList = append(cancelList, cancel) p.all[conn] = func() {} } p.mu.Unlock() for _, cancel := range cancelList { cancel() } for closed := 0; closed < n; closed++ { conn := <-p.free if err2 := conn.Close(); err == nil { err = err2 } } return } // A ConnPrepareFunc is called for each connection in a pool // to set up connection-specific state. // It must be safe to call from multiple goroutines. // // If the ConnPrepareFunc returns an error, // then it will be called the next time the connection is about to be used. // Once ConnPrepareFunc returns nil for a given connection, // it will not be called on that connection again. type ConnPrepareFunc func(conn *sqlite.Conn) error type strerror struct { msg string } func (err strerror) Error() string { return err.msg } zombiezen-go-sqlite-297af96/sqlitex/pool_test.go000066400000000000000000000167041501417116500220300ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex_test import ( "context" "errors" "fmt" "os" "path/filepath" "sync" "testing" "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) const poolSize = 20 // newMemPool returns new sqlitex.Pool attached to new database opened in memory. // // the pool is initialized with size=poolSize. // any error is t.Fatal. func newMemPool(t *testing.T) *sqlitex.Pool { t.Helper() dbpool, err := sqlitex.NewPool("file::memory:?mode=memory&cache=shared", sqlitex.PoolOptions{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI | sqlite.OpenSharedCache, PoolSize: poolSize, }) if err != nil { t.Fatal(err) } return dbpool } func TestPool(t *testing.T) { dbpool := newMemPool(t) defer func() { if err := dbpool.Close(); err != nil { t.Error(err) } }() c := dbpool.Get(nil) c.Prep("DROP TABLE IF EXISTS footable;").Step() if hasRow, err := c.Prep("CREATE TABLE footable (col1 integer);").Step(); err != nil { t.Fatal(err) } else if hasRow { t.Errorf("CREATE TABLE reports having a row") } dbpool.Put(c) c = nil var wg sync.WaitGroup for i := 0; i < poolSize; i++ { wg.Add(1) go func(i int) { for j := 0; j < 10; j++ { testInsert(t, fmt.Sprintf("%d-%d", i, j), dbpool) } wg.Done() }(i) } wg.Wait() c = dbpool.Get(nil) defer dbpool.Put(c) stmt := c.Prep("SELECT COUNT(*) FROM footable;") if hasRow, err := stmt.Step(); err != nil { t.Fatal(err) } else if hasRow { count := int(stmt.ColumnInt64(0)) if want := poolSize * 10 * insertCount; count != want { t.Errorf("SELECT COUNT(*) = %d, want %d", count, want) } } else { t.Errorf("SELECT COUNT(*) reports not having a row") } stmt.Reset() } const insertCount = 120 func testInsert(t *testing.T, id string, dbpool *sqlitex.Pool) { c := dbpool.Get(nil) defer dbpool.Put(c) begin := c.Prep("BEGIN;") commit := c.Prep("COMMIT;") stmt := c.Prep("INSERT INTO footable (col1) VALUES (?);") if _, err := begin.Step(); err != nil { t.Errorf("id=%s: BEGIN step: %v", id, err) } for i := int64(0); i < insertCount; i++ { if err := stmt.Reset(); err != nil { t.Errorf("id=%s: reset: %v", id, err) } stmt.BindInt64(1, i) if _, err := stmt.Step(); err != nil { t.Errorf("id=%s: step: %v", id, err) } } if _, err := commit.Step(); err != nil { t.Errorf("id=%s: COMMIT step: %v", id, err) } } func TestPoolAfterClose(t *testing.T) { // verify that Get after close never try to initialize a Conn and segfault dbpool := newMemPool(t) err := dbpool.Close() if err != nil { t.Fatal(err) } for i := 0; i < 10*poolSize; i++ { conn := dbpool.Get(nil) if conn != nil { t.Fatal("dbpool: Get after Close -> !nil conn") } } } func TestSharedCacheLock(t *testing.T) { dir, err := os.MkdirTemp("", "sqlite-test-") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) dbFile := filepath.Join(dir, "awal.db") c0, err := sqlite.OpenConn(dbFile, 0) if err != nil { t.Fatal(err) } defer func() { if err := c0.Close(); err != nil { t.Error(err) } }() // TODO(someday): CI fails without this old behavior. c0.SetBusyTimeout(10 * time.Second) err = sqlitex.ExecScript(c0, ` DROP TABLE IF EXISTS t; CREATE TABLE t (c, content BLOB); DROP TABLE IF EXISTS t2; CREATE TABLE t2 (c); INSERT INTO t2 (c) VALUES ('hello'); `) if err != nil { t.Fatal(err) } c1, err := sqlite.OpenConn(dbFile, 0) if err != nil { t.Fatal(err) } defer func() { if err := c1.Close(); err != nil { t.Error(err) } }() // TODO(someday): CI fails without this old behavior. c1.SetBusyTimeout(10 * time.Second) c0Lock := func() { if _, err := c0.Prep("BEGIN;").Step(); err != nil { t.Fatal(err) } if _, err := c0.Prep("INSERT INTO t (c, content) VALUES (0, 'hi');").Step(); err != nil { t.Fatal(err) } } c0Unlock := func() { if err := sqlitex.Execute(c0, "COMMIT;", nil); err != nil { t.Fatal(err) } } c0Lock() stmt := c1.Prep("INSERT INTO t (c) VALUES (1);") done := make(chan struct{}) go func() { if _, err := stmt.Step(); err != nil { t.Error(err) return } close(done) }() time.Sleep(10 * time.Millisecond) select { case <-done: t.Error("insert done while transaction was held") default: } c0Unlock() // End the initial transaction, allowing the goroutine to complete select { case <-done: case <-time.After(500 * time.Millisecond): t.Error("second connection insert not completing") } // TODO: It is possible for stmt.Reset to return SQLITE_LOCKED. // Work out why and find a way to test it. } func TestPoolPutMatch(t *testing.T) { dbpool0 := newMemPool(t) dbpool1 := newMemPool(t) defer func() { if err := dbpool0.Close(); err != nil { t.Error(err) } if err := dbpool1.Close(); err != nil { t.Error(err) } }() func() { c := dbpool0.Get(nil) defer func() { if r := recover(); r == nil { t.Error("expect put mismatch panic, got none") } dbpool0.Put(c) }() dbpool1.Put(c) }() } // See https://github.com/crawshaw/sqlite/issues/119 and // https://github.com/zombiezen/go-sqlite/issues/14 func TestPoolWALClose(t *testing.T) { dbName := filepath.Join(t.TempDir(), "wal-close.db") pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, PoolSize: 10, }) if err != nil { t.Fatal(err) } conn := pool.Get(context.Background()) if _, err := os.Stat(dbName + "-wal"); err != nil { t.Error(err) } err = sqlitex.ExecTransient(conn, `CREATE TABLE foo (id integer primary key);`, nil) if err != nil { t.Error(err) } pool.Put(conn) if err := pool.Close(); err != nil { t.Error(err) } if _, err := os.Stat(dbName + "-wal"); !errors.Is(err, os.ErrNotExist) { t.Errorf("After close: %v; want %v", err, os.ErrNotExist) } } func TestPoolPrepareConn(t *testing.T) { dbName := filepath.Join(t.TempDir(), "foo.db") pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ PrepareConn: func(conn *sqlite.Conn) error { err := sqlitex.ExecuteTransient(conn, `PRAGMA foreign_keys = on;`, nil) if err != nil { t.Error("Prepare internal error:", err) } return err }, }) if err != nil { t.Fatal(err) } defer func() { if err := pool.Close(); err != nil { t.Error("Close:", err) } }() conn := pool.Get(context.Background()) if conn == nil { t.Fatal("conn == nil") } defer pool.Put(conn) fkEnabled := false err = sqlitex.ExecuteTransient(conn, `PRAGMA foreign_keys;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fkEnabled = stmt.ColumnBool(0) return nil }, }) if err != nil { t.Fatal(err) } if !fkEnabled { t.Error("foreign_keys not enabled") } } zombiezen-go-sqlite-297af96/sqlitex/query.go000066400000000000000000000055261501417116500211650ustar00rootroot00000000000000// Copyright 2021 Roxy Light // SPDX-License-Identifier: ISC package sqlitex import ( "errors" "zombiezen.com/go/sqlite" ) var ( errNoResults = errors.New("sqlite: statement has no results") errMultipleResults = errors.New("sqlite: statement has multiple result rows") ) func resultSetup(stmt *sqlite.Stmt) error { hasRow, err := stmt.Step() if err != nil { stmt.Reset() return err } if !hasRow { stmt.Reset() return errNoResults } return nil } func resultTeardown(stmt *sqlite.Stmt) error { hasRow, err := stmt.Step() if err != nil { stmt.Reset() return err } if hasRow { stmt.Reset() return errMultipleResults } return stmt.Reset() } // ResultBool reports whether the first column of the first and only row // produced by running stmt // is non-zero. // It returns an error if there is not exactly one result row. func ResultBool(stmt *sqlite.Stmt) (bool, error) { res, err := ResultInt64(stmt) return res != 0, err } // ResultInt returns the first column of the first and only row // produced by running stmt // as an integer. // It returns an error if there is not exactly one result row. func ResultInt(stmt *sqlite.Stmt) (int, error) { res, err := ResultInt64(stmt) return int(res), err } // ResultInt64 returns the first column of the first and only row // produced by running stmt // as an integer. // It returns an error if there is not exactly one result row. func ResultInt64(stmt *sqlite.Stmt) (int64, error) { if err := resultSetup(stmt); err != nil { return 0, err } res := stmt.ColumnInt64(0) if err := resultTeardown(stmt); err != nil { return 0, err } return res, nil } // ResultText returns the first column of the first and only row // produced by running stmt // as text. // It returns an error if there is not exactly one result row. func ResultText(stmt *sqlite.Stmt) (string, error) { if err := resultSetup(stmt); err != nil { return "", err } res := stmt.ColumnText(0) if err := resultTeardown(stmt); err != nil { return "", err } return res, nil } // ResultFloat returns the first column of the first and only row // produced by running stmt // as a real number. // It returns an error if there is not exactly one result row. func ResultFloat(stmt *sqlite.Stmt) (float64, error) { if err := resultSetup(stmt); err != nil { return 0, err } res := stmt.ColumnFloat(0) if err := resultTeardown(stmt); err != nil { return 0, err } return res, nil } // ResultBytes reads the first column of the first and only row // produced by running stmt into buf, // returning the number of bytes read. // It returns an error if there is not exactly one result row. func ResultBytes(stmt *sqlite.Stmt, buf []byte) (int, error) { if err := resultSetup(stmt); err != nil { return 0, err } read := stmt.ColumnBytes(0, buf) if err := resultTeardown(stmt); err != nil { return 0, err } return read, nil } zombiezen-go-sqlite-297af96/sqlitex/query_test.go000066400000000000000000000171471501417116500222260ustar00rootroot00000000000000// Copyright 2024 Roxy Light // SPDX-License-Identifier: ISC package sqlitex import ( "testing" "zombiezen.com/go/sqlite" ) func TestResultInt64(t *testing.T) { conn, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() err = ExecuteScript(conn, ` CREATE TABLE foo ( id integer not null primary key ); INSERT INTO foo VALUES (1), (2);`, nil) if err != nil { t.Fatal(err) } t.Run("Single", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT 42;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() const want = 42 got, err := ResultInt64(stmt) if got != want || err != nil { t.Errorf("ResultInt64(...) = %d, %v; want %d, ", got, err, want) } }) t.Run("Multiple", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() n, err := ResultInt64(stmt) if n != 0 || err == nil { t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) t.Run("NoRows", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() n, err := ResultInt64(stmt) if n != 0 || err == nil { t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) } func TestResultBool(t *testing.T) { conn, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() err = ExecuteScript(conn, ` CREATE TABLE foo ( id integer not null primary key ); INSERT INTO foo VALUES (1), (2);`, nil) if err != nil { t.Fatal(err) } t.Run("False", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT false;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultBool(stmt) if got || err != nil { t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) } }) t.Run("True", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT true;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultBool(stmt) if !got || err != nil { t.Errorf("ResultBool(...) = %t, %v; want true, ", got, err) } }) t.Run("Multiple", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultBool(stmt) if got || err == nil { t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) } else { t.Log("Returned (expected) error:", err) } }) t.Run("NoRows", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo WHERE id > 3;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultBool(stmt) if got || err == nil { t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) } else { t.Log("Returned (expected) error:", err) } }) } func TestResultText(t *testing.T) { conn, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() err = ExecuteScript(conn, ` CREATE TABLE foo ( id integer not null primary key, my_blob blob ); INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) if err != nil { t.Fatal(err) } t.Run("Single", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() const want = "hi" got, err := ResultText(stmt) if got != want || err != nil { t.Errorf("ResultText(...) = %q, %v; want %q, ", got, err, want) } }) t.Run("Multiple", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultText(stmt) if got != "" || err == nil { t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) } else { t.Log("Returned (expected) error:", err) } }) t.Run("NoRows", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() got, err := ResultText(stmt) if got != "" || err == nil { t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) } else { t.Log("Returned (expected) error:", err) } }) } func TestResultFloat(t *testing.T) { conn, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() err = ExecuteScript(conn, ` CREATE TABLE foo ( id integer not null primary key ); INSERT INTO foo VALUES (1), (2);`, nil) if err != nil { t.Fatal(err) } t.Run("Single", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT 42;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() const want = 42.0 got, err := ResultFloat(stmt) if got != want || err != nil { t.Errorf("ResultFloat(...) = %g, %v; want %g, ", got, err, want) } }) t.Run("Multiple", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() n, err := ResultFloat(stmt) if n != 0 || err == nil { t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) t.Run("NoRows", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() n, err := ResultFloat(stmt) if n != 0 || err == nil { t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) } func TestResultBytes(t *testing.T) { conn, err := sqlite.OpenConn(":memory:") if err != nil { t.Fatal(err) } defer func() { if err := conn.Close(); err != nil { t.Error(err) } }() err = ExecuteScript(conn, ` CREATE TABLE foo ( id integer not null primary key, my_blob blob ); INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) if err != nil { t.Fatal(err) } t.Run("Single", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() const want = "hi" buf := make([]byte, 4096) n, err := ResultBytes(stmt, buf) if n != len(want) || err != nil { t.Errorf("ResultBytes(...) = %d, %v; want %d, ", n, err, len(want)) } if got := string(buf[:n]); got != want { t.Errorf("result = %q; want %q", got, want) } }) t.Run("Multiple", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() buf := make([]byte, 4096) n, err := ResultBytes(stmt, buf) if n != 0 || err == nil { t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) t.Run("NoRows", func(t *testing.T) { stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) if err != nil { t.Fatal(err) } defer stmt.Finalize() buf := make([]byte, 4096) n, err := ResultBytes(stmt, buf) if n != 0 || err == nil { t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) } else { t.Log("Returned (expected) error:", err) } }) } zombiezen-go-sqlite-297af96/sqlitex/rand_id.go000066400000000000000000000031671501417116500214170ustar00rootroot00000000000000// Copyright (c) 2019 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "crypto/rand" "fmt" "math/big" "zombiezen.com/go/sqlite" ) // InsertRandID executes stmt with a random value in the range [min, max) for $param. func InsertRandID(stmt *sqlite.Stmt, param string, min, max int64) (int64, error) { if min < 0 { return 0, fmt.Errorf("sqlitex.InsertRandID: min (%d) is negative", min) } for i := 0; ; i++ { v, err := rand.Int(rand.Reader, big.NewInt(max-min)) if err != nil { return 0, fmt.Errorf("sqlitex.InsertRandID: %w", err) } id := v.Int64() + min stmt.Reset() stmt.SetInt64(param, id) _, err = stmt.Step() if err == nil { return id, nil } if i >= 100 || sqlite.ErrCode(err) != sqlite.ResultConstraintPrimaryKey { return 0, fmt.Errorf("sqlitex.InsertRandID: %w", err) } } } zombiezen-go-sqlite-297af96/sqlitex/rand_id_test.go000066400000000000000000000034521501417116500224530ustar00rootroot00000000000000// Copyright (c) 2019 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex_test import ( "testing" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func TestRandID(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err := sqlitex.ExecTransient(conn, "CREATE TABLE t (key PRIMARY KEY, val TEXT);", nil); err != nil { t.Fatal(err) } stmt := conn.Prep(`INSERT INTO t (key, val) VALUES ($key, $val);`) stmt.SetText("$val", "value") for i := 0; i < 10; i++ { const min, max = 1000, 10000 id, err := sqlitex.InsertRandID(stmt, "$key", min, max) if err != nil { t.Fatal(err) } if id < min || id >= max { t.Errorf("id %d out of range [%d, %d)", id, min, max) } countStmt := conn.Prep("SELECT count(*) FROM t WHERE key = $key;") countStmt.SetInt64("$key", id) if count, err := sqlitex.ResultInt(countStmt); err != nil { t.Fatal(err) } else if count != 1 { t.Errorf("missing key %d", id) } } } zombiezen-go-sqlite-297af96/sqlitex/savepoint.go000066400000000000000000000146031501417116500220240ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "fmt" "runtime" "strings" "zombiezen.com/go/sqlite" ) // Save creates a named SQLite transaction using SAVEPOINT. // // On success Savepoint returns a releaseFn that will call either // RELEASE or ROLLBACK depending on whether the parameter *error // points to a nil or non-nil error. This is designed to be deferred. // // https://www.sqlite.org/lang_savepoint.html func Save(conn *sqlite.Conn) (releaseFn func(*error)) { name := "sqlitex.Save" // safe as names can be reused var pc [3]uintptr if n := runtime.Callers(0, pc[:]); n > 0 { frames := runtime.CallersFrames(pc[:n]) if _, more := frames.Next(); more { // runtime.Callers if _, more := frames.Next(); more { // savepoint.Save frame, _ := frames.Next() // caller we care about if frame.Function != "" { name = frame.Function } } } } releaseFn, err := savepoint(conn, name) if err != nil { if sqlite.ErrCode(err) == sqlite.ResultInterrupt { return func(errp *error) { if *errp == nil { *errp = err } } } panic(err) } return releaseFn } func savepoint(conn *sqlite.Conn, name string) (releaseFn func(*error), err error) { if strings.Contains(name, `"`) { return nil, fmt.Errorf("sqlitex.Savepoint: invalid name: %q", name) } if err := Execute(conn, fmt.Sprintf("SAVEPOINT %q;", name), nil); err != nil { return nil, err } releaseFn = func(errp *error) { recoverP := recover() // If a query was interrupted or if a user exec'd COMMIT or // ROLLBACK, then everything was already rolled back // automatically, thus returning the connection to autocommit // mode. if conn.AutocommitEnabled() { // There is nothing to rollback. if recoverP != nil { panic(recoverP) } return } if *errp == nil && recoverP == nil { // Success path. Release the savepoint successfully. *errp = Execute(conn, fmt.Sprintf("RELEASE %q;", name), nil) if *errp == nil { return } // Possible interrupt. Fall through to the error path. if conn.AutocommitEnabled() { // There is nothing to rollback. if recoverP != nil { panic(recoverP) } return } } orig := "" if *errp != nil { orig = (*errp).Error() + "\n\t" } // Error path. // Always run ROLLBACK even if the connection has been interrupted. oldDoneCh := conn.SetInterrupt(nil) defer conn.SetInterrupt(oldDoneCh) err := Execute(conn, fmt.Sprintf("ROLLBACK TO %q;", name), nil) if err != nil { panic(orig + err.Error()) } err = Execute(conn, fmt.Sprintf("RELEASE %q;", name), nil) if err != nil { panic(orig + err.Error()) } if recoverP != nil { panic(recoverP) } } return releaseFn, nil } // Transaction creates a DEFERRED SQLite transaction. // // On success Transaction returns an endFn that will call either // COMMIT or ROLLBACK depending on whether the parameter *error // points to a nil or non-nil error. This is designed to be deferred. // // https://www.sqlite.org/lang_transaction.html func Transaction(conn *sqlite.Conn) (endFn func(*error)) { endFn, err := transaction(conn, "DEFERRED") if err != nil { if sqlite.ErrCode(err) == sqlite.ResultInterrupt { return func(errp *error) { if *errp == nil { *errp = err } } } panic(err) } return endFn } // ImmediateTransaction creates an IMMEDIATE SQLite transaction. // // On success ImmediateTransaction returns an endFn that will call either // COMMIT or ROLLBACK depending on whether the parameter *error // points to a nil or non-nil error. This is designed to be deferred. // // https://www.sqlite.org/lang_transaction.html func ImmediateTransaction(conn *sqlite.Conn) (endFn func(*error), err error) { endFn, err = transaction(conn, "IMMEDIATE") if err != nil { return func(*error) {}, err } return endFn, nil } // ExclusiveTransaction creates an EXCLUSIVE SQLite transaction. // // On success ImmediateTransaction returns an endFn that will call either // COMMIT or ROLLBACK depending on whether the parameter *error // points to a nil or non-nil error. This is designed to be deferred. // // https://www.sqlite.org/lang_transaction.html func ExclusiveTransaction(conn *sqlite.Conn) (endFn func(*error), err error) { endFn, err = transaction(conn, "EXCLUSIVE") if err != nil { return func(*error) {}, err } return endFn, nil } func transaction(conn *sqlite.Conn, mode string) (endFn func(*error), err error) { if err := Execute(conn, "BEGIN "+mode+";", nil); err != nil { return nil, err } endFn = func(errp *error) { recoverP := recover() // If a query was interrupted or if a user exec'd COMMIT or // ROLLBACK, then everything was already rolled back // automatically, thus returning the connection to autocommit // mode. if conn.AutocommitEnabled() { // There is nothing to rollback. if recoverP != nil { panic(recoverP) } return } if *errp == nil && recoverP == nil { // Success path. Commit the transaction. *errp = Execute(conn, "COMMIT;", nil) if *errp == nil { return } // Possible interrupt. Fall through to the error path. if conn.AutocommitEnabled() { // There is nothing to rollback. if recoverP != nil { panic(recoverP) } return } } orig := "" if *errp != nil { orig = (*errp).Error() + "\n\t" } // Error path. // Always run ROLLBACK even if the connection has been interrupted. oldDoneCh := conn.SetInterrupt(nil) defer conn.SetInterrupt(oldDoneCh) err := Execute(conn, "ROLLBACK;", nil) if err != nil { panic(orig + err.Error()) } if recoverP != nil { panic(recoverP) } } return endFn, nil } zombiezen-go-sqlite-297af96/sqlitex/savepoint_test.go000066400000000000000000000256711501417116500230720ustar00rootroot00000000000000// Copyright (c) 2018 David Crawshaw // Copyright (c) 2021 Roxy Light // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // // SPDX-License-Identifier: ISC package sqlitex import ( "context" "errors" "path/filepath" "strings" "testing" "time" "os" "zombiezen.com/go/sqlite" ) func TestSavepointExec(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err := Exec(conn, "CREATE TABLE t (c1);", nil); err != nil { t.Fatal(err) } countFn := func() int { var count int fn := func(stmt *sqlite.Stmt) error { count = stmt.ColumnInt(0) return nil } if err := Exec(conn, "SELECT count(*) FROM t;", fn); err != nil { t.Fatal(err) } return count } errNoSuccess := errors.New("succeed=false") insert := func(succeed bool) (err error) { defer Save(conn)(&err) if err := Exec(conn, `INSERT INTO t VALUES ('hello');`, nil); err != nil { t.Fatal(err) } if succeed { return nil } return errNoSuccess } if err := insert(true); err != nil { t.Fatal(err) } if got := countFn(); got != 1 { t.Errorf("expecting 1 row, got %d", got) } if err := insert(true); err != nil { t.Fatal(err) } if got := countFn(); got != 2 { t.Errorf("expecting 2 rows, got %d", got) } if err := insert(false); err != errNoSuccess { t.Errorf("expecting insert to fail with errNoSuccess, got %v", err) } if got := countFn(); got != 2 { t.Errorf("expecting 2 rows, got %d", got) } } func TestSavepointPanic(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err := Exec(conn, "CREATE TABLE t (c1);", nil); err != nil { t.Fatal(err) } if err := Exec(conn, `INSERT INTO t VALUES ('one');`, nil); err != nil { t.Fatal(err) } defer func() { p := recover() if p == nil { t.Errorf("panic expected") } if err, isErr := p.(error); !isErr || !strings.Contains(err.Error(), "sqlite") { t.Errorf("panic is not an sqlite error: %v", err) } count := 0 fn := func(stmt *sqlite.Stmt) error { count = stmt.ColumnInt(0) return nil } if err := Exec(conn, "SELECT count(*) FROM t;", fn); err != nil { t.Error(err) } if count != 1 { t.Errorf("got %d rows, want 1", count) } }() if err := doPanic(conn); err != nil { t.Errorf("unexpected error: %v", err) } } func doPanic(conn *sqlite.Conn) (err error) { defer Save(conn)(&err) if err := Exec(conn, `INSERT INTO t VALUES ('hello');`, nil); err != nil { return err } conn.Prep("SELECT bad query") // panics return nil } func TestSavepointDone(t *testing.T) { doneCh := make(chan struct{}) conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() conn.SetInterrupt(doneCh) close(doneCh) relFn := Save(conn) relFn(&err) if code, want := sqlite.ErrCode(err), sqlite.ResultInterrupt; code != want { t.Errorf("savepoint release function error code is %v; want %v", code, want) } } func TestSavepointReleaseTx(t *testing.T) { conn1, err := sqlite.OpenConn("file::memory:?mode=memory&cache=shared", 0) if err != nil { t.Fatal(err) } defer conn1.Close() conn2, err := sqlite.OpenConn("file::memory:?mode=memory&cache=shared", 0) if err != nil { t.Fatal(err) } defer conn2.Close() Exec(conn1, "DROP TABLE t;", nil) if err := Exec(conn1, "CREATE TABLE t (c1);", nil); err != nil { t.Fatal(err) } countFn := func() int { var count int fn := func(stmt *sqlite.Stmt) error { count = stmt.ColumnInt(0) return nil } if err := Exec(conn2, "SELECT count(*) FROM t;", fn); err != nil { t.Fatal(err) } return count } errNoSuccess := errors.New("succeed=false") insert := func(succeed bool) (err error) { defer Save(conn1)(&err) if err := Exec(conn1, `INSERT INTO t VALUES ('hello');`, nil); err != nil { t.Fatal(err) } if succeed { return nil } return errNoSuccess } if err := insert(true); err != nil { t.Fatal(err) } if got := countFn(); got != 1 { t.Errorf("expecting 1 row, got %d", got) } if err := insert(false); err == nil { t.Fatal(err) } // If the transaction is still open, countFn will get stuck // on conn2 waiting for conn1's write lock to release. if got := countFn(); got != 1 { t.Errorf("expecting 1 row, got %d", got) } } func TestSavepointInterruptRollback(t *testing.T) { conn, err := sqlite.OpenConn("file::memory:?mode=memory&cache=shared", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err = ExecScript(conn, ` DROP TABLE IF EXISTS t; CREATE TABLE t (c); `); err != nil { t.Fatal(err) } releaseFn := Save(conn) if err := Exec(conn, `INSERT INTO t (c) VALUES (1);`, nil); err != nil { t.Fatal(err) } releaseFn(&err) if err != nil { t.Fatalf("relaseFn err: %v", err) } ctx, cancel := context.WithCancel(context.Background()) conn.SetInterrupt(ctx.Done()) releaseFn1 := Save(conn) if err := Exec(conn, `INSERT INTO t (c) VALUES (2);`, nil); err != nil { t.Fatal(err) } releaseFn2 := Save(conn) if err := Exec(conn, `INSERT INTO t (c) VALUES (3);`, nil); err != nil { t.Fatal(err) } cancel() if err := Exec(conn, `INSERT INTO t (c) VALUES (3);`, nil); err == nil || sqlite.ErrCode(err) != sqlite.ResultInterrupt { t.Fatalf("want SQLITE_INTERRUPT, got %v", err) } err = context.Canceled releaseFn2(&err) // given a real error, should rollback if err != context.Canceled { t.Fatalf("relaseFn2 err: %v", err) } var errNil error releaseFn1(&errNil) // given no error, but we are interrupted, so should rollback if errNil == nil { t.Fatal("relaseFn1 errNil is still nil, should have been set to interrupt") } if sqlite.ErrCode(errNil) != sqlite.ResultInterrupt { t.Fatalf("relaseFn1 errNil=%v, want SQLITE_INTERRUPT", errNil) } conn.SetInterrupt(nil) got, err := ResultInt(conn.Prep("SELECT count(*) FROM t;")) if err != nil { t.Fatal(err) } if got != 1 { t.Errorf("want 1 row, got %d", got) } } var veryLongScript = ` drop table if exists naturals; create table naturals ( n integer unique primary key asc, isprime bool, factor integer); with recursive nn (n) as ( select 2 union all select n+1 as newn from nn where newn < 1e10 ) insert into naturals select n, 1, null from nn; insert or replace into naturals with recursive product (prime,composite) as ( select n, n*n as sqr from naturals where sqr <= (select max(n) from naturals) union all select prime, composite+prime as prod from product where prod <= (select max(n) from naturals) ) select n, 0, prime from product join naturals on (product.composite = naturals.n) ; ` func TestSavepointInterruptRollbackLongQuery(t *testing.T) { conn, err := sqlite.OpenConn("file::memory:?mode=memory&cache=shared", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err = ExecScript(conn, ` DROP TABLE IF EXISTS t; CREATE TABLE t (c); `); err != nil { t.Fatal(err) } releaseFn := Save(conn) if err := Exec(conn, `INSERT INTO t (c) VALUES (1);`, nil); err != nil { t.Fatal(err) } releaseFn(&err) if err != nil { t.Fatalf("relaseFn err: %v", err) } ctx, cancel := context.WithCancel(context.Background()) conn.SetInterrupt(ctx.Done()) testDone := make(chan struct{}) go func() { defer close(testDone) defer func() { if err := recover(); err != nil { t.Log("a panic occurred during rollback\n", err) t.Fail() } }() defer Save(conn)(&err) if err := Exec(conn, `INSERT INTO t (c) VALUES (3);`, nil); err != nil { t.Errorf("interrupted too early") return } err = ExecScript(conn, veryLongScript) }() time.Sleep(20 * time.Millisecond) cancel() <-testDone conn.SetInterrupt(nil) got, err := ResultInt(conn.Prep("SELECT count(*) FROM t;")) if err != nil { t.Fatal(err) } if got != 1 { t.Errorf("want 1 row, got %d", got) } } func TestSavepointBusySnapshot(t *testing.T) { dir, err := os.MkdirTemp("", "sqlitex-test-") if err != nil { t.Fatal(err) } db := filepath.Join(dir, "busysnapshot.db") // No SQLITE_OPEN_SHAREDCACHE. flags := sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL conn0, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer conn0.Close() conn1, err := sqlite.OpenConn(db, flags) if err != nil { t.Fatal(err) } defer conn1.Close() if err = ExecScript(conn0, ` DROP TABLE IF EXISTS t; CREATE TABLE t (c, b BLOB); INSERT INTO t (c, b) VALUES (4, 'hi'); `); err != nil { t.Fatal(err) } conn0Release0 := Save(conn0) c, err := ResultInt(conn0.Prep("SELECT count(*) FROM t WHERE c > 3;")) if err != nil { t.Fatal(err) } // An insert on conn1 invalidates the deferred transaction on conn0. if err := Exec(conn1, "INSERT INTO t (c) VALUES (4);", nil); err != nil { t.Fatal(err) } stmt := conn0.Prep("UPDATE t SET c = $c WHERE c = 4;") stmt.SetInt64("$c", int64(c)) _, conn0Err := stmt.Step() if sqlite.ErrCode(conn0Err) != sqlite.ResultBusySnapshot { t.Fatalf("want SQLITE_BUSY_SNAPSHOT, got: %v", conn0Err) } conn0Release0(&conn0Err) if sqlite.ErrCode(conn0Err) != sqlite.ResultBusySnapshot { t.Fatalf("after savepoint want SQLITE_BUSY_SNAPSHOT, got: %v", conn0Err) } } func TestTransactionExec(t *testing.T) { conn, err := sqlite.OpenConn(":memory:", 0) if err != nil { t.Fatal(err) } defer conn.Close() if err := Exec(conn, "CREATE TABLE t (c1);", nil); err != nil { t.Fatal(err) } countFn := func() int { var count int fn := func(stmt *sqlite.Stmt) error { count = stmt.ColumnInt(0) return nil } if err := Exec(conn, "SELECT count(*) FROM t;", fn); err != nil { t.Fatal(err) } return count } errNoSuccess := errors.New("succeed=false") insert := func(succeed bool) (err error) { defer Transaction(conn)(&err) if err := Exec(conn, `INSERT INTO t VALUES ('hello');`, nil); err != nil { t.Fatal(err) } if succeed { return nil } return errNoSuccess } if err := insert(true); err != nil { t.Fatal(err) } if got := countFn(); got != 1 { t.Errorf("expecting 1 row, got %d", got) } if err := insert(true); err != nil { t.Fatal(err) } if got := countFn(); got != 2 { t.Errorf("expecting 2 rows, got %d", got) } if err := insert(false); err != errNoSuccess { t.Errorf("expecting insert to fail with errNoSuccess, got %v", err) } if got := countFn(); got != 2 { t.Errorf("expecting 2 rows, got %d", got) } } zombiezen-go-sqlite-297af96/vtable.go000066400000000000000000000722611501417116500176040ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package sqlite import ( "fmt" "strings" "sync" "unsafe" "modernc.org/libc" "modernc.org/libc/sys/types" lib "modernc.org/sqlite/lib" ) // A Module declares a [virtual table] that can be registered with a [Conn] // via [Conn.SetModule]. // // [virtual table]: https://sqlite.org/vtab.html type Module struct { // Connect establishes a connection to an existing virtual table. // This is the only required field. Connect VTableConnectFunc // Create is called to create a new instance of the virtual table // in response to a [CREATE VIRTUAL TABLE statement]. // If it is nil, then the virtual table is [eponymous]. // UseConnectAsCreate determines whether the virtual table is eponymous-only. // // [CREATE VIRTUAL TABLE statement]: https://sqlite.org/lang_createvtab.html // [eponymous]: https://sqlite.org/vtab.html#epovtab Create VTableConnectFunc // If UseConnectAsCreate is true and Create is nil, // then the virtual table is eponymous, but not eponymous-only. // This means that the virtual table can still be given a name // with CREATE VIRTUAL TABLE // and indicates that the virtual table has no persistent state // that needs to be created and destroyed. UseConnectAsCreate bool } // VTableConnectFunc is a [Module.Connect] or [Module.Create] callback. type VTableConnectFunc func(*Conn, *VTableConnectOptions) (VTable, *VTableConfig, error) // VTableConnectOptions is the set of arguments to a [VTableConnectFunc]. type VTableConnectOptions struct { // ModuleName is the name of the [Module] being invoked. ModuleName string // DatabaseName is the name of the database in which the new virtual table is being created. // The database name is "main" for the primary database, // or "temp" for TEMP database, // or the name given at the end of the ATTACH statement for attached databases. DatabaseName string // VTableName is the name of the name of the new virtual table. // For eponymous virtual tables, this will be the same as ModuleName. VTableName string // Arguments passed to the CREATE VIRTUAL TABLE statement. Args []string } // VTableConfig specifies the configuration of a [VTable] returned by [VTableConnectFunc]. // Declaration is the only required field. type VTableConfig struct { // Declaration must be a [CREATE TABLE statement] // that defines the columns in the virtual table and their data type. // The name of the table in this CREATE TABLE statement is ignored, // as are all constraints. // // [CREATE TABLE statement]: https://sqlite.org/lang_createtable.html Declaration string // If ConstraintSupport is true, then the virtual table implementation // guarantees that if Update or DeleteRow on [WritableVTable] // return a [ResultConstraint] error, // they will do so before any modifications to internal or persistent data structures // have been made. ConstraintSupport bool // If AllowIndirect is false, then the virtual table may only be used from top-level SQL. // If AllowIndirect is true, then the virtual table can be used in VIEWs, TRIGGERs, // and schema structures (e.g. CHECK constraints and DEFAULT clauses). // // This is the inverse of SQLITE_DIRECTONLY. // See https://sqlite.org/c3ref/c_vtab_constraint_support.html // for more details. // This defaults to false for better security. AllowIndirect bool } // VTable represents a connected [virtual table]. // // [virtual table]: https://sqlite.org/vtab.html type VTable interface { // BestIndex informs SQLite the best way to access the virtual table. // While compiling a single SQL query, // the SQLite core might call BestIndex multiple times with different inputs. // The SQLite core will then select the combination // that appears to give the best performance. BestIndex(*IndexInputs) (*IndexOutputs, error) // Open creates a new cursor. Open() (VTableCursor, error) // Disconnect releases any resources associated with the virtual table. Disconnect() error // Destroy is called when the table is "DROP"ed // to tear down any persistent data structures // and release any resources associated with the virtual table. Destroy() error } // VTableUpdateParams is the set of parameters to the [WritableVTable] Update method. type VTableUpdateParams struct { OldRowID Value NewRowID Value Columns []Value } // IsInsert reports whether the arguments represent an INSERT. // If not, then the arguments represent an UPDATE. func (p VTableUpdateParams) IsInsert() bool { return p.OldRowID.Type() == TypeNull } // A WritableVTable is a [VTable] that supports modifications. type WritableVTable interface { VTable Update(params VTableUpdateParams) (rowID int64, err error) DeleteRow(rowID Value) error } // A TransactionVTable is a [VTable] that supports transactions. type TransactionVTable interface { VTable // Begin begins a transaction on a virtual table. // Virtual table transactions do not nest, // so the Begin method will not be invoked more than once // on a single virtual table // without an intervening call to either Commit or Rollback. Begin() error // Sync signals the start of a two-phase commit on a virtual table. // This method is only invoked after a call to the Begin method // and prior to a Commit or Rollback. Sync() error // Commit causes a virtual table transaction to commit. Commit() error // Rollback causes a virtual table transaction to rollback. Rollback() error } // A SavepointVTable is a [VTable] that supports savepoints. type SavepointVTable interface { TransactionVTable // Savepoint signals that the virtual table // should save its current state as savepoint N. Savepoint(n int) error // Release invalidates all savepoints greater than or equal to n. Release(n int) error // RollbackTo signals that the state of the virtual table // should return to what it was when Savepoint(n) was last called. // This invalidates all savepoints greater than n. RollbackTo(n int) error } // A RenameVTable is a [VTable] that supports its non-eponymous form being renamed. type RenameVTable interface { VTable Rename(new string) error } // IndexInputs is the set of arguments that the SQLite core passes to // the [VTable] BestIndex function. type IndexInputs struct { // Constraints corresponds to the WHERE clause. Constraints []IndexConstraint // OrderBy corresponds to the ORDER BY clause. OrderBy []IndexOrderBy // ColumnsUsed is a bitmask of columns used by the statement. ColumnsUsed uint64 } func newIndexInputs(tls *libc.TLS, infoPtr uintptr) *IndexInputs { info := (*lib.Sqlite3_index_info)(unsafe.Pointer(infoPtr)) inputs := &IndexInputs{ Constraints: make([]IndexConstraint, info.FnConstraint), OrderBy: make([]IndexOrderBy, info.FnOrderBy), ColumnsUsed: info.FcolUsed, } ppVal := lib.Xsqlite3_malloc(tls, int32(unsafe.Sizeof(uintptr(0)))) if ppVal != 0 { defer lib.Xsqlite3_free(tls, ppVal) } for i := range inputs.Constraints { inputs.Constraints[i].copyFromC(tls, infoPtr, int32(i), ppVal) } aOrderBy := info.FaOrderBy for i := range inputs.OrderBy { o := (*lib.Sqlite3_index_orderby)(unsafe.Pointer(aOrderBy)) inputs.OrderBy[i] = IndexOrderBy{ Column: int(o.FiColumn), Desc: o.Fdesc != 0, } aOrderBy += unsafe.Sizeof(lib.Sqlite3_index_orderby{}) } return inputs } // IndexOrderBy is a term in the ORDER BY clause. type IndexOrderBy struct { // Column is column index. // Column indices start at 0. Column int // Desc is true if descending or false if ascending. Desc bool } // IndexOutputs is the information that the [VTable] BestIndex function // returns to the SQLite core. type IndexOutputs struct { // ConstraintUsage is a mapping from [IndexInputs] Constraints // to [VTableCursor] Filter arguments. // The mapping is in the same order as [IndexInputs] Constraints // and must not contain more than len(IndexInputs.Constraints) elements. // If len(ConstraintUsage) < len(IndexInputs.Constraints), // then ConstraintUsage is treated as if the missing elements have the zero value. ConstraintUsage []IndexConstraintUsage // ID is used to identify the index in [VTableCursor] Filter. ID IndexID // OrderByConsumed is true if the output is already ordered. OrderByConsumed bool // EstimatedCost is an estimate of the cost of a particular strategy. // A cost of N indicates that the cost of the strategy // is similar to a linear scan of an SQLite table with N rows. // A cost of log(N) indicates that the expense of the operation // is similar to that of a binary search on a unique indexed field // of an SQLite table with N rows. // A negative or zero cost uses a large default cost unless UseZeroEstimates is true. EstimatedCost float64 // EstimatedRows is an estimate of the number of rows // that will be returned by the strategy. // A negative or zero estimate uses 25 unless UseZeroEstimates is true. EstimatedRows int64 // If UseZeroEstimates is true and EstimatedCost or EstimatedRows is zero, // then the zeroes will be used instead of being interpreted as defaults. UseZeroEstimates bool // IndexFlags is a bitmask of other flags about the index. IndexFlags IndexFlags } func (outputs *IndexOutputs) copyToC(tls *libc.TLS, infoPtr uintptr) error { info := (*lib.Sqlite3_index_info)(unsafe.Pointer(infoPtr)) aConstraintUsage := info.FaConstraintUsage for _, u := range outputs.ConstraintUsage { ptr := (*lib.Sqlite3_index_constraint_usage)(unsafe.Pointer(aConstraintUsage)) ptr.FargvIndex = int32(u.ArgvIndex) if u.Omit { ptr.Fomit = 1 } else { ptr.Fomit = 0 } aConstraintUsage += unsafe.Sizeof(lib.Sqlite3_index_constraint_usage{}) } info.FidxNum = outputs.ID.Num if len(outputs.ID.String) == 0 { info.FidxStr = 0 info.FneedToFreeIdxStr = 0 } else { var err error info.FidxStr, err = sqliteCString(tls, outputs.ID.String) if err != nil { return err } info.FneedToFreeIdxStr = 1 } if outputs.OrderByConsumed { info.ForderByConsumed = 1 } else { info.ForderByConsumed = 0 } if outputs.EstimatedCost > 0 || outputs.UseZeroEstimates { info.FestimatedCost = outputs.EstimatedCost } if outputs.EstimatedRows > 0 || outputs.UseZeroEstimates { info.FestimatedRows = outputs.EstimatedRows } info.FidxFlags = int32(outputs.IndexFlags) return nil } // IndexConstraintUsage maps a single constraint from [IndexInputs] Constraints // to a [VTableCursor] Filter argument in the [IndexOutputs] ConstraintUsage list. type IndexConstraintUsage struct { // ArgvIndex is the intended [VTableCursor] Filter argument index plus one. // If ArgvIndex is zero or negative, // then the constraint is not passed to Filter. // Within the [IndexOutputs] ConstraintUsage list, // there must be exactly one entry with an ArgvIndex of 1, // another of 2, another of 3, and so forth // to as many or as few as the [VTable] BestIndex method wants. ArgvIndex int // If Omit is true, then it is a hint to SQLite // that the virtual table will guarantee that the constraint will always be satisfied. // SQLite will always double-check that rows satisfy the constraint if Omit is false, // but may skip this check if Omit is true. Omit bool } // IndexID is a virtual table index identifier. // The meaning of its fields is defined by the virtual table implementation. // String cannot contain NUL bytes. type IndexID struct { Num int32 String string } // IndexFlags is a bitmap of options returned in [IndexOutputs.IndexFlags]. type IndexFlags uint32 const ( // IndexScanUnique indicates that the virtual table // will only return zero or one rows given the input constraints. IndexScanUnique IndexFlags = lib.SQLITE_INDEX_SCAN_UNIQUE ) // VTableCursor is a cursor over a [VTable] used to loop through the table. type VTableCursor interface { // Filter begins a search of a virtual table. // The ID is one that is returned by [VTable] BestIndex. // The arguments will be populated as specified by ConstraintUsage in [IndexOutputs]. Filter(id IndexID, argv []Value) error // Next advances the cursor to the next row of a result set // initiated by a call to [VTableCursor] Filter. // If the cursor is already pointing at the last row when this routine is called, // then the cursor no longer points to valid data // and a subsequent call to the [VTableCursor] EOF method must return true. Next() error // Column returns the value for the i-th column of the current row. // Column indices start at 0. // // If noChange is true, then the column access is part of an UPDATE operation // during which the column value will not change. // This can be used as a hint to return [Unchanged] instead of fetching the value: // [WritableVTable] Update implementations can check [Value.NoChange] to test for this condition. Column(i int, noChange bool) (Value, error) // RowID returns the row ID of the row that the cursor is currently pointing at. RowID() (int64, error) // EOF reports if the cursor is not pointing to a valid row of data. EOF() bool // Close releases any resources associated with the cursor. Close() error } // SetModule registers or unregisters a virtual table module with the given name. func (c *Conn) SetModule(name string, module *Module) error { if c == nil { return fmt.Errorf("sqlite: set module %q: nil connection", name) } cname, err := libc.CString(name) if err != nil { return fmt.Errorf("sqlite: set module %q: %v", name, err) } defer libc.Xfree(c.tls, cname) if module == nil { res := ResultCode(lib.Xsqlite3_create_module_v2(c.tls, c.conn, cname, 0, 0, 0)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: set module %q: %w", name, err) } return nil } if module.Connect == nil { return fmt.Errorf("sqlite: set module %q: connect not provided", name) } cmod := lib.Xsqlite3_malloc(c.tls, int32(unsafe.Sizeof(lib.Sqlite3_module{}))) if cmod == 0 { return fmt.Errorf("sqlite: set module %q: %w", name, ResultNoMem.ToError()) } libc.Xmemset(c.tls, cmod, 0, types.Size_t(unsafe.Sizeof(lib.Sqlite3_module{}))) cmodPtr := (*lib.Sqlite3_module)(unsafe.Pointer(cmod)) cmodPtr.FiVersion = 3 cmodPtr.FxConnect = cFuncPointer(vtabConnectTrampoline) if module.Create != nil { cmodPtr.FxCreate = cFuncPointer(vtabCreateTrampoline) } else if module.UseConnectAsCreate { cmodPtr.FxCreate = cmodPtr.FxConnect } cmodPtr.FxBestIndex = cFuncPointer(vtabBestIndexTrampoline) cmodPtr.FxDisconnect = cFuncPointer(vtabDisconnect) cmodPtr.FxDestroy = cFuncPointer(vtabDestroy) cmodPtr.FxOpen = cFuncPointer(vtabOpenTrampoline) cmodPtr.FxClose = cFuncPointer(vtabCloseTrampoline) cmodPtr.FxFilter = cFuncPointer(vtabFilterTrampoline) cmodPtr.FxNext = cFuncPointer(vtabNextTrampoline) cmodPtr.FxEof = cFuncPointer(vtabEOFTrampoline) cmodPtr.FxColumn = cFuncPointer(vtabColumnTrampoline) cmodPtr.FxRowid = cFuncPointer(vtabRowIDTrampoline) cmodPtr.FxUpdate = cFuncPointer(vtabUpdateTrampoline) cmodPtr.FxBegin = cFuncPointer(vtabBeginTrampoline) cmodPtr.FxSync = cFuncPointer(vtabSyncTrampoline) cmodPtr.FxCommit = cFuncPointer(vtabCommitTrampoline) cmodPtr.FxRollback = cFuncPointer(vtabRollbackTrampoline) cmodPtr.FxRename = cFuncPointer(vtabRenameTrampoline) cmodPtr.FxSavepoint = cFuncPointer(vtabSavepointTrampoline) cmodPtr.FxRelease = cFuncPointer(vtabReleaseTrampoline) cmodPtr.FxRollbackTo = cFuncPointer(vtabRollbackToTrampoline) xDestroy := cFuncPointer(destroyModule) xmodules.mu.Lock() defensiveCopy := new(Module) *defensiveCopy = *module // Module pointer address is unique for lifetime of module. xmodules.m[cmod] = defensiveCopy xmodules.mu.Unlock() res := ResultCode(lib.Xsqlite3_create_module_v2(c.tls, c.conn, cname, cmod, cmod, xDestroy)) if err := res.ToError(); err != nil { return fmt.Errorf("sqlite: set module %q: %w", name, err) } return nil } func vtabCreateTrampoline(tls *libc.TLS, db uintptr, pAux uintptr, argc int32, argv uintptr, ppVTab uintptr, pzErr uintptr) int32 { xmodules.mu.RLock() module := xmodules.m[pAux] xmodules.mu.RUnlock() return callConnectFunc(tls, module.Create, db, argc, argv, ppVTab, pzErr) } func vtabConnectTrampoline(tls *libc.TLS, db uintptr, pAux uintptr, argc int32, argv uintptr, ppVTab uintptr, pzErr uintptr) int32 { xmodules.mu.RLock() module := xmodules.m[pAux] xmodules.mu.RUnlock() return callConnectFunc(tls, module.Connect, db, argc, argv, ppVTab, pzErr) } func callConnectFunc(tls *libc.TLS, connect VTableConnectFunc, db uintptr, argc int32, argv uintptr, ppVTab uintptr, pzErr uintptr) (retcode int32) { allConns.mu.RLock() c := allConns.table[db] allConns.mu.RUnlock() options := new(VTableConnectOptions) if argc > 0 { options.ModuleName = libc.GoString(*(*uintptr)(unsafe.Pointer(argv))) argc-- argv += uintptr(ptrSize) } if argc > 0 { options.DatabaseName = libc.GoString(*(*uintptr)(unsafe.Pointer(argv))) argc-- argv += uintptr(ptrSize) } if argc > 0 { options.VTableName = libc.GoString(*(*uintptr)(unsafe.Pointer(argv))) argc-- argv += uintptr(ptrSize) } if argc > 0 { options.Args = make([]string, argc) for i := range options.Args { options.Args[i] = libc.GoString(*(*uintptr)(unsafe.Pointer(argv))) argv += uintptr(ptrSize) } } vtab, cfg, err := connect(c, options) if err != nil { zerr, _ := sqliteCString(tls, err.Error()) *(*uintptr)(unsafe.Pointer(pzErr)) = zerr return int32(ErrCode(err)) } defer func() { if retcode != lib.SQLITE_OK { vtab.Disconnect() } }() // Call vtab configuration functions based on result. cdecl, err := libc.CString(cfg.Declaration) if err != nil { return lib.SQLITE_NOMEM } defer libc.Xfree(tls, cdecl) if res := ResultCode(lib.Xsqlite3_declare_vtab(tls, db, cdecl)); !res.IsSuccess() { return int32(res) } if !cfg.AllowIndirect { lib.Xsqlite3_vtab_config(tls, db, lib.SQLITE_VTAB_DIRECTONLY, 0) } if cfg.ConstraintSupport { vargs := libc.NewVaList(int32(1)) lib.Xsqlite3_vtab_config(tls, db, lib.SQLITE_VTAB_DIRECTONLY, vargs) libc.Xfree(tls, vargs) } vtabWrapperSize := int32(unsafe.Sizeof(vtabWrapper{})) pvtab := lib.Xsqlite3_malloc(tls, vtabWrapperSize) *(*uintptr)(unsafe.Pointer(ppVTab)) = pvtab if pvtab == 0 { return lib.SQLITE_NOMEM } libc.Xmemset(tls, pvtab, 0, types.Size_t(vtabWrapperSize)) avt := assertVTable(vtab) xvtables.mu.Lock() id := xvtables.ids.next() xvtables.m[id] = avt xvtables.mu.Unlock() (*vtabWrapper)(unsafe.Pointer(pvtab)).id = id return lib.SQLITE_OK } func vtabDisconnect(tls *libc.TLS, pVTab uintptr) int32 { id := (*vtabWrapper)(unsafe.Pointer(pVTab)).id lib.Xsqlite3_free(tls, pVTab) xvtables.mu.Lock() xvtables.ids.reclaim(id) vtab := xvtables.m[id] delete(xvtables.m, id) xvtables.mu.Unlock() return int32(ErrCode(vtab.Disconnect())) } func vtabDestroy(tls *libc.TLS, pVTab uintptr) int32 { id := (*vtabWrapper)(unsafe.Pointer(pVTab)).id lib.Xsqlite3_free(tls, pVTab) xvtables.mu.Lock() xvtables.ids.reclaim(id) vtab := xvtables.m[id] delete(xvtables.m, id) xvtables.mu.Unlock() return int32(ErrCode(vtab.Destroy())) } func vtabBestIndexTrampoline(tls *libc.TLS, pVTab uintptr, infoPtr uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) info := (*lib.Sqlite3_index_info)(unsafe.Pointer(infoPtr)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() outputs, err := vtab.BestIndex(newIndexInputs(tls, infoPtr)) if err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } if len(outputs.ConstraintUsage) > int(info.FnConstraint) { vw.setErrorMessage(tls, fmt.Sprintf("len(ConstraintUsage) = %d (> %d)", len(outputs.ConstraintUsage), info.FnConstraint)) return int32(ResultMisuse) } if err := outputs.copyToC(tls, infoPtr); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } func vtabOpenTrampoline(tls *libc.TLS, pVTab uintptr, ppCursor uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) vtabID := vw.id xvtables.mu.RLock() vtab := xvtables.m[vtabID] xvtables.mu.RUnlock() cursor, err := vtab.Open() if err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } cursorWrapperSize := int32(unsafe.Sizeof(vtabWrapper{})) pcursor := lib.Xsqlite3_malloc(tls, cursorWrapperSize) *(*uintptr)(unsafe.Pointer(ppCursor)) = pcursor if pcursor == 0 { cursor.Close() vw.setErrorMessage(tls, "no memory for cursor wrapper") return lib.SQLITE_NOMEM } libc.Xmemset(tls, pcursor, 0, types.Size_t(cursorWrapperSize)) xcursors.mu.Lock() cursorID := xcursors.ids.next() xcursors.m[cursorID] = cursor xcursors.mu.Unlock() (*cursorWrapper)(unsafe.Pointer(pcursor)).id = cursorID return lib.SQLITE_OK } func vtabCloseTrampoline(tls *libc.TLS, pCursor uintptr) int32 { id := (*cursorWrapper)(unsafe.Pointer(pCursor)).id pVTab := (*cursorWrapper)(unsafe.Pointer(pCursor)).base.FpVtab xcursors.mu.Lock() cur := xcursors.m[id] delete(xcursors.m, id) xcursors.ids.reclaim(id) xcursors.mu.Unlock() lib.Xsqlite3_free(tls, pCursor) if err := cur.Close(); err != nil { (*vtabWrapper)(unsafe.Pointer(pVTab)).setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } func vtabFilterTrampoline(tls *libc.TLS, pCursor uintptr, idxNum int32, idxStr uintptr, argc int32, argv uintptr) int32 { cw := (*cursorWrapper)(unsafe.Pointer(pCursor)) xcursors.mu.RLock() cur := xcursors.m[cw.id] xcursors.mu.RUnlock() idxID := IndexID{ Num: idxNum, String: libc.GoString(idxStr), } goArgv := make([]Value, 0, int(argc)) for ; len(goArgv) < cap(goArgv); argv += uintptr(ptrSize) { goArgv = append(goArgv, Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(argv)), }) } if err := cur.Filter(idxID, goArgv); err != nil { cw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } func vtabNextTrampoline(tls *libc.TLS, pCursor uintptr) int32 { cw := (*cursorWrapper)(unsafe.Pointer(pCursor)) xcursors.mu.RLock() cur := xcursors.m[cw.id] xcursors.mu.RUnlock() if err := cur.Next(); err != nil { cw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } func vtabEOFTrampoline(tls *libc.TLS, pCursor uintptr) int32 { id := (*cursorWrapper)(unsafe.Pointer(pCursor)).id xcursors.mu.RLock() cur := xcursors.m[id] xcursors.mu.RUnlock() if cur.EOF() { return 1 } return 0 } func vtabColumnTrampoline(tls *libc.TLS, pCursor uintptr, ctx uintptr, n int32) int32 { id := (*cursorWrapper)(unsafe.Pointer(pCursor)).id xcursors.mu.RLock() cur := xcursors.m[id] xcursors.mu.RUnlock() goCtx := Context{tls: tls, ptr: ctx} noChange := lib.Xsqlite3_vtab_nochange(tls, ctx) != 0 v, err := cur.Column(int(n), noChange) if err != nil { goCtx.result(TextValue(err.Error()), nil) return int32(ErrCode(err)) } if noChange && v.tls == nil && v.NoChange() { // Skip calling a result function if the method returns Unchanged. return lib.SQLITE_OK } goCtx.result(v, nil) return lib.SQLITE_OK } func vtabRowIDTrampoline(tls *libc.TLS, pCursor uintptr, pRowid uintptr) int32 { cw := (*cursorWrapper)(unsafe.Pointer(pCursor)) xcursors.mu.RLock() cur := xcursors.m[cw.id] xcursors.mu.RUnlock() rowID, err := cur.RowID() if err != nil { cw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } *(*int64)(unsafe.Pointer(pRowid)) = rowID return lib.SQLITE_OK } func vtabUpdateTrampoline(tls *libc.TLS, pVTab uintptr, argc int32, argv uintptr, pRowid uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Write == nil { vw.setErrorMessage(tls, fmt.Sprintf("%T does not implement WritableVTable", vtab.VTable)) return lib.SQLITE_READONLY } if argc < 1 { panic("SQLite did not give enough arguments to xUpdate") } oldRowID := Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(argv)), } if argc == 1 { if err := vtab.Write.DeleteRow(oldRowID); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } goArgs := VTableUpdateParams{ OldRowID: oldRowID, } argv += unsafe.Sizeof(uintptr(0)) goArgs.NewRowID = Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(argv)), } if argc > 2 { goArgs.Columns = make([]Value, argc-2) argv += unsafe.Sizeof(uintptr(0)) for i := range goArgs.Columns { goArgs.Columns[i] = Value{ tls: tls, ptrOrType: *(*uintptr)(unsafe.Pointer(argv)), } argv += unsafe.Sizeof(uintptr(0)) } } insertRowID, err := vtab.Write.Update(goArgs) if err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } *(*int64)(unsafe.Pointer(pRowid)) = insertRowID return lib.SQLITE_OK } func vtabBeginTrampoline(tls *libc.TLS, pVTab uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Transaction != nil { if err := vtab.Transaction.Begin(); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabSyncTrampoline(tls *libc.TLS, pVTab uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Transaction != nil { if err := vtab.Transaction.Sync(); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabCommitTrampoline(tls *libc.TLS, pVTab uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Transaction != nil { if err := vtab.Transaction.Commit(); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabRollbackTrampoline(tls *libc.TLS, pVTab uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Transaction != nil { if err := vtab.Transaction.Rollback(); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabRenameTrampoline(tls *libc.TLS, pVTab uintptr, zNew uintptr) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Rename == nil { vw.setErrorMessage(tls, fmt.Sprintf("no Rename method for %T", vtab.VTable)) return lib.SQLITE_READONLY } if err := vtab.Rename.Rename(libc.GoString(zNew)); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } return lib.SQLITE_OK } func vtabSavepointTrampoline(tls *libc.TLS, pVTab uintptr, n int32) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Savepoint != nil { if err := vtab.Savepoint.Savepoint(int(n)); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabReleaseTrampoline(tls *libc.TLS, pVTab uintptr, n int32) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Savepoint != nil { if err := vtab.Savepoint.Release(int(n)); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func vtabRollbackToTrampoline(tls *libc.TLS, pVTab uintptr, n int32) int32 { vw := (*vtabWrapper)(unsafe.Pointer(pVTab)) xvtables.mu.RLock() vtab := xvtables.m[vw.id] xvtables.mu.RUnlock() if vtab.Savepoint != nil { if err := vtab.Savepoint.RollbackTo(int(n)); err != nil { vw.setErrorMessage(tls, err.Error()) return int32(ErrCode(err)) } } return lib.SQLITE_OK } func destroyModule(tls *libc.TLS, pAux uintptr) { xmodules.mu.Lock() delete(xmodules.m, pAux) xmodules.mu.Unlock() lib.Xsqlite3_free(tls, pAux) } type vtabWrapper struct { base lib.Sqlite3_vtab id uintptr } func (vw *vtabWrapper) setErrorMessage(tls *libc.TLS, s string) { if vw.base.FzErrMsg != 0 { lib.Xsqlite3_free(tls, vw.base.FzErrMsg) } vw.base.FzErrMsg, _ = sqliteCString(tls, s) } type cursorWrapper struct { base lib.Sqlite3_vtab_cursor id uintptr } func (cw *cursorWrapper) setErrorMessage(tls *libc.TLS, s string) { vw := (*vtabWrapper)(unsafe.Pointer(cw.base.FpVtab)) vw.setErrorMessage(tls, s) } type assertedVTable struct { VTable Write WritableVTable Transaction TransactionVTable Savepoint SavepointVTable Rename RenameVTable } func assertVTable(vtab VTable) assertedVTable { avt := assertedVTable{VTable: vtab} avt.Write, _ = vtab.(WritableVTable) avt.Transaction, _ = vtab.(TransactionVTable) avt.Savepoint, _ = vtab.(SavepointVTable) avt.Rename, _ = vtab.(RenameVTable) return avt } var ( xmodules = struct { mu sync.RWMutex m map[uintptr]*Module }{ m: make(map[uintptr]*Module), } xvtables = struct { mu sync.RWMutex m map[uintptr]assertedVTable ids idGen }{ m: make(map[uintptr]assertedVTable), } xcursors = struct { mu sync.RWMutex m map[uintptr]VTableCursor ids idGen }{ m: make(map[uintptr]VTableCursor), } ) // sqliteCString copies a Go string to SQLite-allocated memory. func sqliteCString(tls *libc.TLS, s string) (uintptr, error) { if strings.Contains(s, "\x00") { return 0, fmt.Errorf("%q contains NUL bytes", s) } csize := len(s) + 1 c := lib.Xsqlite3_malloc(tls, int32(csize)) if c == 0 { return 0, fmt.Errorf("%w: cannot allocate %d bytes", ResultNoMem.ToError(), len(s)) } cslice := unsafe.Slice((*byte)(unsafe.Pointer(c)), csize) copy(cslice, s) cslice[len(s)] = 0 return c, nil } zombiezen-go-sqlite-297af96/vtable_example_test.go000066400000000000000000000044631501417116500223550ustar00rootroot00000000000000// Copyright 2023 Roxy Light // SPDX-License-Identifier: ISC package sqlite_test import ( "fmt" "log" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) func ExampleVTable() { conn, err := sqlite.OpenConn(":memory:") if err != nil { log.Fatal(err) } defer conn.Close() err = conn.SetModule("templatevtab", &sqlite.Module{ Connect: templatevtabConnect, }) if err != nil { log.Fatal(err) } err = sqlitex.ExecuteTransient( conn, `SELECT a, b FROM templatevtab ORDER BY rowid;`, &sqlitex.ExecOptions{ ResultFunc: func(stmt *sqlite.Stmt) error { fmt.Printf("%4d, %4d\n", stmt.ColumnInt(0), stmt.ColumnInt(1)) return nil }, }, ) if err != nil { log.Fatal(err) } // Output: // 1001, 2001 // 1002, 2002 // 1003, 2003 // 1004, 2004 // 1005, 2005 // 1006, 2006 // 1007, 2007 // 1008, 2008 // 1009, 2009 // 1010, 2010 } type templatevtab struct{} const ( templatevarColumnA = iota templatevarColumnB ) func templatevtabConnect(c *sqlite.Conn, opts *sqlite.VTableConnectOptions) (sqlite.VTable, *sqlite.VTableConfig, error) { vtab := new(templatevtab) cfg := &sqlite.VTableConfig{ Declaration: "CREATE TABLE x(a,b)", } return vtab, cfg, nil } func (vt *templatevtab) BestIndex(*sqlite.IndexInputs) (*sqlite.IndexOutputs, error) { return &sqlite.IndexOutputs{ EstimatedCost: 10, EstimatedRows: 10, }, nil } func (vt *templatevtab) Open() (sqlite.VTableCursor, error) { return &templatevtabCursor{rowid: 1}, nil } func (vt *templatevtab) Disconnect() error { return nil } func (vt *templatevtab) Destroy() error { return nil } type templatevtabCursor struct { rowid int64 } func (cur *templatevtabCursor) Filter(id sqlite.IndexID, argv []sqlite.Value) error { cur.rowid = 1 return nil } func (cur *templatevtabCursor) Next() error { cur.rowid++ return nil } func (cur *templatevtabCursor) Column(i int, noChange bool) (sqlite.Value, error) { switch i { case templatevarColumnA: return sqlite.IntegerValue(1000 + cur.rowid), nil case templatevarColumnB: return sqlite.IntegerValue(2000 + cur.rowid), nil default: panic("unreachable") } } func (cur *templatevtabCursor) RowID() (int64, error) { return cur.rowid, nil } func (cur *templatevtabCursor) EOF() bool { return cur.rowid > 10 } func (cur *templatevtabCursor) Close() error { return nil }