libsql-sequel 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +28 -0
- data/README.md +144 -0
- data/docs/architecture.md +43 -0
- data/docs/compatibility.md +38 -0
- data/docs/development.md +61 -0
- data/docs/testing-review.md +46 -0
- data/examples/local.rb +19 -0
- data/lib/libsql-sequel.rb +5 -0
- data/lib/sequel/adapters/libsql.rb +178 -0
- data/lib/sequel/adapters/turso.rb +6 -0
- data/lib/sequel/libsql/configuration.rb +106 -0
- data/lib/sequel/libsql/connection.rb +165 -0
- data/lib/sequel/libsql/dataset.rb +58 -0
- data/lib/sequel/libsql/native.rb +115 -0
- data/lib/sequel/libsql/result_types.rb +91 -0
- data/lib/sequel/libsql/type_converter.rb +53 -0
- data/lib/sequel/libsql/version.rb +7 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 37c836e635f6104c6972774e8cb3dba6397cfbf5b6c958c19f15eaaca4fc1367
|
|
4
|
+
data.tar.gz: adce3fe4850eab4d30b4b303f404520f969390060dded1b89def96675d2f7249
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d02c3ee895aa85b2e4bae54743d0fc8b8b0028a8170b9b87212caeab594798b080c52dba582b2fd882a19e23a3aea5989ba5f0b53dc33ba68f7c924cb544b606
|
|
7
|
+
data.tar.gz: 8c777ebc4996b0d96a5e4fd437337347568820f5a8a3adc2569ce69ad603f6b133acb386e6f8c2e59a641789b62f7c8aef780e66234908ba05e28e394276df37
|
data/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francesco Canessa
|
|
4
|
+
|
|
5
|
+
Adapter conventions and portions adapted from Sequel:
|
|
6
|
+
Copyright (c) 2007-2008 Sharon Rosner
|
|
7
|
+
Copyright (c) 2008-2026 Jeremy Evans and contributors
|
|
8
|
+
|
|
9
|
+
Native API integration informed by libsql-ruby:
|
|
10
|
+
Copyright (c) 2024 Turso (libSQL)
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to
|
|
14
|
+
deal in the Software without restriction, including without limitation the
|
|
15
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
16
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
25
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
27
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
28
|
+
IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# libsql-sequel
|
|
2
|
+
|
|
3
|
+
A modular Ruby adapter for using [Sequel](https://github.com/jeremyevans/sequel) with local libSQL databases, remote Turso databases, and embedded replicas through [libsql-ruby](https://github.com/tursodatabase/libsql-ruby).
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'libsql-sequel'
|
|
7
|
+
|
|
8
|
+
DB = Sequel.connect(
|
|
9
|
+
adapter: :libsql,
|
|
10
|
+
database: ENV.fetch('TURSO_DATABASE_URL'),
|
|
11
|
+
auth_token: ENV.fetch('TURSO_AUTH_TOKEN')
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DB.create_table?(:items) do
|
|
15
|
+
primary_key :id
|
|
16
|
+
String :name, null: false
|
|
17
|
+
Integer :quantity, default: 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
id = DB[:items].insert(name: 'Hello, Turso', quantity: 3)
|
|
21
|
+
DB[:items].where(id: id).first
|
|
22
|
+
# => {id: 1, name: "Hello, Turso", quantity: 3}
|
|
23
|
+
|
|
24
|
+
DB.disconnect
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Install from [RubyGems](https://rubygems.org/gems/libsql-sequel):
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# Gemfile
|
|
33
|
+
gem 'libsql-sequel', '~> 0.3.0'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then run `bundle install`. To install without Bundler:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
gem install libsql-sequel -v 0.3.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Ruby 3.3+ and Sequel 5.80–5.x are required. The native dependency is **pinned to `turso_libsql` 0.2.0**: 0.2.1 contains native debug output of connection credentials and SQL. Do not widen that dependency without reviewing the replacement library. The adapter uses the bundled native library with scoped corrections for the driver's Ruby wrapper; it does not patch global `Libsql` classes or require `sqlite3` at runtime.
|
|
43
|
+
|
|
44
|
+
The driver bundles Apple Silicon macOS and glibc Linux x86_64/aarch64 libraries. Intel macOS, Windows, musl Linux, and JRuby are not supported by this pinned distribution. Current verification was on Apple Silicon macOS / Ruby 4.0.5. See [compatibility and verification](docs/compatibility.md).
|
|
45
|
+
|
|
46
|
+
## Connection modes
|
|
47
|
+
|
|
48
|
+
Turso Cloud's `turso://` URLs are also accepted, both as `database:`/`url:`
|
|
49
|
+
options and directly through `Sequel.connect('turso://...', auth_token: ...)`.
|
|
50
|
+
They are normalized to `https://` for the pinned native driver before opening
|
|
51
|
+
a connection. The same credential/query/fragment validation applies. A
|
|
52
|
+
`turso://` database URL is never treated as a local file path.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Memory: a single pooled connection.
|
|
56
|
+
db = Sequel.connect(adapter: :libsql)
|
|
57
|
+
|
|
58
|
+
# Local file: regular Sequel pooling (default four connections).
|
|
59
|
+
db = Sequel.connect(adapter: :libsql, database: 'app.db', max_connections: 4)
|
|
60
|
+
|
|
61
|
+
# Native libsql:// URLs work directly. https:// and http:// use the hash form.
|
|
62
|
+
db = Sequel.connect(ENV.fetch('TURSO_DATABASE_URL'),
|
|
63
|
+
auth_token: ENV.fetch('TURSO_AUTH_TOKEN'))
|
|
64
|
+
|
|
65
|
+
# Embedded replica: local file plus remote primary URL.
|
|
66
|
+
db = Sequel.connect(
|
|
67
|
+
adapter: :libsql,
|
|
68
|
+
database: ENV.fetch('TURSO_DATABASE_URL'),
|
|
69
|
+
path: 'replica.db',
|
|
70
|
+
auth_token: ENV.fetch('TURSO_AUTH_TOKEN'),
|
|
71
|
+
sync_interval: 1_000,
|
|
72
|
+
read_your_writes: true
|
|
73
|
+
)
|
|
74
|
+
db.sync # => {frame_no: ..., frames_synced: ...}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`url:` can also specify the remote URL; `database:` or `path:` then names the local replica. Memory databases and replicas require `max_connections: 1`. This avoids separate memory databases and multiple replication workers targeting one file. Replica sync cannot run inside a transaction. Embedded replication is implemented but has not been tested against a live Turso primary.
|
|
78
|
+
|
|
79
|
+
Keep tokens in the `auth_token:` option rather than the URL. URLs with credentials, query parameters, or fragments are rejected.
|
|
80
|
+
|
|
81
|
+
For Puma and other forking servers, initialize native connections inside each worker after forking. If the parent has opened connections, call `DB.disconnect` in `before_fork` to release them in their owning process. Calling `DB.disconnect` in a child quietly discards inherited handles without invoking native destructors; inherited queries still raise `Sequel::Error`. Disconnecting does not reset an already initialized Rust runtime, so child cleanup alone does not make preloading the native runtime safe.
|
|
82
|
+
|
|
83
|
+
Local connections enable foreign keys, case-sensitive `LIKE`, and a 5,000 ms SQLite busy timeout by default. `timeout:`, `case_sensitive_like:`, `auto_vacuum:`, `synchronous:`, and `temp_store:` are local-only options. The native remote parser rejects those PRAGMAs; remote connections keep the server's `LIKE` behavior. `timeout:` is a SQLite lock timeout, not an HTTP timeout. `readonly:`, custom regexp functions, `disable_dqs:`, offline-write mode, and custom TLS options are unsupported and rejected.
|
|
84
|
+
|
|
85
|
+
## Sequel features
|
|
86
|
+
|
|
87
|
+
SQL generation, schema introspection, migrations, models, associations, upserts, and `RETURNING` reuse Sequel's shared SQLite implementation. The adapter scheme is `:libsql`; `database_type` is `:sqlite`.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
DB.transaction do
|
|
91
|
+
DB[:items].insert(name: 'committed')
|
|
92
|
+
DB.transaction(savepoint: true) do
|
|
93
|
+
DB[:items].insert(name: 'rolled back')
|
|
94
|
+
raise Sequel::Rollback
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
find_item = DB[:items].where(id: :$id).prepare(:first, :find_item)
|
|
99
|
+
find_item.call(id: 1)
|
|
100
|
+
DB.call(:find_item, id: 1)
|
|
101
|
+
|
|
102
|
+
DB[:items].where(id: :$id).call(:update, {id: 1, quantity: 5}, quantity: :$quantity)
|
|
103
|
+
DB[:items].returning(:id).insert(name: 'returned')
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Bound values use native parameters. Named Sequel prepared queries prepare a native statement for each execution and always release it; there is no persistent statement cache. `DB.run` supports SQL batches. Commit and rollback run as SQL so errors propagate to Sequel instead of using the driver's abort-prone native transaction helpers. There are no automatic retries of failed writes.
|
|
107
|
+
|
|
108
|
+
## Values and result types
|
|
109
|
+
|
|
110
|
+
Integers preserve signed 64-bit values; strings are converted to UTF-8; blobs preserve all bytes. Use `Sequel.blob(bytes)` for binary data. Invalid text encodings and strings that cannot be converted to UTF-8 raise `Sequel::Error`. NUL-containing text is rejected on input because the native C API truncates it. The same upstream limitation affects existing TEXT values containing NUL when reading them; store those values as blobs. Out-of-range bound integers, invalid text, and non-finite bound floats are rejected.
|
|
111
|
+
|
|
112
|
+
The native API does not expose declared result-column types. Simple single-table datasets infer date, datetime, time, decimal, and boolean conversions from Sequel's schema cache, including direct column aliases and prepared queries. Raw SQL, joins, CTEs, compounds, and arbitrary expressions retain native SQLite values. Use explicit hints when necessary:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
DB.fetch('SELECT active, created_at FROM items').with_libsql_types(
|
|
116
|
+
active: :boolean,
|
|
117
|
+
created_at: :datetime
|
|
118
|
+
).all
|
|
119
|
+
|
|
120
|
+
# Supported hints: :boolean, :date, :datetime, :timestamp, :time, :decimal,
|
|
121
|
+
# :integer, :float, :blob, or a callable. NULL always remains nil.
|
|
122
|
+
DB.fetch('SELECT upper(name) AS label FROM items').with_libsql_types(
|
|
123
|
+
label: ->(value) { value.freeze }
|
|
124
|
+
).all
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use `Sequel::Model` normally for single-table model datasets. Joined/eager-graph results with boolean or temporal columns may need explicit result type hints. Expression aliases never acquire a type merely because they match a table column name. SQLite's own numeric affinity and decimal precision limitations still apply.
|
|
128
|
+
|
|
129
|
+
## Development and tests
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
bundle install
|
|
133
|
+
bundle exec rake check # syntax, visibility, and warning-enabled tests
|
|
134
|
+
bundle exec rake lint # syntax and method visibility only
|
|
135
|
+
bundle exec rake test # real native local/file databases
|
|
136
|
+
bundle exec rake upstream # upstream SQLite + integration specs
|
|
137
|
+
bundle exec rake remote_local # real native HTTP client, local Hrana server
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`upstream` and `remote_local` need the ignored reference checkouts; setup instructions are in [development.md](docs/development.md). `rake` defaults to `check`. Checks follow Sequel's own syntax/warnings/visibility approach; Sequel does not configure RuboCop or a complexity analyzer. See [the spec and code-quality review](docs/testing-review.md).
|
|
141
|
+
|
|
142
|
+
For an explicit test against a **disposable** hosted database, set `LIBSQL_TEST_URL` and `LIBSQL_TEST_AUTH_TOKEN` in your environment, then run `bundle exec rake remote`. It creates randomly named test tables and drops those tables afterward. Hosted authentication and HTTPS remote operations have been validated with a `turso://` URL; embedded replication remains unverified. See [compatibility and verification](docs/compatibility.md) for the tested scope.
|
|
143
|
+
|
|
144
|
+
See [architecture](docs/architecture.md), [compatibility](docs/compatibility.md), and [the local example](examples/local.rb).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
The adapter follows Sequel's small Database/Dataset adapter pattern and delegates SQL dialect behavior to `Sequel::SQLite::DatabaseMethods` and `DatasetMethods`. It does not subclass the sqlite3-backed adapter, avoiding a second native database driver in production.
|
|
4
|
+
|
|
5
|
+
| Component | Responsibility |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `lib/sequel/adapters/libsql.rb` | Register `:libsql`, create pooled connections, execute/log SQL, map errors, bind Ruby values, expose replica sync |
|
|
8
|
+
| `lib/sequel/libsql/configuration.rb` | Normalize memory/file/remote/replica options, reject unsupported or conflicting configuration |
|
|
9
|
+
| `lib/sequel/libsql/connection.rb` | Own database/connection handles; scope statements, results, and rows with `ensure` |
|
|
10
|
+
| `lib/sequel/libsql/native.rb` | Use the driver's native library/ABI; correct error, string, blob, and signed-integer handling |
|
|
11
|
+
| `lib/sequel/libsql/dataset.rb` | Map native rows to Sequel hashes, bind query parameters, and quote text |
|
|
12
|
+
| `lib/sequel/libsql/result_types.rb` | Resolve column origins and explicit type hints; reject ambiguous inference |
|
|
13
|
+
| `lib/sequel/libsql/type_converter.rb` | Convert storage values to semantic SQLite types |
|
|
14
|
+
|
|
15
|
+
## Ownership and concurrency
|
|
16
|
+
|
|
17
|
+
Each Sequel pool slot owns one native Database and Connection. A query owns its Statement, Result, and one Row at a time. Values are copied into Ruby before the row is freed. Native strings/blobs returned by the C API are copied and freed exactly once. Cleanup runs in reverse order on success, errors, early iteration exit, and connection setup failure. A result retained after its block raises rather than touching freed native memory.
|
|
18
|
+
|
|
19
|
+
Potentially blocking initialization, prepare, execute, query, row advance, and sync calls release the Ruby GVL. Sequel serializes access to a given connection; independent pool slots can run concurrently. Memory and embedded-replica pools are restricted to one slot. In the owning process, explicit `disconnect` closes both handles, is idempotent, and leaves Sequel able to reconnect lazily.
|
|
20
|
+
|
|
21
|
+
There are no finalizers: explicit ownership avoids GC freeing a native parent while a child is in use. Application code must disconnect databases it no longer needs. Connections detect queries from a different process and reject them. In a child, `disconnect` only clears inherited references, allowing Sequel to empty its pool without calling unsafe native destructors or affecting the parent's connections. Release parent connections in `before_fork`. The upstream Rust runtime must be initialized in each worker after fork; disconnecting does not reset it, and this is not a fork-safe driver.
|
|
22
|
+
|
|
23
|
+
## Transactions and prepared queries
|
|
24
|
+
|
|
25
|
+
Sequel reserves one pool connection for the transaction and issues SQL BEGIN, COMMIT, ROLLBACK, and SAVEPOINT commands. The driver's native Transaction object is deliberately unused because its commit/rollback C functions return void and unwrap errors. SQL errors instead pass through a typed `Native::Error`, then Sequel's error classification.
|
|
26
|
+
|
|
27
|
+
Sequel's unnumbered argument mapper generates `?` placeholders and orders/repeats values correctly. Both `.call` and named `.prepare` queries use native binding. Native statements are short-lived, so failed statements cannot leave stale bindings, and DDL never needs to clear a statement cache. This trades additional prepare work (including remote round trips) for predictable resource handling.
|
|
28
|
+
|
|
29
|
+
## Type information
|
|
30
|
+
|
|
31
|
+
The C API exposes runtime types and column names, but not column declarations/origins. General sqlite3-style automatic result conversion is therefore impossible without a new upstream API. This adapter resolves only known single-table column projections, consulting Sequel's schema cache after the SQL has successfully prepared. It leaves expressions and ambiguous projections unchanged and offers `with_libsql_types` for explicit conversions.
|
|
32
|
+
|
|
33
|
+
Sequel's cached placeholder literalizer optimization is disabled because it replaces projected columns with opaque arguments. Without native declared-type metadata, that made repeated `get` calls lose their conversions after the cache activated. Native bound variables and named prepared statements remain supported; the tradeoff is additional Ruby SQL generation work.
|
|
34
|
+
|
|
35
|
+
Numeric timestamps follow Sequel's SQLite convention: Integer means Unix seconds and Float means Julian day. Numeric TIME values preserve microseconds. BLOB columns containing integer/real/text storage values convert through their string representation, and nonnumeric text in a decimal column remains text, matching Sequel's SQLite adapter.
|
|
36
|
+
|
|
37
|
+
Sequel invalidates its own schema cache for schema DSL changes. External schema changes and arbitrary DDL via `run` follow Sequel's usual cache rules; reload the affected schema or reconnect the Database object when needed. No extra unbounded type cache is introduced.
|
|
38
|
+
|
|
39
|
+
## Driver boundary
|
|
40
|
+
|
|
41
|
+
The compatibility layer uses `turso_libsql`'s shipped structs and binary functions without altering `Libsql` or `CLibsql` globally. Error messages are copied before freeing their C allocation. Signed integers are recovered from the upstream unsigned declaration. Blob binding is checked before String binding, and text uses UTF-8 byte lengths with a retained input pointer. Slice release uses the correct ABI type. Descriptor strings remain retained for their native database lifetime.
|
|
42
|
+
|
|
43
|
+
The exact 0.2.0 dependency prevents accidental activation of the credential-logging 0.2.1 native release. A future driver upgrade needs local/HTTP contract tests plus native binary review, not just a version bump.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Compatibility and verification
|
|
2
|
+
|
|
3
|
+
## Verified in this workspace
|
|
4
|
+
|
|
5
|
+
As of 2026-10-31, Apple Silicon macOS, Ruby 4.0.5, `turso_libsql` 0.2.0, bundled SQLite 3.45.1:
|
|
6
|
+
|
|
7
|
+
- Native local/file tests cover CRUD, signed integer boundaries, UTF-8, arbitrary/empty blobs, invalid input, native lifetime cleanup, prepared queries, constraints, transactions/savepoints/commit failure, models, associations, migrations, schema alteration, pool concurrency, reconnect, and type hints.
|
|
8
|
+
- The same project suite passes with Sequel 5.80.0 and 5.109.0.
|
|
9
|
+
- Sequel upstream at `75b425dd1c1f34908426dd70371d14e4afb9c933`: SQLite adapter plus integration run passes (901 tests, six upstream skips). The test sources are unmodified. Some additional groups are never defined for an external adapter: seven SQLite type-conversion cases gated on `adapter_scheme == :sqlite` now have local equivalents. See [the coverage review](testing-review.md).
|
|
10
|
+
- Real native HTTP execution against the official Hrana v3 Python fixture covers prepared Unicode/blob values, affected counts, inserted IDs, constraints, transactions/savepoints, reconnect, schema metadata, empty results, NULL values, models using prepared statements, and INSERT/UPDATE/DELETE RETURNING.
|
|
11
|
+
- The fixture wrapper only normalizes padded/unpadded base64 between Python and Rust. SQL and stream handling stay upstream. This is a protocol integration test, not a hosted Turso test.
|
|
12
|
+
- Hosted Turso remote mode was verified with a `turso://` URL normalized to HTTPS and a real database token, using Sequel 5.109.0. The warning-enabled `test/remote.rb` suite passed all five tests (24 assertions), covering CRUD, prepared bindings, Unicode/blobs, constraints, transactions/savepoints/rollback, reconnect persistence, schema metadata, models, and returning writes. Only randomly named test tables were created and dropped; credentials were not logged.
|
|
13
|
+
|
|
14
|
+
## Not verified
|
|
15
|
+
|
|
16
|
+
Live embedded replication, sync scheduling/read-your-writes behavior against a primary, encryption, Linux native execution, and Ruby 3.3/3.4 execution have not been tested here. The connection descriptor and sync API support replicas; that does not establish end-to-end replication correctness. Hosted remote checks do not cover token expiry/rotation, custom TLS settings, or production load behavior.
|
|
17
|
+
|
|
18
|
+
## Known limits
|
|
19
|
+
|
|
20
|
+
- No declared result type metadata from the C API: raw SQL/joins/CTEs/expressions need explicit hints for semantic SQLite types.
|
|
21
|
+
- Existing TEXT values containing NUL are truncated by the upstream C API; the adapter rejects NUL text inputs. BLOB is binary-safe.
|
|
22
|
+
- Remote native SQL parsing rejects local PRAGMAs such as busy_timeout/case_sensitive_like. Remote LIKE behavior follows the server. Local-only options are rejected when a remote URL is configured.
|
|
23
|
+
- No custom regexp functions, read-only opening, loadable extensions, DQS configuration, offline-write sync mode, or configurable HTTP timeout.
|
|
24
|
+
- Memory/replica pools are limited to one connection. Disconnecting a memory database destroys it.
|
|
25
|
+
- Initialize in worker processes after fork; release any parent connections in `before_fork`. Child `disconnect` quietly discards inherited handles, while inherited queries remain unsupported. This cleanup does not reset an inherited native runtime or establish that reconnecting in the child is safe.
|
|
26
|
+
- No automatic retry or idempotency policy for transport failures. If a write response is lost, its outcome can be uncertain.
|
|
27
|
+
- `turso_libsql` 0.2.0 ships only Apple Silicon macOS and glibc Linux x86_64/aarch64 binaries. Other platforms need an independently validated driver build.
|
|
28
|
+
- This targets libSQL and Turso's libSQL-compatible service, not the separate newer `turso` Rust engine API.
|
|
29
|
+
|
|
30
|
+
## Upstream references
|
|
31
|
+
|
|
32
|
+
- [libsql-ruby](https://github.com/tursodatabase/libsql-ruby): FFI declarations, Ruby wrappers, examples, bundled libraries. Upstream main differs from published 0.2.0 and 0.2.1.
|
|
33
|
+
- [libsql-activerecord](https://github.com/tursodatabase/libsql-activerecord): ORM execution and schema comparison.
|
|
34
|
+
- [Sequel SQLite adapter](https://github.com/jeremyevans/sequel/blob/75b425dd1c1f34908426dd70371d14e4afb9c933/lib/sequel/adapters/sqlite.rb) and shared SQLite modules: integration conventions and dialect behavior.
|
|
35
|
+
- [sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby): comparison of value and declared-column metadata APIs.
|
|
36
|
+
- [libsql-c](https://github.com/tursodatabase/libsql-c): native ABI, ownership, descriptors, errors, and parameter conversion. Published 0.2.0-era revision: `cb3a8c2c2c7330e104e4df536189eddb83a8f491`.
|
|
37
|
+
- [libsql-client-ts](https://github.com/tursodatabase/libsql-client-ts/tree/9496c782af413f6380235c7f5d0c97b5371f49b7/testing/hrana-test-server): official protocol fixture.
|
|
38
|
+
- [hrana-client-ts](https://github.com/libsql/hrana-client-ts) and [libSQL](https://github.com/tursodatabase/libsql): HTTP streams, remote SQL behavior, and blob encoding.
|
data/docs/development.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
bundle install
|
|
5
|
+
bundle exec rake check
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Tests use real native libSQL databases and clean up temporary files. No network service or credentials are required for the default suite. `sqlite3` is a development dependency for upstream compatibility tests; it is not loaded by the adapter.
|
|
9
|
+
|
|
10
|
+
## Code checks
|
|
11
|
+
|
|
12
|
+
`rake` defaults to `check`: `lint` followed by the tests. As in Sequel, tests run with `-w` and, on Ruby 3.4+, `-W:strict_unused_block`. There is no RuboCop/Standard configuration or complexity analyzer in Sequel's repository.
|
|
13
|
+
|
|
14
|
+
- `bundle exec rake check_syntax` checks Ruby syntax and parser warnings in project library, test, task, and example files, plus Gemfile/Rakefile/gemspec. It fails on invalid syntax or warnings. Ignored clones, private notes, and installed gems are outside its file list.
|
|
15
|
+
- `bundle exec rake check_visibility` uses Jeremy Evans' `visibility_checker`, also used by Sequel's optional visibility checks. It checks the database, dataset, and prepared dataset; changes originating in the adapter fail the task. Preexisting visibility changes inside Sequel itself are excluded.
|
|
16
|
+
- `bundle exec rake lint` runs both checks.
|
|
17
|
+
- `.github/workflows/ci.yml` runs `rake check` on Linux with Ruby 3.3, 3.4, and 4.0. The workflow configuration has been validated locally; those hosted jobs have not yet run for these changes.
|
|
18
|
+
|
|
19
|
+
Code size was reviewed using Ruby's Prism AST to locate complete methods. No artificial complexity score is enforced. The largest configuration/type-inference methods were split by responsibility; see [testing-review.md](testing-review.md) for the measured sizes and coverage findings.
|
|
20
|
+
|
|
21
|
+
## Reference sources
|
|
22
|
+
|
|
23
|
+
Reference repositories are checked out under Git-ignored `tmp/upstream/`. The adapter does not load implementation code from them.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
mkdir -p tmp/upstream
|
|
27
|
+
git clone --depth 1 https://github.com/tursodatabase/libsql-ruby.git tmp/upstream/libsql-ruby
|
|
28
|
+
git clone --depth 1 https://github.com/tursodatabase/libsql-activerecord.git tmp/upstream/libsql-activerecord
|
|
29
|
+
git clone --depth 1 https://github.com/jeremyevans/sequel.git tmp/upstream/sequel
|
|
30
|
+
git clone --depth 1 https://github.com/sparklemotion/sqlite3-ruby.git tmp/upstream/sqlite3-ruby
|
|
31
|
+
git clone --depth 1 https://github.com/tursodatabase/libsql-c.git tmp/upstream/libsql-c
|
|
32
|
+
git clone --depth 1 https://github.com/tursodatabase/libsql-client-ts.git tmp/upstream/libsql-client-ts
|
|
33
|
+
git clone --depth 1 https://github.com/libsql/hrana-client-ts.git tmp/upstream/hrana-client-ts
|
|
34
|
+
git clone --depth 1 --filter=blob:none --sparse https://github.com/tursodatabase/libsql.git tmp/upstream/libsql
|
|
35
|
+
git -C tmp/upstream/libsql sparse-checkout set libsql/src libsql-hrana
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`bundle exec rake upstream` runs the cloned Sequel SQLite adapter tests plus its integration suite in the upstream working directory, preserving its migration fixtures. It loads the cloned Sequel itself rather than accidentally loading two Sequel versions into one process. Tests use an in-memory database.
|
|
39
|
+
|
|
40
|
+
## Native HTTP integration
|
|
41
|
+
|
|
42
|
+
Prepare the official test server in an isolated Python environment:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
python3.11 -m venv tmp/hrana-venv
|
|
46
|
+
tmp/hrana-venv/bin/pip install -r tmp/upstream/libsql-client-ts/testing/hrana-test-server/requirements.txt
|
|
47
|
+
bundle exec rake remote_local
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The fixture needs a free localhost port 8080. It creates a temporary SQLite file, spawns the Ruby tests, then shuts down and removes its database. `test/support/hrana_server.py` fixes only the Python fixture's base64 padding incompatibility with the Rust client. No upstream source is modified.
|
|
51
|
+
|
|
52
|
+
For a disposable hosted database, export `LIBSQL_TEST_URL` and `LIBSQL_TEST_AUTH_TOKEN`, then run `bundle exec rake remote`. Tests create randomly named tables and drop only those tables. Do not point destructive integration fixtures from another project at a production database.
|
|
53
|
+
|
|
54
|
+
## Packaging
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
mkdir -p pkg
|
|
58
|
+
gem build libsql-sequel.gemspec --output pkg/libsql-sequel-0.3.0.gem
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The explicit gem file list includes Ruby library files, public docs, examples, README, and LICENSE. It excludes private notes, cloned repositories, credentials, tests, and temporary databases. Nothing is automatically published.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Spec and code-quality review
|
|
2
|
+
|
|
3
|
+
Reviewed 2026-10-31 against the cloned upstream sources, after the initial adapter commit `0b966b1` was pushed.
|
|
4
|
+
|
|
5
|
+
## Upstream comparison
|
|
6
|
+
|
|
7
|
+
| Source | Existing coverage | Action here |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Sequel SQLite adapter + integration specs | SQL generation, schema changes, constraints, transactions, models, associations, migrations, prepared queries | Continue running unmodified upstream specs with `rake upstream` |
|
|
10
|
+
| Sequel SQLite type-conversion group | Seven cases defined only for `adapter_scheme == :sqlite`; absent from the external-adapter run | Added local equivalents in `test/sqlite_types_test.rb` |
|
|
11
|
+
| Sequel SQLite-specific URI, regexp, temporary-release groups | Target sqlite3-specific APIs or explicitly create `Sequel.sqlite` | Keep native libSQL URL tests and explicit rejection of unsupported regexp options; do not pretend these groups exercise this adapter |
|
|
12
|
+
| libsql-activerecord | No dedicated adapter spec directory or test CI; example Rails scaffold has controller/system CRUD tests and an empty model test | Existing model tests cover local CRUD; added remote model lifecycle with Sequel's prepared-statements plugin |
|
|
13
|
+
| libsql-activerecord execution code | Separates row-returning queries, batch execution, affected counts, and insert IDs | Added remote empty-result metadata, NULL values, and UPDATE/DELETE RETURNING cases |
|
|
14
|
+
|
|
15
|
+
The ActiveRecord adapter cannot be tested by loading its Rails scaffold into this project: that would exercise ActiveRecord's adapter, not the Sequel implementation.
|
|
16
|
+
|
|
17
|
+
## Bugs exposed by the new specs
|
|
18
|
+
|
|
19
|
+
- Integer and Float timestamps now use Unix seconds and Julian days, respectively, supporting both Time and DateTime output classes.
|
|
20
|
+
- Numeric TIME values now round fractional seconds to microseconds as Sequel does.
|
|
21
|
+
- Numeric/text storage values in a BLOB column now become `Sequel::SQL::Blob` rather than raising or remaining plain strings.
|
|
22
|
+
- Nonnumeric text in a decimal column remains text, consistent with SQLite's flexible storage and Sequel's converter.
|
|
23
|
+
- Repeated `get` calls no longer lose result types once Sequel's placeholder-literalizer cache activates. The adapter disables that optimization because it hides projection origins; native bound/prepared statements still work.
|
|
24
|
+
|
|
25
|
+
## Linting and size review
|
|
26
|
+
|
|
27
|
+
Sequel has no RuboCop, Standard, Flog, Reek, or cyclomatic-complexity configuration. Its Rakefile supplies `check_syntax`, warning-enabled spec commands, and optional `visibility_checker` integration. Its contribution guide asks contributors to follow surrounding code style rather than prescribing an automatic formatter.
|
|
28
|
+
|
|
29
|
+
This repository now provides `check_syntax`, `check_visibility`, `lint`, and `check` tasks. Syntax and visibility failures were tested with deliberately invalid temporary probes and correctly returned nonzero exit statuses. These checks apply only to project code, not ignored upstream checkouts. No wholesale reformatting was performed.
|
|
30
|
+
|
|
31
|
+
Prism AST source ranges identified `Configuration#initialize` (58 lines) and `Dataset#libsql_types` (50 lines) as the longest methods. Configuration now separates location normalization, URL validation, credentials, replication options, and option validation. `ResultTypes` handles projection origins and `TypeConverter` handles semantic conversions. `Dataset` shrank from 130 to 58 lines. The longest library method is now 24 lines; the largest library file is 178 lines. These are source-size measurements, not cyclomatic scores.
|
|
32
|
+
|
|
33
|
+
## Verification
|
|
34
|
+
|
|
35
|
+
- `bundle exec rake check upstream remote_local`: 40 local tests / 196 assertions; 901 upstream tests / 7469 assertions / six upstream skips; five real native HTTP tests / 24 assertions. No failures or errors. Current-bundle syntax, warnings, and adapter visibility checks pass.
|
|
36
|
+
- The 40 local tests also pass with Sequel 5.80.0. That older Sequel emits a Ruby 4 URI deprecation warning from its own code; it is not an adapter warning.
|
|
37
|
+
- The CI workflow parses and invokes `rake check`, but its Linux/Ruby matrix has not been executed here.
|
|
38
|
+
|
|
39
|
+
Hosted Turso authentication/TLS, live replica sync/read-your-writes behavior, and encryption still need real service tests. Local descriptor support or mocked native calls would not establish those guarantees, so this review does not claim them.
|
|
40
|
+
|
|
41
|
+
## Source pointers
|
|
42
|
+
|
|
43
|
+
- [Sequel Rakefile](https://github.com/jeremyevans/sequel/blob/75b425dd1c1f34908426dd70371d14e4afb9c933/Rakefile)
|
|
44
|
+
- [Sequel SQLite specs](https://github.com/jeremyevans/sequel/blob/75b425dd1c1f34908426dd70371d14e4afb9c933/spec/adapters/sqlite_spec.rb)
|
|
45
|
+
- [Sequel contribution guide](https://github.com/jeremyevans/sequel/blob/75b425dd1c1f34908426dd70371d14e4afb9c933/CONTRIBUTING)
|
|
46
|
+
- [libsql-activerecord example tests](https://github.com/tursodatabase/libsql-activerecord/tree/4873624743b046dde6c0188963b22576d8341e3a/examples/railsapp/test)
|
data/examples/local.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'libsql-sequel'
|
|
4
|
+
|
|
5
|
+
Sequel.connect(:adapter => :libsql) do |db|
|
|
6
|
+
db.create_table(:items) do
|
|
7
|
+
primary_key :id
|
|
8
|
+
String :name, :null => false
|
|
9
|
+
TrueClass :active, :default => true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
items = db[:items]
|
|
13
|
+
id = items.insert(:name => 'Hello, libSQL')
|
|
14
|
+
find = items.where(:id => :$id).prepare(:first, :find_item)
|
|
15
|
+
p find.call(:id => id)
|
|
16
|
+
|
|
17
|
+
db.transaction(:rollback => :always){items.insert(:name => 'discarded')}
|
|
18
|
+
puts "#{items.count} persisted item"
|
|
19
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'sequel'
|
|
4
|
+
require 'sequel/adapters/shared/sqlite'
|
|
5
|
+
require_relative '../libsql/connection'
|
|
6
|
+
require_relative '../libsql/configuration'
|
|
7
|
+
require_relative '../libsql/dataset'
|
|
8
|
+
|
|
9
|
+
module Sequel
|
|
10
|
+
module LibSQL
|
|
11
|
+
class Database < Sequel::Database
|
|
12
|
+
include Sequel::SQLite::DatabaseMethods
|
|
13
|
+
|
|
14
|
+
set_adapter_scheme :libsql
|
|
15
|
+
|
|
16
|
+
# libsql://host is a native remote URL. Use a hash for local files.
|
|
17
|
+
def self.uri_to_options(uri)
|
|
18
|
+
{:database => uri.to_s}
|
|
19
|
+
end
|
|
20
|
+
private_class_method :uri_to_options
|
|
21
|
+
|
|
22
|
+
def connect(server)
|
|
23
|
+
options = server_opts(server)
|
|
24
|
+
configuration = Configuration.new(self, options)
|
|
25
|
+
connection = Connection.new(configuration.native_options)
|
|
26
|
+
begin
|
|
27
|
+
pragmas = connection_pragmas
|
|
28
|
+
if configuration.remote?
|
|
29
|
+
# The native remote SQL parser rejects these local-only PRAGMAs.
|
|
30
|
+
pragmas = pragmas.grep(/\APRAGMA foreign_keys = /)
|
|
31
|
+
else
|
|
32
|
+
timeout = Integer(options.fetch(:timeout, 5000))
|
|
33
|
+
raise Sequel::Error, 'timeout must be a nonnegative 32-bit millisecond value' unless (0..2_147_483_647).cover?(timeout)
|
|
34
|
+
pragmas += ["PRAGMA busy_timeout = #{timeout}"]
|
|
35
|
+
end
|
|
36
|
+
pragmas.each do |sql|
|
|
37
|
+
log_connection_yield(sql, connection){connection.execute_batch(sql)}
|
|
38
|
+
end
|
|
39
|
+
connection
|
|
40
|
+
rescue Exception
|
|
41
|
+
connection.close
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def disconnect_connection(connection)
|
|
47
|
+
connection.close
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Match SQLite's unix-second and Julian-day timestamp representations.
|
|
51
|
+
def to_application_timestamp(value)
|
|
52
|
+
case value
|
|
53
|
+
when Integer
|
|
54
|
+
super(Time.at(value).to_s)
|
|
55
|
+
when Float
|
|
56
|
+
super(DateTime.jd(value).to_s)
|
|
57
|
+
else
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def execute(sql, opts=OPTS, &block)
|
|
63
|
+
_execute(:select, sql, opts, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def execute_dui(sql, opts=OPTS)
|
|
67
|
+
_execute(:update, sql, opts)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_insert(sql, opts=OPTS)
|
|
71
|
+
_execute(:insert, sql, opts)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# DB.run accepts SQL batches, as in Sequel's SQLite adapter.
|
|
75
|
+
def execute_ddl(sql, opts=OPTS)
|
|
76
|
+
synchronize(opts[:server]) do |connection|
|
|
77
|
+
log_connection_yield(sql, connection){connection.execute_batch(sql)}
|
|
78
|
+
end
|
|
79
|
+
rescue Native::Error => e
|
|
80
|
+
raise_error(e)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Pull remote changes into an embedded replica. Writes use the native
|
|
84
|
+
# read-your-writes setting (enabled by default).
|
|
85
|
+
def sync(opts=OPTS)
|
|
86
|
+
synchronize(opts[:server]) do |connection|
|
|
87
|
+
raise Sequel::Error, 'cannot sync inside a transaction' if in_transaction?(:server => opts[:server])
|
|
88
|
+
connection.sync
|
|
89
|
+
end
|
|
90
|
+
rescue Native::Error => e
|
|
91
|
+
raise_error(e)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def adapter_initialize
|
|
97
|
+
set_integer_booleans
|
|
98
|
+
self.transaction_mode = @opts[:transaction_mode].to_sym if @opts[:transaction_mode]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def connection_pool_default_options
|
|
102
|
+
options = super.dup
|
|
103
|
+
options[:max_connections] = 1 if Configuration.new(self, @opts).single_connection?
|
|
104
|
+
options
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def database_error_classes
|
|
108
|
+
[Native::Error]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Validation sends SELECT NULL, while transaction commands send no rows.
|
|
112
|
+
# The native batch API handles both without leaking a result cursor.
|
|
113
|
+
def connection_execute_method
|
|
114
|
+
:execute_batch
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def database_specific_error_class(exception, opts)
|
|
118
|
+
case exception.message
|
|
119
|
+
when /libSQL connection is closed|Hrana:.*stream closed/i
|
|
120
|
+
DatabaseDisconnectError
|
|
121
|
+
when /UNIQUE constraint failed|PRIMARY KEY must be unique/i
|
|
122
|
+
UniqueConstraintViolation
|
|
123
|
+
when /NOT NULL constraint failed/i
|
|
124
|
+
NotNullConstraintViolation
|
|
125
|
+
when /FOREIGN KEY constraint failed/i
|
|
126
|
+
ForeignKeyConstraintViolation
|
|
127
|
+
when /CHECK constraint failed/i
|
|
128
|
+
CheckConstraintViolation
|
|
129
|
+
when /cannot store \w+ value in \w+ column|constraint failed/i
|
|
130
|
+
ConstraintViolation
|
|
131
|
+
else
|
|
132
|
+
super
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def dataset_class_default
|
|
137
|
+
Dataset
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def _execute(type, sql, opts)
|
|
141
|
+
if sql.is_a?(Symbol)
|
|
142
|
+
sql = prepared_statement(sql).prepared_sql
|
|
143
|
+
end
|
|
144
|
+
arguments = opts.fetch(:arguments, [])
|
|
145
|
+
arguments = if arguments.is_a?(Hash)
|
|
146
|
+
arguments.transform_values{|value| bound_value(value)}
|
|
147
|
+
else
|
|
148
|
+
arguments.map{|value| bound_value(value)}
|
|
149
|
+
end
|
|
150
|
+
synchronize(opts[:server]) do |connection|
|
|
151
|
+
log_connection_yield(sql, connection, opts[:arguments]) do
|
|
152
|
+
if type == :select && block_given?
|
|
153
|
+
connection.query(sql, arguments){|result| yield result}
|
|
154
|
+
else
|
|
155
|
+
changed = connection.execute(sql, arguments)
|
|
156
|
+
type == :insert ? connection.last_insert_id : changed
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue Native::Error => e
|
|
161
|
+
raise_error(e)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def bound_value(value)
|
|
165
|
+
case value
|
|
166
|
+
when Time, Date, DateTime
|
|
167
|
+
literal(value)[1...-1]
|
|
168
|
+
when BigDecimal
|
|
169
|
+
value.to_s('F')
|
|
170
|
+
when true, false
|
|
171
|
+
integer_booleans ? (value ? 1 : 0) : (value ? 't' : 'f')
|
|
172
|
+
else
|
|
173
|
+
value
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module LibSQL
|
|
5
|
+
# Normalize local, remote, and embedded-replica options in one place.
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_reader :native_options
|
|
8
|
+
|
|
9
|
+
def initialize(db, options)
|
|
10
|
+
@db = db
|
|
11
|
+
@options = options
|
|
12
|
+
path, url = location
|
|
13
|
+
validate_url(url)
|
|
14
|
+
# Turso Cloud now supplies turso:// URLs. The pinned native driver
|
|
15
|
+
# predates that alias but supports the equivalent HTTPS endpoint.
|
|
16
|
+
url = url.sub(/\Aturso:/i, 'https:') if url
|
|
17
|
+
@native_options = {:path => path, :url => url}
|
|
18
|
+
add_credentials
|
|
19
|
+
add_replication_options
|
|
20
|
+
validate_options
|
|
21
|
+
rescue URI::InvalidURIError, ArgumentError, TypeError
|
|
22
|
+
raise Sequel::Error, 'invalid libSQL connection configuration'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replica?
|
|
26
|
+
!!(@native_options[:path] && @native_options[:url])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remote?
|
|
30
|
+
!!@native_options[:url]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def single_connection?
|
|
34
|
+
replica? || @native_options[:path] == ':memory:'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def location
|
|
40
|
+
database = @options[:database]
|
|
41
|
+
database = nil if database.to_s.empty?
|
|
42
|
+
url = @options[:url]
|
|
43
|
+
if database && /\A(?:turso|libsql|https?):\/\//i.match?(database.to_s)
|
|
44
|
+
raise Sequel::Error, 'specify the remote URL with database or url, not both' if url && url != database
|
|
45
|
+
url = database
|
|
46
|
+
database = nil
|
|
47
|
+
end
|
|
48
|
+
path = @options[:path] || database
|
|
49
|
+
raise Sequel::Error, 'path must not be empty' if path && path.to_s.empty?
|
|
50
|
+
if @options[:path] && database && @options[:path].to_s != database.to_s
|
|
51
|
+
raise Sequel::Error, 'database and path specify different local files'
|
|
52
|
+
end
|
|
53
|
+
path = ':memory:' unless path || url
|
|
54
|
+
[path && path.to_s, url && url.to_s]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_url(url)
|
|
58
|
+
if url
|
|
59
|
+
uri = URI.parse(url.to_s)
|
|
60
|
+
unless %w[turso libsql https http].include?(uri.scheme) && uri.host && !uri.userinfo && !uri.fragment && !uri.query
|
|
61
|
+
raise Sequel::Error, 'libSQL URL must use turso, libsql, https, or http with a host and no credentials, query, or fragment'
|
|
62
|
+
end
|
|
63
|
+
elsif @options[:auth_token]
|
|
64
|
+
raise Sequel::Error, 'auth_token requires a remote URL'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add_credentials
|
|
69
|
+
[:auth_token, :encryption_key].each{|key| @native_options[key] = @options[key] if @options.key?(key)}
|
|
70
|
+
# 0.2.0 requires a non-null token pointer even for an unauthenticated
|
|
71
|
+
# local Hrana server. An empty token is the native no-auth convention.
|
|
72
|
+
@native_options[:auth_token] ||= '' if remote?
|
|
73
|
+
if @options.key?(:namespace)
|
|
74
|
+
unless CLibsql::DatabaseDesc.members.include?(:namespace)
|
|
75
|
+
raise Sequel::Error, 'namespace is not supported by this turso_libsql release'
|
|
76
|
+
end
|
|
77
|
+
@native_options[:namespace] = @options[:namespace]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_replication_options
|
|
82
|
+
@native_options[:read_your_writes] = @db.send(:typecast_value_boolean, @options.fetch(:read_your_writes, true))
|
|
83
|
+
interval = Integer(@options.fetch(:sync_interval, 0))
|
|
84
|
+
raise Sequel::Error, 'sync_interval must be an unsigned 64-bit millisecond interval' unless (0...(1 << 64)).cover?(interval)
|
|
85
|
+
raise Sequel::Error, 'sync_interval requires an embedded replica' if interval > 0 && !replica?
|
|
86
|
+
raise Sequel::Error, 'an embedded replica requires a file path, not :memory:' if replica? && @native_options[:path] == ':memory:'
|
|
87
|
+
@native_options[:sync_interval] = interval
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_options
|
|
91
|
+
if remote?
|
|
92
|
+
[:timeout, :case_sensitive_like, :auto_vacuum, :synchronous, :temp_store].each do |key|
|
|
93
|
+
raise Sequel::Error, "#{key} is only supported for local libSQL databases" if @options.key?(key)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
[:readonly, :disable_dqs, :setup_regexp_function, :synced, :webpki].each do |key|
|
|
98
|
+
raise Sequel::Error, "#{key} is not supported by this adapter" if @options.key?(key)
|
|
99
|
+
end
|
|
100
|
+
if single_connection? && Integer(@options.fetch(:max_connections, 1)) != 1
|
|
101
|
+
raise Sequel::Error, 'memory databases and embedded replicas require max_connections: 1'
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'native'
|
|
4
|
+
|
|
5
|
+
module Sequel
|
|
6
|
+
module LibSQL
|
|
7
|
+
# Owns a native database and its connection for one Sequel pool slot.
|
|
8
|
+
class Connection
|
|
9
|
+
def initialize(options)
|
|
10
|
+
@pid = Process.pid
|
|
11
|
+
@replica = options[:path] && options[:url]
|
|
12
|
+
desc = CLibsql::DatabaseDesc.new
|
|
13
|
+
@strings = []
|
|
14
|
+
[:path, :url, :auth_token, :encryption_key, :namespace].each do |key|
|
|
15
|
+
next if options[key].nil?
|
|
16
|
+
desc[key] = pointer = FFI::MemoryPointer.from_string(Native.string(options[key]))
|
|
17
|
+
@strings << pointer
|
|
18
|
+
end
|
|
19
|
+
desc[:sync_interval] = options.fetch(:sync_interval, 0)
|
|
20
|
+
desc[:disable_read_your_writes] = !options.fetch(:read_your_writes, true)
|
|
21
|
+
@database = Native.check(Native.database_init(desc))
|
|
22
|
+
begin
|
|
23
|
+
@connection = Native.check(Native.database_connect(@database))
|
|
24
|
+
rescue Exception
|
|
25
|
+
close
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def execute(sql, arguments = [])
|
|
31
|
+
with_statement(sql, arguments) do |statement|
|
|
32
|
+
Native.check(Native.statement_execute(statement))[:rows_changed]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def execute_batch(sql)
|
|
37
|
+
check_open
|
|
38
|
+
Native.check(Native.connection_batch(@connection, Native.string(sql)))
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def query(sql, arguments = [])
|
|
43
|
+
with_statement(sql, arguments) do |statement|
|
|
44
|
+
result = Result.new(Native.check(Native.statement_query(statement)))
|
|
45
|
+
begin
|
|
46
|
+
yield result
|
|
47
|
+
ensure
|
|
48
|
+
result.close
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def last_insert_id
|
|
54
|
+
check_open
|
|
55
|
+
Native.check(Native.connection_info(@connection))[:last_inserted_id]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sync
|
|
59
|
+
check_open
|
|
60
|
+
raise Sequel::Error, 'sync requires an embedded replica (both path and remote URL)' unless @replica
|
|
61
|
+
result = Native.check(Native.database_sync(@database))
|
|
62
|
+
{:frame_no => result[:frame_no], :frames_synced => result[:frames_synced]}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def close
|
|
66
|
+
# Inherited handles belong to the parent; their runtime cannot be used
|
|
67
|
+
# or destroyed in the child. Let Sequel empty the child's pool safely.
|
|
68
|
+
if @pid != Process.pid
|
|
69
|
+
@connection = @database = @strings = nil
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
if @connection
|
|
73
|
+
CLibsql.libsql_connection_deinit(@connection)
|
|
74
|
+
@connection = nil
|
|
75
|
+
end
|
|
76
|
+
if @database
|
|
77
|
+
CLibsql.libsql_database_deinit(@database)
|
|
78
|
+
@database = nil
|
|
79
|
+
end
|
|
80
|
+
@strings = nil
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def check_process
|
|
87
|
+
raise Sequel::Error, 'libSQL connections cannot be used after fork; connect inside each worker' unless @pid == Process.pid
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_open
|
|
91
|
+
check_process
|
|
92
|
+
raise Native::Error, 'libSQL connection is closed' unless @connection
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def with_statement(sql, arguments)
|
|
96
|
+
check_open
|
|
97
|
+
statement = Native.check(Native.connection_prepare(@connection, Native.string(sql)))
|
|
98
|
+
begin
|
|
99
|
+
case arguments
|
|
100
|
+
when Array
|
|
101
|
+
arguments.each do |value|
|
|
102
|
+
Native.bind_value(value){|v| Native.check(CLibsql.libsql_statement_bind_value(statement, v))}
|
|
103
|
+
end
|
|
104
|
+
when Hash
|
|
105
|
+
arguments.each do |name, value|
|
|
106
|
+
name = Native.string(name)
|
|
107
|
+
name = ":#{name}" unless /\A[:@$]/.match?(name)
|
|
108
|
+
Native.bind_value(value){|v| Native.check(CLibsql.libsql_statement_bind_named(statement, name, v))}
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
raise Sequel::Error, 'libSQL arguments must be an Array or Hash'
|
|
112
|
+
end
|
|
113
|
+
yield statement
|
|
114
|
+
ensure
|
|
115
|
+
CLibsql.libsql_statement_deinit(statement)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Only lives inside Connection#query; rows are freed even on break or raise.
|
|
121
|
+
class Result
|
|
122
|
+
def initialize(rows)
|
|
123
|
+
@rows = rows
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def columns
|
|
127
|
+
check_open
|
|
128
|
+
@columns ||= Array.new(Native.rows_column_count(@rows)) do |index|
|
|
129
|
+
Native.read_slice(CLibsql.libsql_rows_column_name(@rows, index), true)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def each
|
|
134
|
+
check_open
|
|
135
|
+
loop do
|
|
136
|
+
check_open
|
|
137
|
+
row = Native.check(Native.rows_next(@rows))
|
|
138
|
+
break if row[:inner].null?
|
|
139
|
+
begin
|
|
140
|
+
values = Array.new(columns.length) do |index|
|
|
141
|
+
Native.value(Native.check(CLibsql.libsql_row_value(row, index))[:ok])
|
|
142
|
+
end
|
|
143
|
+
yield values
|
|
144
|
+
ensure
|
|
145
|
+
CLibsql.libsql_row_deinit(row)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def close
|
|
151
|
+
if @rows
|
|
152
|
+
CLibsql.libsql_rows_deinit(@rows)
|
|
153
|
+
@rows = nil
|
|
154
|
+
end
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def check_open
|
|
161
|
+
raise Native::Error, 'libSQL result is closed' unless @rows
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'result_types'
|
|
4
|
+
require_relative 'type_converter'
|
|
5
|
+
|
|
6
|
+
module Sequel
|
|
7
|
+
module LibSQL
|
|
8
|
+
class Dataset < Sequel::Dataset
|
|
9
|
+
include Sequel::SQLite::DatasetMethods
|
|
10
|
+
|
|
11
|
+
BindArgumentMethods = prepared_statements_module(:bind, Sequel::Dataset::UnnumberedArgumentMapper)
|
|
12
|
+
PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)
|
|
13
|
+
|
|
14
|
+
# Explicit conversions for raw SQL, expressions, or ambiguous projections.
|
|
15
|
+
# DB.fetch(sql).with_libsql_types(:active=>:boolean, :created_at=>:datetime)
|
|
16
|
+
def with_libsql_types(types)
|
|
17
|
+
clone(:libsql_types => (@opts[:libsql_types] || OPTS).merge(types).freeze)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Cached literalizers replace selected columns with opaque arguments.
|
|
21
|
+
# Without C decltypes, keep the projection AST for stable conversions.
|
|
22
|
+
def supports_placeholder_literalizer?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch_rows(sql)
|
|
27
|
+
execute(sql) do |result|
|
|
28
|
+
columns = result.columns.map{|column| output_identifier(column)}
|
|
29
|
+
types = ResultTypes.new(self).for_columns(columns)
|
|
30
|
+
converter = TypeConverter.new(db)
|
|
31
|
+
self.columns = columns
|
|
32
|
+
result.each do |values|
|
|
33
|
+
row = {}
|
|
34
|
+
columns.each_with_index do |name, index|
|
|
35
|
+
value = values[index]
|
|
36
|
+
row[name] = converter.call(value, types[name])
|
|
37
|
+
end
|
|
38
|
+
yield row
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def bound_variable_modules
|
|
46
|
+
[BindArgumentMethods]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prepared_statement_modules
|
|
50
|
+
[PreparedStatementMethods]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def literal_string_append(sql, value)
|
|
54
|
+
sql << "'" << Native.string(value).gsub("'", "''") << "'"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
gem 'turso_libsql', '= 0.2.0'
|
|
4
|
+
require 'libsql'
|
|
5
|
+
|
|
6
|
+
module Sequel
|
|
7
|
+
module LibSQL
|
|
8
|
+
# Private compatibility boundary for turso_libsql 0.2.x. Its high-level
|
|
9
|
+
# wrappers have unsafe error handling and incorrect value conversions.
|
|
10
|
+
# Reuse its ABI structs and library, without changing global driver classes.
|
|
11
|
+
module Native
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
extend FFI::Library
|
|
15
|
+
ffi_lib(*CLibsql.ffi_libraries.map(&:name))
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
:database_init => [[CLibsql::DatabaseDesc.by_value], CLibsql::Database.by_value],
|
|
19
|
+
:database_connect => [[CLibsql::Database.by_value], CLibsql::Connection.by_value],
|
|
20
|
+
:database_sync => [[CLibsql::Database.by_value], CLibsql::Sync.by_value],
|
|
21
|
+
:connection_prepare => [[CLibsql::Connection.by_value, :string], CLibsql::Statement.by_value],
|
|
22
|
+
:connection_info => [[CLibsql::Connection.by_value], CLibsql::ConnectionInfo.by_value],
|
|
23
|
+
:connection_batch => [[CLibsql::Connection.by_value, :string], CLibsql::Batch.by_value],
|
|
24
|
+
:statement_execute => [[CLibsql::Statement.by_value], CLibsql::Execute.by_value],
|
|
25
|
+
:statement_query => [[CLibsql::Statement.by_value], CLibsql::Rows.by_value],
|
|
26
|
+
:rows_next => [[CLibsql::Rows.by_value], CLibsql::Row.by_value]
|
|
27
|
+
}.each do |name, (args, result)|
|
|
28
|
+
attach_function name, :"libsql_#{name}", args, result, :blocking => true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The upstream declaration incorrectly uses Row.by_value for this call.
|
|
32
|
+
attach_function :slice_deinit, :libsql_slice_deinit, [CLibsql::Slice.by_value], :void
|
|
33
|
+
|
|
34
|
+
# The released gem and upstream main use different names for this symbol.
|
|
35
|
+
count_function = CLibsql.respond_to?(:libsql_rows_column_count) ? :libsql_rows_column_count : :libsql_rows_column_length
|
|
36
|
+
attach_function :rows_column_count, count_function, [CLibsql::Rows.by_value], :int32
|
|
37
|
+
|
|
38
|
+
def self.check(result)
|
|
39
|
+
return result if result[:err].null?
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
message = CLibsql.libsql_error_message(result[:err])
|
|
43
|
+
ensure
|
|
44
|
+
CLibsql.libsql_error_deinit(result[:err])
|
|
45
|
+
end
|
|
46
|
+
raise Error, message
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.string(value)
|
|
50
|
+
value = value.to_s.encode(Encoding::UTF_8)
|
|
51
|
+
raise Sequel::Error, 'libSQL text cannot contain NUL bytes; use Sequel.blob for binary data' if value.include?("\0")
|
|
52
|
+
raise Sequel::Error, 'libSQL text must be valid UTF-8' unless value.valid_encoding?
|
|
53
|
+
value
|
|
54
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
55
|
+
raise Sequel::Error, 'libSQL text must be convertible to UTF-8; use Sequel.blob for binary data'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# C text slices include a terminal NUL; blob slices do not.
|
|
59
|
+
def self.read_slice(slice, text)
|
|
60
|
+
return text ? '' : Sequel.blob('') if slice[:ptr].null?
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
length = slice[:len]
|
|
64
|
+
value = slice[:ptr].read_string_length(text ? [length - 1, 0].max : length)
|
|
65
|
+
text ? value.force_encoding(Encoding::UTF_8) : Sequel.blob(value)
|
|
66
|
+
ensure
|
|
67
|
+
slice_deinit(slice)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.value(value)
|
|
72
|
+
case value[:type]
|
|
73
|
+
when :null
|
|
74
|
+
nil
|
|
75
|
+
when :integer
|
|
76
|
+
# The Ruby driver declares uint64, while the C API returns int64.
|
|
77
|
+
integer = value[:value][:integer]
|
|
78
|
+
integer >= (1 << 63) ? integer - (1 << 64) : integer
|
|
79
|
+
when :real
|
|
80
|
+
value[:value][:real]
|
|
81
|
+
when :text, :blob
|
|
82
|
+
read_slice(value[:value][value[:type]], value[:type] == :text)
|
|
83
|
+
else
|
|
84
|
+
raise Error, 'unknown libSQL value type'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Keep the input pointer alive until the native bind has copied its bytes.
|
|
89
|
+
def self.bind_value(value)
|
|
90
|
+
case value
|
|
91
|
+
when nil
|
|
92
|
+
yield CLibsql.libsql_null
|
|
93
|
+
when Integer
|
|
94
|
+
unless (-(1 << 63)...(1 << 63)).cover?(value)
|
|
95
|
+
raise Sequel::Error, 'libSQL integers must fit in signed 64 bits'
|
|
96
|
+
end
|
|
97
|
+
yield CLibsql.libsql_integer(value)
|
|
98
|
+
when Float
|
|
99
|
+
raise Sequel::Error, 'libSQL cannot bind non-finite floats' unless value.finite?
|
|
100
|
+
yield CLibsql.libsql_real(value)
|
|
101
|
+
when SQL::Blob
|
|
102
|
+
pointer = FFI::MemoryPointer.new(:char, [value.bytesize, 1].max)
|
|
103
|
+
pointer.put_bytes(0, value)
|
|
104
|
+
yield CLibsql.libsql_blob(pointer, value.bytesize)
|
|
105
|
+
when String
|
|
106
|
+
value = string(value)
|
|
107
|
+
pointer = FFI::MemoryPointer.from_string(value)
|
|
108
|
+
yield CLibsql.libsql_text(pointer, value.bytesize)
|
|
109
|
+
else
|
|
110
|
+
raise Sequel::Error, "unsupported libSQL bound value type: #{value.class}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module LibSQL
|
|
5
|
+
# Resolve only projections with a known table-column origin. C exposes no
|
|
6
|
+
# declared types, so raw SQL and ambiguous result columns need explicit hints.
|
|
7
|
+
class ResultTypes
|
|
8
|
+
def initialize(dataset)
|
|
9
|
+
@dataset = dataset
|
|
10
|
+
@opts = dataset.opts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def for_columns(columns)
|
|
14
|
+
types = inferred_types
|
|
15
|
+
columns.tally.each{|name, count| types.delete(name) if count > 1}
|
|
16
|
+
types.merge(@opts[:libsql_types] || OPTS)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def inferred_types
|
|
22
|
+
if original = @opts[:orig_dataset]
|
|
23
|
+
return self.class.new(original).send(:inferred_types)
|
|
24
|
+
end
|
|
25
|
+
return {} unless table = source_table
|
|
26
|
+
selection = @opts[:returning] || @opts[:select]
|
|
27
|
+
return {} if selection && !selection.empty? && selection.none?{|column| column?(column)}
|
|
28
|
+
|
|
29
|
+
schema = @dataset.db.schema(table).to_h
|
|
30
|
+
return schema.transform_values{|info| info[:type]} if !selection || selection.empty?
|
|
31
|
+
types = {}
|
|
32
|
+
selection.each{|column| add_projection(types, schema, column)}
|
|
33
|
+
types
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def source_table
|
|
37
|
+
return if [:sql, :join, :with, :compounds].any?{|key| @opts[key]}
|
|
38
|
+
sources = @opts[:from]
|
|
39
|
+
return unless sources && sources.length == 1
|
|
40
|
+
source = sources.first
|
|
41
|
+
source = source.expression if source.is_a?(SQL::AliasedExpression)
|
|
42
|
+
source if identifier?(source) || source.is_a?(String)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add_projection(types, schema, column)
|
|
46
|
+
if column.is_a?(SQL::ColumnAll)
|
|
47
|
+
return schema.each{|name, info| types[name] = info[:type]}
|
|
48
|
+
end
|
|
49
|
+
name, aliaz = column_names(column)
|
|
50
|
+
if name && (info = schema[output_identifier(name)])
|
|
51
|
+
types[output_identifier(aliaz || name)] = info[:type]
|
|
52
|
+
elsif aliaz
|
|
53
|
+
types.delete(output_identifier(aliaz))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def column_names(column)
|
|
58
|
+
aliaz = nil
|
|
59
|
+
if column.is_a?(SQL::AliasedExpression)
|
|
60
|
+
aliaz = column.alias
|
|
61
|
+
column = column.expression
|
|
62
|
+
end
|
|
63
|
+
name = case column
|
|
64
|
+
when Symbol
|
|
65
|
+
_, name, symbol_alias = @dataset.send(:split_symbol, column)
|
|
66
|
+
aliaz ||= symbol_alias
|
|
67
|
+
name
|
|
68
|
+
when SQL::Identifier
|
|
69
|
+
column.value
|
|
70
|
+
when SQL::QualifiedIdentifier
|
|
71
|
+
column.column
|
|
72
|
+
end
|
|
73
|
+
[name, aliaz]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def column?(column)
|
|
77
|
+
column = column.expression if column.is_a?(SQL::AliasedExpression)
|
|
78
|
+
identifier?(column) || column.is_a?(SQL::ColumnAll)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def identifier?(value)
|
|
82
|
+
value.is_a?(Symbol) || value.is_a?(SQL::Identifier) || value.is_a?(SQL::QualifiedIdentifier)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def output_identifier(value)
|
|
86
|
+
value = value.value if value.is_a?(SQL::Identifier)
|
|
87
|
+
@dataset.send(:output_identifier, value)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module LibSQL
|
|
5
|
+
# Apply SQLite's semantic types after decoding the native storage value.
|
|
6
|
+
class TypeConverter
|
|
7
|
+
FALSE_VALUES = [0, '0', 'false', 'f', 'no', 'n'].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(database)
|
|
10
|
+
@database = database
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(value, type)
|
|
14
|
+
return nil if value.nil?
|
|
15
|
+
return type.call(value) if type.respond_to?(:call)
|
|
16
|
+
case type
|
|
17
|
+
when :boolean
|
|
18
|
+
!FALSE_VALUES.include?(value.is_a?(String) ? value.downcase : value)
|
|
19
|
+
when :date
|
|
20
|
+
value.is_a?(Numeric) ? Date.jd(value.to_i) : Sequel.string_to_date(value)
|
|
21
|
+
when :datetime, :timestamp
|
|
22
|
+
@database.to_application_timestamp(value)
|
|
23
|
+
when :time
|
|
24
|
+
time(value)
|
|
25
|
+
when :decimal
|
|
26
|
+
decimal(value)
|
|
27
|
+
when :integer
|
|
28
|
+
value.to_i
|
|
29
|
+
when :float
|
|
30
|
+
value.to_f
|
|
31
|
+
when :blob
|
|
32
|
+
Sequel.blob(value.to_s)
|
|
33
|
+
else
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def time(value)
|
|
41
|
+
return Sequel.string_to_time(value) if value.is_a?(String)
|
|
42
|
+
seconds, fraction = value.divmod(1)
|
|
43
|
+
SQLTime.create(seconds / 3600, (seconds % 3600) / 60, seconds % 60, (fraction * 1_000_000).round)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decimal(value)
|
|
47
|
+
BigDecimal(value.to_s)
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
value.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: libsql-sequel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Francesco Canessa
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sequel
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.80'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '6'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.80'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '6'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: turso_libsql
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - '='
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 0.2.0
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - '='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 0.2.0
|
|
46
|
+
description: Sequel SQL, models, migrations, and transactions using the native libSQL
|
|
47
|
+
Ruby driver.
|
|
48
|
+
executables: []
|
|
49
|
+
extensions: []
|
|
50
|
+
extra_rdoc_files: []
|
|
51
|
+
files:
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- docs/architecture.md
|
|
55
|
+
- docs/compatibility.md
|
|
56
|
+
- docs/development.md
|
|
57
|
+
- docs/testing-review.md
|
|
58
|
+
- examples/local.rb
|
|
59
|
+
- lib/libsql-sequel.rb
|
|
60
|
+
- lib/sequel/adapters/libsql.rb
|
|
61
|
+
- lib/sequel/adapters/turso.rb
|
|
62
|
+
- lib/sequel/libsql/configuration.rb
|
|
63
|
+
- lib/sequel/libsql/connection.rb
|
|
64
|
+
- lib/sequel/libsql/dataset.rb
|
|
65
|
+
- lib/sequel/libsql/native.rb
|
|
66
|
+
- lib/sequel/libsql/result_types.rb
|
|
67
|
+
- lib/sequel/libsql/type_converter.rb
|
|
68
|
+
- lib/sequel/libsql/version.rb
|
|
69
|
+
homepage: https://github.com/makevoid/libsql-sequel
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata: {}
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.3'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 4.0.10
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: A libSQL and Turso adapter for Sequel
|
|
90
|
+
test_files: []
|