Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-24.04
env:
# The oldest iyi this release supports (README, Requirements).
IYI_VERSION: 0.20.0
IYI_VERSION: 0.21.0
steps:
- uses: actions/checkout@v4

Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
## Unreleased

Requires iyi 0.21.0 and iyi-db 0.5.0. iyi 0.21.0 refuses an operation on
raw memory in a module that is not marked `@[Unsafe("why")]` (SPEC.md
III.13), and 0.20.x refuses the mark above a `module` header, so no source
of this driver compiles on both.

* `sqlite/core`, the module that calls libsqlite3, says so, with why.
* `sqlite/dates` no longer touches raw memory: it reads a time's text
through `byte_at`, so the compiler checks every index.
* `REGEXP_FN` is marked `@[UnsafeReach]`. 0.21.0 proves a `@[SafeReach]`
claim instead of taking it on trust, and handing the proc to
`sqlite3_create_function` is beyond the proof; a module marked
`@[Unsafe]` keeps the claim on trust that way (SPEC.md III.4.4).
* `sqlite/pool_test`'s writers stay in the test's unit
(`spawn(unit: :same)`), as crystal-sqlite3's fibers shared a thread.
0.21.0 runs a plain spawn that captures only a `Share` `Database` in
parallel (SPEC.md III.4.15), and two SQLite connections writing at once
with no `busy_timeout` are refused "database is locked".

## v0.4.0

Requires iyi 0.20.0 and iyi-db 0.4.0.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require: errors are values, types are closed, and tasks are structured.

## Requirements

