mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-24 03:34:18 +01:00
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:
105
sqlite3/README.md
Normal file
105
sqlite3/README.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
39
sqlite3/tests/test-wal.c
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user