Skip to content

Commit 6bb87fa

Browse files
Added support for uuid7
1 parent d994639 commit 6bb87fa

13 files changed

Lines changed: 437 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
### Added
77

8-
* Added support for the SQLite `uuid` extension, enabled via the `uuid` feature flag.
8+
* Added support for the SQLite `uuid4` extension, enabled via the `uuid4` feature flag.
9+
* Added support for the Postgres18 `uuid7` extension, enabled via the `uuid7` feature flag, ported from PostgreSQL implementation.
910

1011
### Fixed
1112

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ js-sys = { version = "0.3.81", default-features = false }
2424
# <https://utelle.github.io/SQLite3MultipleCiphers>
2525
sqlite3mc = []
2626
# Link the uuid extension to the library.
27-
uuid = []
27+
uuid4 = []
28+
uuid7 = []
2829

2930
[build-dependencies]
3031
cc = "1.2.27"

build.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,11 @@ fn compile() {
264264
#[allow(unused_mut)]
265265
let mut extensions: Vec<(&str, &str)> = Vec::new();
266266

267-
#[cfg(feature = "uuid")]
268-
extensions.push(("uuid", "sqlite3/ext/uuid.c"));
267+
#[cfg(feature = "uuid4")]
268+
extensions.push(("uuid4", "sqlite3/ext/uuid4.c"));
269+
270+
#[cfg(feature = "uuid7")]
271+
extensions.push(("uuid7", "sqlite3/ext/uuid7.c"));
269272

270273
for (_feature, source) in &extensions {
271274
cc.file(source);

shim/wasm-shim.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
#include <stdint.h>
33
#include <time.h>
44

5+
#define clock_gettime rust_sqlite_wasm_clock_gettime
6+
int rust_sqlite_wasm_clock_gettime(clockid_t clock_id, struct timespec *tp);
7+
58
/* string */
69
#define strcmp rust_sqlite_wasm_strcmp
710
int rust_sqlite_wasm_strcmp(const char *l, const char *r);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ static void sqlite3UuidBlobFunc(
228228
#ifdef _WIN32
229229
__declspec(dllexport)
230230
#endif
231-
int sqlite3_uuid_init(
231+
int sqlite3_uuid4_init(
232232
sqlite3 *db,
233233
char **pzErrMsg,
234234
const sqlite3_api_routines *pApi

sqlite3/ext/uuid7.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
** SQLite extension for UUIDv7.
3+
** Adapted from PostgreSQL implementation.
4+
** Original source:
5+
** <https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/uuid.c>
6+
*/
7+
#include "sqlite3ext.h"
8+
SQLITE_EXTENSION_INIT1
9+
#include <assert.h>
10+
#include <string.h>
11+
#include <time.h>
12+
#include <stdint.h>
13+
14+
/*
15+
** We use a local implementation of isxdigit to avoid a dependency on <ctype.h>.
16+
*/
17+
static int sqlite3Isxdigit(int c){
18+
return (c>='0' && c<='9') || (c>='a' && c<='f') || (c>='A' && c<='F');
19+
}
20+
21+
/*
22+
** Helper to convert hex to int.
23+
*/
24+
static unsigned char sqlite3UuidHexToInt(int h){
25+
assert( (h>='0' && h<='9') || (h>='a' && h<='f') || (h>='A' && h<='F') );
26+
if( h>='0' && h<='9' ) return h - '0';
27+
if( h>='a' && h<='f' ) return h - 'a' + 10;
28+
return h - 'A' + 10;
29+
}
30+
31+
/*
32+
** Convert blob to UUID string.
33+
*/
34+
static void sqlite3UuidBlobToStr(const unsigned char *aBlob, unsigned char *zStr){
35+
static const char zDigits[] = "0123456789abcdef";
36+
int i, k;
37+
unsigned char x;
38+
k = 0;
39+
for(i=0, k=0x550; i<16; i++, k=k>>1){
40+
if( k&1 ){
41+
zStr[0] = '-';
42+
zStr++;
43+
}
44+
x = aBlob[i];
45+
zStr[0] = zDigits[x>>4];
46+
zStr[1] = zDigits[x&0xf];
47+
zStr += 2;
48+
}
49+
*zStr = 0;
50+
}
51+
52+
/*
53+
** UUIDv7 Generation Logic
54+
*/
55+
56+
#define NS_PER_S 1000000000LL
57+
#define NS_PER_MS 1000000LL
58+
59+
/*
60+
** UUID version 7 uses 12 bits in "rand_a" to store 1/4096 (or 2^12) fractions of
61+
** sub-millisecond.
62+
*/
63+
#define SUBMS_BITS 12
64+
#define SUBMS_MINIMAL_STEP_NS ((NS_PER_MS / (1 << 12)) + 1)
65+
66+
static int64_t get_real_time_ns_ascending(void)
67+
{
68+
static int64_t last_time_ns = 0;
69+
struct timespec ts;
70+
int64_t now_ns;
71+
72+
clock_gettime(CLOCK_REALTIME, &ts);
73+
now_ns = (int64_t)ts.tv_sec * NS_PER_S + ts.tv_nsec;
74+
75+
/*
76+
* If the clock moved backwards or stalled, we must advance it to
77+
* maintain monotonicity as required by UUIDv7.
78+
*/
79+
if (now_ns <= last_time_ns)
80+
{
81+
now_ns = last_time_ns + SUBMS_MINIMAL_STEP_NS;
82+
}
83+
84+
last_time_ns = now_ns;
85+
return now_ns;
86+
}
87+
88+
static void generate_uuidv7(unsigned char *uuid_out)
89+
{
90+
int64_t current_ns;
91+
uint64_t unix_ts_ms;
92+
uint32_t sub_ms;
93+
94+
current_ns = get_real_time_ns_ascending();
95+
96+
unix_ts_ms = current_ns / NS_PER_MS;
97+
/* Calculate sub-millisecond fraction, scaled to 12 bits (4096) */
98+
sub_ms = (uint32_t)(((current_ns % NS_PER_MS) * 4096) / NS_PER_MS);
99+
100+
/* Fill with random data first */
101+
sqlite3_randomness(16, uuid_out);
102+
103+
/*
104+
** uuid[0-5]: unix_ts_ms (48 bits)
105+
** uuid[6]: ver (4 bits) | rand_a (4 bits from sub_ms)
106+
** uuid[7]: rand_a (8 bits from sub_ms)
107+
** uuid[8]: var (2 bits) | rand_b (6 bits)
108+
*/
109+
110+
/* Encode timestamp (big-endian) */
111+
uuid_out[0] = (unsigned char)(unix_ts_ms >> 40);
112+
uuid_out[1] = (unsigned char)(unix_ts_ms >> 32);
113+
uuid_out[2] = (unsigned char)(unix_ts_ms >> 24);
114+
uuid_out[3] = (unsigned char)(unix_ts_ms >> 16);
115+
uuid_out[4] = (unsigned char)(unix_ts_ms >> 8);
116+
uuid_out[5] = (unsigned char)(unix_ts_ms);
117+
118+
/* Version 7 and top 4 bits of sub_ms */
119+
uuid_out[6] = 0x70 | ((sub_ms >> 8) & 0x0F);
120+
121+
/* Lower 8 bits of sub_ms */
122+
uuid_out[7] = (unsigned char)(sub_ms & 0xFF);
123+
124+
/* Variant 1 (0b10xx) */
125+
uuid_out[8] = (uuid_out[8] & 0x3F) | 0x80;
126+
}
127+
128+
static void sqlite3Uuid7Func(
129+
sqlite3_context *context,
130+
int argc,
131+
sqlite3_value **argv
132+
){
133+
unsigned char aBlob[16];
134+
unsigned char zStr[37];
135+
(void)argc;
136+
(void)argv;
137+
138+
generate_uuidv7(aBlob);
139+
sqlite3UuidBlobToStr(aBlob, zStr);
140+
sqlite3_result_text(context, (char*)zStr, 36, SQLITE_TRANSIENT);
141+
}
142+
143+
#ifdef _WIN32
144+
__declspec(dllexport)
145+
#endif
146+
int sqlite3_uuid7_init(
147+
sqlite3 *db,
148+
char **pzErrMsg,
149+
const sqlite3_api_routines *pApi
150+
){
151+
int rc = SQLITE_OK;
152+
SQLITE_EXTENSION_INIT2(pApi);
153+
(void)pzErrMsg;
154+
rc = sqlite3_create_function_v2(db, "uuid7", 0, SQLITE_UTF8|SQLITE_INNOCUOUS, 0,
155+
sqlite3Uuid7Func, 0, 0, 0);
156+
return rc;
157+
}

src/lib.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,34 @@ pub use self::shim::WasmOsCallback;
4040
/// In-memory VFS implementation.
4141
pub use rsqlite_vfs::memvfs::{MemVfsError, MemVfsUtil};
4242

43-
#[cfg(feature = "uuid")]
43+
#[cfg(feature = "uuid4")]
4444
extern "C" {
45-
pub fn sqlite3_uuid_init(
45+
pub fn sqlite3_uuid4_init(
4646
db: *mut sqlite3,
4747
pzErrMsg: *mut *mut core::ffi::c_char,
4848
pApi: *const sqlite3_api_routines,
4949
) -> core::ffi::c_int;
5050
}
5151

52-
#[cfg(feature = "uuid")]
53-
pub fn register_uuid_extension() {
52+
#[cfg(feature = "uuid4")]
53+
pub fn register_uuid4_extension() {
5454
unsafe {
55-
sqlite3_auto_extension(Some(sqlite3_uuid_init));
55+
sqlite3_auto_extension(Some(sqlite3_uuid4_init));
56+
}
57+
}
58+
59+
#[cfg(feature = "uuid7")]
60+
extern "C" {
61+
pub fn sqlite3_uuid7_init(
62+
db: *mut sqlite3,
63+
pzErrMsg: *mut *mut core::ffi::c_char,
64+
pApi: *const sqlite3_api_routines,
65+
) -> core::ffi::c_int;
66+
}
67+
68+
#[cfg(feature = "uuid7")]
69+
pub fn register_uuid7_extension() {
70+
unsafe {
71+
sqlite3_auto_extension(Some(sqlite3_uuid7_init));
5672
}
5773
}

src/shim.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,55 @@ pub unsafe extern "C" fn sqlite3_os_end() -> core::ffi::c_int {
276276
crate::bindings::SQLITE_OK
277277
}
278278

279+
/// Polyfill for `clock_gettime` to support UUIDv7 generation.
280+
///
281+
/// This function provides a monotonic-ish time source based on `Date::now()` for the C
282+
/// `uuid7` extension. UUIDv7 requires a timestamp to ensure k-sortable IDs.
283+
///
284+
/// # Arguments
285+
/// * `_clock_id`: Ignored. We always use the JS `Date::now()` time.
286+
/// * `tp`: Pointer to `struct timespec`.
287+
///
288+
/// # Safety
289+
/// This function is unsafe because it dereferences raw pointers.
290+
/// It assumes `tp` points to a valid `struct timespec` compatible with the
291+
/// `musl` definition used during compilation of the C extensions.
292+
///
293+
/// # Implementation Details
294+
/// The assumed `struct timespec` layout for the target `wasm32-unknown-unknown` (via musl) is:
295+
/// ```c
296+
/// struct timespec {
297+
/// long long tv_sec; // 64-bit seconds
298+
/// long tv_nsec; // 32-bit nanoseconds
299+
/// };
300+
/// ```
301+
/// Note: Integers align to their size, so `tv_nsec` typically usually follows `tv_sec`
302+
/// immediately or with padding depending on architecture. Here we treat `tv_sec` as 64-bit
303+
/// aligned (offset 0) and `tv_nsec` as 32-bit.
304+
#[no_mangle]
305+
pub unsafe extern "C" fn rust_sqlite_wasm_clock_gettime(
306+
_clock_id: c_int,
307+
tp: *mut c_void,
308+
) -> c_int {
309+
let now = Date::now(); // ms
310+
let seconds = (now / 1000.0) as i64;
311+
let nanoseconds = ((now % 1000.0) * 1_000_000.0) as i32;
312+
313+
// struct timespec { time_t tv_sec; long tv_nsec; ... }
314+
// time_t is 64-bit on musl (generic), long is 32-bit.
315+
// Alignment of structure is 8 bytes.
316+
let tp = tp as *mut i64;
317+
*tp = seconds;
318+
319+
// Offset by 8 bytes for next field (64-bit integer takes 8 bytes)
320+
// We cast to *mut i32 to treat the memory after the 64-bit int as 32-bit ints.
321+
// Since `tp` is *mut i64, `tp.offset(1)` moves 8 bytes forward.
322+
let tp_nsec = tp.offset(1) as *mut i32;
323+
*tp_nsec = nanoseconds;
324+
325+
0
326+
}
327+
279328
#[cfg(test)]
280329
mod tests {
281330
use super::*;

tests/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ wasm-bindgen-test = "0.3.54"
1111

1212
[features]
1313
sqlite3mc = ["sqlite-wasm-rs/sqlite3mc"]
14-
uuid = ["sqlite-wasm-rs/uuid"]
14+
uuid4 = ["sqlite-wasm-rs/uuid4"]
15+
uuid7 = ["sqlite-wasm-rs/uuid7"]
1516

1617
[[test]]
1718
name = "integration_tests"

tests/tests/full/mod.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#[cfg(feature = "sqlite3mc")]
22
mod sqlite3mc;
3-
#[cfg(feature = "uuid")]
4-
mod uuid_verify;
3+
#[cfg(feature = "uuid4")]
4+
mod uuid4_verify;
5+
#[cfg(feature = "uuid7")]
6+
mod uuid7_verify;
57
mod vfs;
68

79
use sqlite_wasm_rs::*;
@@ -116,3 +118,65 @@ pub fn drop_or_create_foo_table(db: *mut sqlite3) -> bool {
116118

117119
true
118120
}
121+
122+
/// Helper to execute a SQL statement ignoring result rows.
123+
/// Panics if execution fails.
124+
///
125+
/// # Arguments
126+
/// * `db` - Pointer to the open SQLite database connection.
127+
/// * `sql` - The SQL statement to execute.
128+
pub fn exec(db: *mut sqlite3, sql: &str) {
129+
let sql_c = std::ffi::CString::new(sql).unwrap();
130+
let ret = unsafe {
131+
sqlite3_exec(
132+
db,
133+
sql_c.as_ptr().cast(),
134+
None,
135+
std::ptr::null_mut(),
136+
std::ptr::null_mut(),
137+
)
138+
};
139+
assert_eq!(ret, SQLITE_OK, "exec failed for: {}", sql);
140+
}
141+
142+
/// Helper to prepare a SQL statement.
143+
/// Returns the raw statement pointer.
144+
/// Panics if preparation fails.
145+
///
146+
/// # Arguments
147+
/// * `db` - Pointer to the open SQLite database connection.
148+
/// * `sql` - The SQL statement to prepare.
149+
pub fn prepare(db: *mut sqlite3, sql: &str) -> *mut sqlite3_stmt {
150+
let sql_c = std::ffi::CString::new(sql).unwrap();
151+
let mut stmt = std::ptr::null_mut();
152+
let ret = unsafe {
153+
sqlite3_prepare_v2(
154+
db,
155+
sql_c.as_ptr().cast(),
156+
-1,
157+
&mut stmt,
158+
std::ptr::null_mut(),
159+
)
160+
};
161+
assert_eq!(ret, SQLITE_OK, "prepare failed for: {}", sql);
162+
stmt
163+
}
164+
165+
/// Helper to read a text column from the current row of a statement.
166+
/// Returns an empty string if NULL.
167+
///
168+
/// # Arguments
169+
/// * `stmt` - Pointer to the prepared statement.
170+
/// * `col` - Zero-based index of the column to read.
171+
pub fn text_from_col(stmt: *mut sqlite3_stmt, col: i32) -> String {
172+
unsafe {
173+
let ptr = sqlite3_column_text(stmt, col);
174+
if ptr.is_null() {
175+
String::new()
176+
} else {
177+
std::ffi::CStr::from_ptr(ptr.cast())
178+
.to_string_lossy()
179+
.into_owned()
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)