iyi 0.20.0 or later, iyi-db 0.4.0 (required by this package's `iyi.mod`), and
iyi 0.21.0 or later, iyi-db 0.5.0 (required by this package's `iyi.mod`), and
the SQLite library (`libsqlite3`):

* **Linux:** the development package, `apt install libsqlite3-dev`.
Expand All @@ -24,6 +24,10 @@ the SQLite library (`libsqlite3`):
developer prompt with `lib /def:sqlite3.def /out:sqlite3.lib /machine:x64`.
A build copies `sqlite3.dll` beside the executable.

0.21.0 refuses raw memory in a module that does not say it handles some
(SPEC.md III.13), and a module says so with `@[Unsafe("why")]` above its
`module` header, which 0.20.x refuses.

## Installation

Add it to a project under the short name `sqlite`:
Expand Down
2 changes: 1 addition & 1 deletion iyi.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `iyi get example.com/someone/lib` adds a dependency here.
module github.com/iyilang/sqlite

require github.com/iyilang/db v0.4.0 as db
require github.com/iyilang/db v0.5.0 as db
2 changes: 1 addition & 1 deletion iyi.sum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github.com/iyilang/db v0.4.0 s1:eb713fe8bccc5b89be8d2efa16e515ae96667a15
github.com/iyilang/db v0.5.0 s1:a435c49b63b1fa7cbfbc97d6d36d5f83636f4675
3 changes: 2 additions & 1 deletion sqlite/core.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#
# The names carry a `SQLite3` prefix because this module imports iyi-db's
# `Connection`, `Statement` and `ResultSet` by those names.
@[Unsafe("calls libsqlite3 through its C binding with the connection and statement handles it answers, passing text and blobs by pointer and copying the text and blobs it answers out of the library's memory")]
module sqlite/core

import std/int
Expand Down Expand Up @@ -198,7 +199,7 @@ SQLITE_UTF8 = 1
# The `regexp(pattern, text)` function SQLite's `REGEXP` operator calls,
# registered on every connection. A pattern iyi's `Regex` refuses answers
# an SQL error naming it; a `NULL` operand answers `NULL`.
@[SafeReach("captures nothing, is assigned once and only read; regexp keeps no state between calls")]
@[UnsafeReach("captures nothing and is only read: sqlite3_create_function keeps its pointer and calls it, and regexp keeps no state between calls")]
REGEXP_FN = ->(context : LibSQLite3::SQLite3Context, argc : Int32, argv : LibSQLite3::SQLite3Value*) do
regexp(context, argv)
nil
Expand Down
29 changes: 14 additions & 15 deletions sqlite/dates.iyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,36 @@ pub def parse_time(text : String) : Time | DBError
invalid = DBError.new("Invalid SQLite3 time: #{text.inspect}, expected #{DATE_FORMAT_SUBSECOND.inspect} or #{DATE_FORMAT_SECOND.inspect}")
size = text.bytesize
return invalid if size < 19
bytes = text.to_unsafe
return invalid unless bytes[4] == '-'.ord.to_u8 && bytes[7] == '-'.ord.to_u8 && bytes[10] == ' '.ord.to_u8 &&
bytes[13] == ':'.ord.to_u8 && bytes[16] == ':'.ord.to_u8
return invalid unless text.byte_at(4) == '-'.ord.to_u8 && text.byte_at(7) == '-'.ord.to_u8 && text.byte_at(10) == ' '.ord.to_u8 &&
text.byte_at(13) == ':'.ord.to_u8 && text.byte_at(16) == ':'.ord.to_u8

year = digits(bytes, 0, 4)
month = digits(bytes, 5, 2)
day = digits(bytes, 8, 2)
hour = digits(bytes, 11, 2)
minute = digits(bytes, 14, 2)
second = digits(bytes, 17, 2)
year = digits(text, 0, 4)
month = digits(text, 5, 2)
day = digits(text, 8, 2)
hour = digits(text, 11, 2)
minute = digits(text, 14, 2)
second = digits(text, 17, 2)
return invalid if year < 0 || month < 1 || month > 12 || day < 1 || day > Time.days_in_month(year, month)
return invalid if hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59

nanosecond = 0
if size > 19
fraction = size - 20
return invalid unless bytes[19] == '.'.ord.to_u8 && fraction >= 1 && fraction <= 9
nanosecond = digits(bytes, 20, fraction)
return invalid unless text.byte_at(19) == '.'.ord.to_u8 && fraction >= 1 && fraction <= 9
nanosecond = digits(text, 20, fraction)
return invalid if nanosecond < 0
(9 - fraction).times { nanosecond *= 10 }
end

Time.utc(year, month, day, hour, minute, second, nanosecond: nanosecond)
end

# The number written in the *count* ASCII digits at *start*, or -1 when one
# of them is not a digit.
private def digits(bytes : Pointer(UInt8), start : Int32, count : Int32) : Int32
# The number written in the *count* ASCII digits at byte *start* of *text*,
# or -1 when one of them is not a digit.
private def digits(text : String, start : Int32, count : Int32) : Int32
value = 0
count.times do |i|
byte = bytes[start + i]
byte = text.byte_at(start + i)
return -1 unless byte >= '0'.ord.to_u8 && byte <= '9'.ord.to_u8
value = value * 10 + (byte - '0'.ord.to_u8).to_i32
end
Expand Down
8 changes: 6 additions & 2 deletions sqlite/pool_test.iyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# crystal-sqlite3's spec/pool_spec.cr.
#
# crystal-sqlite3 spawned fibers and waited on a channel; here they are the
# tasks of a group, which waits for them as it ends (SPEC.md III.4.1).
# tasks of a group, which waits for them as it ends (SPEC.md III.4.1). They
# stay in the test's unit, as crystal-sqlite3's fibers shared one thread:
# a plain spawn of a block that captures only a `Share` `Database` runs in
# parallel (III.4.15), and two connections writing at once with no
# `busy_timeout` are refused "database is locked".
module sqlite/pool_test

import db
Expand All @@ -17,7 +21,7 @@ describe "DB::Pool" do

group do |g|
fibers.times do |f|
g.spawn do
g.spawn(unit: :same) do
(1..max_n).each do |n|
db.exec("insert into numbers (n, fiber) values (?, ?)", n, f).or_panic
sleep 10
Expand Down
Loading