Merge 'sqlite3: Add Rust tests for SQLite3 API implementation' from Karan Janthe

This PR introduces test for SQLite3 API implementation, adding both unit
tests in Rust and maintaining existing C compatibility tests.
## Changes
- Added unit tests for core SQLite3 operations in `lib.rs`:
  - Database initialization and shutdown
  - Memory database operations
  - Error code handling and messages
  - Statement preparation and execution
  - Version information verification

Closes #1108
This commit is contained in:
Pekka Enberg
2025-03-08 09:03:18 +02:00
6 changed files with 361 additions and 30 deletions

105
sqlite3/README.md Normal file
View File

@@ -0,0 +1,105 @@
# SQLite3 Implementation for Limbo
This directory contains a Rust implementation of the SQLite3 C API. The implementation serves as a compatibility layer between SQLite's C API and Limbo's native Rust database implementation.
## Purpose
This implementation provides SQLite3 API compatibility for Limbo, allowing existing applications that use SQLite to work with Limbo without modification. The code:
1. Implements the SQLite3 C API functions in Rust
2. Translates between C and Rust data structures
3. Maps SQLite operations to equivalent Limbo operations
4. Maintains API compatibility with SQLite version 3.42.0
## Testing Strategy
We employ a dual-testing approach to ensure complete compatibility with SQLite:
### Test Database Setup
Before running tests, you need to set up a test database:
```bash
# Create testing directory
mkdir -p ../../testing
# Create and initialize test database
sqlite3 ../../testing/testing.db ".databases"
```
This creates an empty SQLite database that both test suites will use.
### 1. C Test Suite (`/tests`)
- Written in C to test the exact same API that real applications use
- Can be compiled and run against both:
- Official SQLite library (for verification)
- Our Rust implementation (for validation)
- Serves as the "source of truth" for correct behavior
To run C tests against official SQLite:
```bash
cd tests
make clean
make LIBS="-lsqlite3"
./sqlite3-tests
```
To run C tests against our implementation:
```bash
cd tests
make clean
make LIBS="-L../target/debug -lsqlite3"
./sqlite3-tests
```
### 2. Rust Tests (`src/lib.rs`)
- Unit tests written in Rust
- Test the same functionality as C tests
- Provide better debugging capabilities
- Help with development and implementation
To run Rust tests:
```bash
cargo test
```
### Why Two Test Suites?
1. **Behavior Verification**: C tests ensure our implementation matches SQLite's behavior exactly by running the same tests against both
2. **Development Efficiency**: Rust tests provide better debugging and development experience
3. **Complete Coverage**: Both test suites together provide comprehensive testing from both C and Rust perspectives
### Common Test Issues
1. **Missing Test Database**
- Error: `SQLITE_CANTOPEN (14)` in tests
- Solution: Create test database as shown in "Test Database Setup"
2. **Wrong Database Path**
- Tests expect database at `../../testing/testing.db`
- Verify path relative to where tests are run
3. **Permission Issues**
- Ensure test database is readable/writable
- Check directory permissions
## Implementation Notes
- All public functions are marked with `#[no_mangle]` and follow SQLite's C API naming convention
- Uses `unsafe` blocks for C API compatibility
- Implements error handling similar to SQLite
- Maintains thread safety guarantees of SQLite
## Contributing
When adding new features or fixing bugs:
1. Add C tests that can run against both implementations
2. Add corresponding Rust tests
3. Verify behavior matches SQLite by running C tests against both implementations
4. Ensure all existing tests pass in both suites
5. Make sure test database exists and is accessible
## Status
This is an ongoing implementation. Some functions are marked with `stub!()` macro, indicating they're not yet implemented. Check individual function documentation for implementation status.

View File

@@ -31,6 +31,12 @@
#define SQLITE_STATE_BUSY 109
/* WAL Checkpoint modes */
#define SQLITE_CHECKPOINT_PASSIVE 0
#define SQLITE_CHECKPOINT_FULL 1
#define SQLITE_CHECKPOINT_RESTART 2
#define SQLITE_CHECKPOINT_TRUNCATE 3
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;
@@ -244,6 +250,17 @@ const char *sqlite3_libversion(void);
int sqlite3_libversion_number(void);
/* WAL Checkpoint functions */
int sqlite3_wal_checkpoint(sqlite3 *db, const char *db_name);
int sqlite3_wal_checkpoint_v2(
sqlite3 *db,
const char *db_name,
int mode,
int *log_size,
int *checkpoint_count
);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus

