diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6ec3a8..75e1a49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d308d..b48190c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index a1b3cd0..0e9484d 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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`: diff --git a/iyi.mod b/iyi.mod index 4ccd6c0..8345678 100644 --- a/iyi.mod +++ b/iyi.mod @@ -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 diff --git a/iyi.sum b/iyi.sum index e1de698..3da0b3a 100644 --- a/iyi.sum +++ b/iyi.sum @@ -1 +1 @@ -github.com/iyilang/db v0.4.0 s1:eb713fe8bccc5b89be8d2efa16e515ae96667a15 +github.com/iyilang/db v0.5.0 s1:a435c49b63b1fa7cbfbc97d6d36d5f83636f4675 diff --git a/sqlite/core.iyi b/sqlite/core.iyi index 8d47edb..629a026 100644 --- a/sqlite/core.iyi +++ b/sqlite/core.iyi @@ -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 @@ -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 diff --git a/sqlite/dates.iyi b/sqlite/dates.iyi index 3ea32a0..7ce0d36 100644 --- a/sqlite/dates.iyi +++ b/sqlite/dates.iyi @@ -28,24 +28,23 @@ 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 @@ -53,12 +52,12 @@ pub def parse_time(text : String) : Time | DBError 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 diff --git a/sqlite/pool_test.iyi b/sqlite/pool_test.iyi index 9f7b886..3a54a26 100644 --- a/sqlite/pool_test.iyi +++ b/sqlite/pool_test.iyi @@ -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 @@ -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