View File

@@ -113,7 +113,7 @@ pub unsafe extern "C" fn sqlite3_open(
":memory:" => Arc::new(limbo_core::MemoryIO::new()),
_ => match limbo_core::PlatformIO::new() {
Ok(io) => Arc::new(io),
Err(_) => return SQLITE_MISUSE,
Err(_) => return SQLITE_CANTOPEN,
},
};
match limbo_core::Database::open_file(io, filename, false) {
@@ -1079,3 +1079,174 @@ pub unsafe extern "C" fn sqlite3_wal_checkpoint_v2(
}
SQLITE_OK
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr;
#[test]
fn test_libversion() {
unsafe {
let version = sqlite3_libversion();
assert!(!version.is_null());
}
}
#[test]
fn test_libversion_number() {
unsafe {
let version_num = sqlite3_libversion_number();
assert_eq!(version_num, 3042000);
}
}
#[test]
fn test_open_misuse() {
unsafe {
let mut db = ptr::null_mut();
assert_eq!(sqlite3_open(ptr::null(), &mut db), SQLITE_MISUSE);
}
}
#[test]
fn test_open_not_found() {
unsafe {
let mut db = ptr::null_mut();
assert_eq!(
sqlite3_open(b"not-found/local.db\0".as_ptr() as *const i8, &mut db),
SQLITE_CANTOPEN
);
}
}
#[test]
fn test_open_existing() {
unsafe {
let mut db = ptr::null_mut();
assert_eq!(
sqlite3_open(b"../../testing/testing.db\0".as_ptr() as *const i8, &mut db),
SQLITE_OK
);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_close() {
unsafe {
assert_eq!(sqlite3_close(ptr::null_mut()), SQLITE_OK);
}
}
#[test]
fn test_prepare_misuse() {
unsafe {
let mut db = ptr::null_mut();
assert_eq!(
sqlite3_open(b"../../testing/testing.db\0".as_ptr() as *const i8, &mut db),
SQLITE_OK
);
let mut stmt = ptr::null_mut();
assert_eq!(
sqlite3_prepare_v2(db, b"SELECT 1\0".as_ptr() as *const i8, -1, &mut stmt, ptr::null_mut()),
SQLITE_OK
);
assert_eq!(sqlite3_finalize(stmt), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_wal_checkpoint() {
unsafe {
// Test with NULL db handle
assert_eq!(sqlite3_wal_checkpoint(ptr::null_mut(), ptr::null()), SQLITE_MISUSE);
// Test with valid db
let mut db = ptr::null_mut();
assert_eq!(
sqlite3_open(b"../../testing/testing.db\0".as_ptr() as *const i8, &mut db),
SQLITE_OK
);
assert_eq!(sqlite3_wal_checkpoint(db, ptr::null()), SQLITE_OK);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
#[test]
fn test_wal_checkpoint_v2() {
unsafe {
// Test with NULL db handle
assert_eq!(
sqlite3_wal_checkpoint_v2(
ptr::null_mut(),
ptr::null(),
SQLITE_CHECKPOINT_PASSIVE,
ptr::null_mut(),
ptr::null_mut()
),
SQLITE_MISUSE
);
// Test with valid db
let mut db = ptr::null_mut();
assert_eq!(
sqlite3_open(b"../../testing/testing.db\0".as_ptr() as *const i8, &mut db),
SQLITE_OK
);
let mut log_size = 0;
let mut checkpoint_count = 0;
// Test different checkpoint modes
assert_eq!(
sqlite3_wal_checkpoint_v2(
db,
ptr::null(),
SQLITE_CHECKPOINT_PASSIVE,
&mut log_size,
&mut checkpoint_count
),
SQLITE_OK
);
assert_eq!(
sqlite3_wal_checkpoint_v2(
db,
ptr::null(),
SQLITE_CHECKPOINT_FULL,
&mut log_size,
&mut checkpoint_count
),
SQLITE_OK
);
assert_eq!(
sqlite3_wal_checkpoint_v2(
db,
ptr::null(),
SQLITE_CHECKPOINT_RESTART,
&mut log_size,
&mut checkpoint_count
),
SQLITE_OK
);
assert_eq!(
sqlite3_wal_checkpoint_v2(
db,
ptr::null(),
SQLITE_CHECKPOINT_TRUNCATE,
&mut log_size,
&mut checkpoint_count
),
SQLITE_OK
);
assert_eq!(sqlite3_close(db), SQLITE_OK);
}
}
}

View File

@@ -1,44 +1,39 @@
V =
ifeq ($(strip $(V)),)
E = @echo
Q = @
else
E = @\#
Q =
endif
export E Q
PROGRAM = sqlite3-tests
CFLAGS = -g -Wall -std=c17 -MMD -MP
# Compiler settings
CC = gcc
CFLAGS = -g -Wall -std=c17 -I../include
# Libraries
LIBS ?= -lsqlite3
LIBS += -lm
OBJS += main.o
OBJS += test-aux.o
OBJS += test-close.o
OBJS += test-open.o
OBJS += test-prepare.o
# Target program
PROGRAM = sqlite3-tests
# Object files
OBJS = main.o \
test-aux.o \
test-close.o \
test-open.o \
test-prepare.o \
test-wal.o
# Default target
all: $(PROGRAM)
# Test target
test: $(PROGRAM)
$(E) " TEST"
$(Q) $(CURDIR)/$(PROGRAM)
./$(PROGRAM)
# Compile source files
%.o: %.c
$(E) " CC " $@
$(Q) $(CC) $(CFLAGS) -c $< -o $@ -I$(HEADERS)
$(CC) $(CFLAGS) -c $< -o $@
# Link program
$(PROGRAM): $(OBJS)
$(E) " LINK " $@
$(Q) $(CC) -o $@ $^ $(LIBS)
$(CC) -o $@ $(OBJS) $(LIBS)
# Clean target
clean:
$(E) " CLEAN"
$(Q) rm -f $(PROGRAM)
$(Q) rm -f $(OBJS) *.d
.PHONY: clean
rm -f $(PROGRAM) $(OBJS)
-include $(OBJS:.o=.d)
.PHONY: all test clean

View File

@@ -5,6 +5,8 @@ extern void test_open_not_found();
extern void test_open_existing();
extern void test_close();
extern void test_prepare_misuse();
extern void test_wal_checkpoint();
extern void test_wal_checkpoint_v2();
int main(int argc, char *argv[])
{
@@ -15,6 +17,8 @@ int main(int argc, char *argv[])
test_open_existing();
test_close();
test_prepare_misuse();
test_wal_checkpoint();
test_wal_checkpoint_v2();
return 0;
}

39
sqlite3/tests/test-wal.c Normal file
View File

@@ -0,0 +1,39 @@
#include "check.h"
#include <sqlite3.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
void test_wal_checkpoint(void)
{
sqlite3 *db;
// Test with NULL db handle
CHECK_EQUAL(SQLITE_MISUSE, sqlite3_wal_checkpoint(NULL, NULL));
// Test with valid db
CHECK_EQUAL(SQLITE_OK, sqlite3_open("../../testing/testing.db", &db));
CHECK_EQUAL(SQLITE_OK, sqlite3_wal_checkpoint(db, NULL));
CHECK_EQUAL(SQLITE_OK, sqlite3_close(db));
}
void test_wal_checkpoint_v2(void)
{
sqlite3 *db;
int log_size, checkpoint_count;
// Test with NULL db handle
CHECK_EQUAL(SQLITE_MISUSE, sqlite3_wal_checkpoint_v2(NULL, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL));
// Test with valid db
CHECK_EQUAL(SQLITE_OK, sqlite3_open("../../testing/testing.db", &db));
// Test different checkpoint modes
CHECK_EQUAL(SQLITE_OK, sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_PASSIVE, &log_size, &checkpoint_count));
CHECK_EQUAL(SQLITE_OK, sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_FULL, &log_size, &checkpoint_count));
CHECK_EQUAL(SQLITE_OK, sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_RESTART, &log_size, &checkpoint_count));
CHECK_EQUAL(SQLITE_OK, sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, &log_size, &checkpoint_count));
CHECK_EQUAL(SQLITE_OK, sqlite3_close(db));
}