diff --git a/.github/workflows/flutter-package.yml b/.github/workflows/flutter-package.yml new file mode 100644 index 0000000..1573e72 --- /dev/null +++ b/.github/workflows/flutter-package.yml @@ -0,0 +1,84 @@ +name: publish flutter package +on: + push: + tags: + - '*.*.*' + +permissions: + contents: read + id-token: write + +jobs: + publish: + runs-on: ubuntu-22.04 + name: publish to pub.dev + + steps: + + - uses: actions/checkout@v4.2.2 + + - name: download release assets + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "VERSION=$VERSION" >> $GITHUB_ENV + + mkdir -p artifacts + cd artifacts + + # Download all platform binaries from the GitHub release + gh release download "$VERSION" --pattern "vector-*.tar.gz" + + # Extract all archives + for archive in vector-*.tar.gz; do + name=$(basename "$archive" "-$VERSION.tar.gz") + mkdir -p "$name" + tar -xzf "$archive" -C "$name" + rm "$archive" + done + + ls -la + env: + GH_TOKEN: ${{ github.token }} + + - uses: dart-lang/setup-dart@v1.7.1 + + - name: assemble and publish flutter package + run: | + FLUTTER_DIR=packages/flutter + + # Android + mkdir -p $FLUTTER_DIR/native_libraries/android + cp artifacts/vector-android-arm64-v8a/vector.so $FLUTTER_DIR/native_libraries/android/vector_android_arm64.so + cp artifacts/vector-android-armeabi-v7a/vector.so $FLUTTER_DIR/native_libraries/android/vector_android_arm.so + cp artifacts/vector-android-x86_64/vector.so $FLUTTER_DIR/native_libraries/android/vector_android_x64.so + + # iOS device + mkdir -p $FLUTTER_DIR/native_libraries/ios + cp artifacts/vector-ios/vector.dylib $FLUTTER_DIR/native_libraries/ios/vector_ios_arm64.dylib + + # iOS simulator (keep universal/fat binary as-is) + mkdir -p $FLUTTER_DIR/native_libraries/ios-sim + cp artifacts/vector-ios-sim/vector.dylib $FLUTTER_DIR/native_libraries/ios-sim/vector_ios-sim.dylib + + # macOS (separate arch-specific dylibs) + mkdir -p $FLUTTER_DIR/native_libraries/mac + cp artifacts/vector-macos-arm64/vector.dylib $FLUTTER_DIR/native_libraries/mac/vector_mac_arm64.dylib + cp artifacts/vector-macos-x86_64/vector.dylib $FLUTTER_DIR/native_libraries/mac/vector_mac_x64.dylib + + # Linux + mkdir -p $FLUTTER_DIR/native_libraries/linux + cp artifacts/vector-linux-x86_64/vector.so $FLUTTER_DIR/native_libraries/linux/vector_linux_x64.so + cp artifacts/vector-linux-arm64/vector.so $FLUTTER_DIR/native_libraries/linux/vector_linux_arm64.so + + # Windows + mkdir -p $FLUTTER_DIR/native_libraries/windows + cp artifacts/vector-windows-x86_64/vector.dll $FLUTTER_DIR/native_libraries/windows/vector_windows_x64.dll + + # Update version + sed -i "s/^version: .*/version: $VERSION/" $FLUTTER_DIR/pubspec.yaml + + # Publish to pub.dev + cd $FLUTTER_DIR + dart pub get + dart pub publish --dry-run + dart pub publish --force diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4384e4..4ef3ba0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,10 @@ name: Build, Test and Release on: push: + branches: + - '**' + tags-ignore: + - '**' workflow_dispatch: permissions: @@ -333,12 +337,9 @@ jobs: - uses: actions/setup-node@v4 if: steps.tag.outputs.version != '' with: - node-version: '20' + node-version: '24' registry-url: 'https://registry.npmjs.org' - - name: update npm # npm 11.5.1 is required for OIDC auth https://docs.npmjs.com/trusted-publishers - run: npm install -g npm@11.5.1 - - name: build and publish npm packages if: steps.tag.outputs.version != '' run: | @@ -395,9 +396,11 @@ jobs: - uses: softprops/action-gh-release@v2.2.1 if: steps.tag.outputs.version != '' with: + token: ${{ secrets.RELEASE_PAT }} body: | # Packages + [**Flutter/Dart**](https://pub.dev/packages/sqlite_vector): `flutter pub add sqlite_vector:${{ steps.tag.outputs.version }}` or `dart pub add sqlite_vector:${{ steps.tag.outputs.version }}` [**Node**](https://www.npmjs.com/package/@sqliteai/sqlite-vector): `npm install @sqliteai/sqlite-vector` [**WASM**](https://www.npmjs.com/package/@sqliteai/sqlite-wasm): `npm install @sqliteai/sqlite-wasm` [**Android**](https://central.sonatype.com/artifact/ai.sqlite/vector): `ai.sqlite:vector:${{ steps.tag.outputs.version }}` diff --git a/.gitignore b/.gitignore index a0f5427..f52516e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,10 @@ packages/node/test-platform-packages/ .DS_Store Thumbs.db CLAUDE.md + +# Dart/Flutter +.dart_tool/ +pubspec.lock +.flutter-plugins +.flutter-plugins-dependencies +packages/flutter/native_libraries/ \ No newline at end of file diff --git a/Makefile b/Makefile index 7ae5d70..2b56bec 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,11 @@ $(BUILD_DIR)/%.o: %.c test: $(TARGET) $(SQLITE3) ":memory:" -cmd ".bail on" ".load ./dist/vector" "SELECT vector_version();" +TEST_SRC = test/test_vector.c libs/sqlite3.c $(SRC_FILES) +unittest: + $(CC) $(CFLAGS) -DSQLITE_CORE -O2 $(TEST_SRC) -o $(BUILD_DIR)/test_vector -lm -lpthread + ./$(BUILD_DIR)/test_vector + # Clean up generated files clean: rm -rf $(BUILD_DIR)/* $(DIST_DIR)/* *.gcda *.gcno *.gcov *.sqlite @@ -229,4 +234,4 @@ help: @echo " xcframework - Build the Apple XCFramework" @echo " aar - Build the Android AAR package" -.PHONY: all clean test extension help version xcframework aar +.PHONY: all clean test unittest extension help version xcframework aar diff --git a/README.md b/README.md index f40b0c0..2cf9ef0 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,27 @@ pip install sqliteai-vector For usage details and examples, see the [Python package documentation](./packages/python/README.md). +### Flutter Package + +Add the [sqlite_vector](https://pub.dev/packages/sqlite_vector) package to your project: + +```bash +flutter pub add sqlite_vector # Flutter projects +dart pub add sqlite_vector # Dart projects +``` + +Usage with `sqlite3` package: +```dart +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_vector/sqlite_vector.dart'; + +sqlite3.loadSqliteVectorExtension(); +final db = sqlite3.openInMemory(); +print(db.select('SELECT vector_version()')); +``` + +For a complete example, see the [Flutter example](https://github.com/sqliteai/sqlite-extensions-guide/blob/main/examples/flutter/README.md). + ## Documentation Extensive API documentation can be found in the [API page](https://github.com/sqliteai/sqlite-vector/blob/main/API.md). diff --git a/examples/flutter/lib/main.dart b/examples/flutter/lib/main.dart new file mode 100644 index 0000000..3a07d4a --- /dev/null +++ b/examples/flutter/lib/main.dart @@ -0,0 +1,53 @@ +// Copyright (c) 2025 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE.md). + +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_vector/sqlite_vector.dart'; + +void main() { + // Load the sqlite-vector extension (call once at startup). + sqlite3.loadSqliteVectorExtension(); + + // Open an in-memory database. + final db = sqlite3.openInMemory(); + + // Verify the extension is loaded. + final version = db.select('SELECT vector_version()'); + print('sqlite-vector version: ${version.first.values.first}'); + + // Create a regular table with a BLOB column for vectors. + db.execute(''' + CREATE TABLE items ( + id INTEGER PRIMARY KEY, + embedding BLOB + ) + '''); + + // Insert sample Float32 vectors (4 dimensions). + final stmt = + db.prepare('INSERT INTO items (embedding) VALUES (vector_as_f32(?))'); + stmt.execute(['[1.0, 2.0, 3.0, 4.0]']); + stmt.execute(['[5.0, 6.0, 7.0, 8.0]']); + stmt.execute(['[1.1, 2.1, 3.1, 4.1]']); + stmt.dispose(); + + // Initialize the vector index. + db.execute( + "SELECT vector_init('items', 'embedding', 'type=FLOAT32,dimension=4')", + ); + + // Find the 2 nearest neighbors using vector_full_scan. + final results = db.select(''' + SELECT e.id, v.distance + FROM items AS e + JOIN vector_full_scan('items', 'embedding', vector_as_f32('[1.0, 2.0, 3.0, 4.0]'), 2) AS v + ON e.id = v.rowid + '''); + + print('Nearest neighbors:'); + for (final row in results) { + print(' id=${row['id']}, distance=${row['distance']}'); + } + + db.dispose(); +} diff --git a/examples/flutter/pubspec.yaml b/examples/flutter/pubspec.yaml new file mode 100644 index 0000000..79bfb78 --- /dev/null +++ b/examples/flutter/pubspec.yaml @@ -0,0 +1,11 @@ +name: sqlite_vector_example +description: Example app demonstrating sqlite_vector usage. +publish_to: none + +environment: + sdk: ^3.10.0 + +dependencies: + sqlite3: ^3.0.0 + sqlite_vector: + path: ../../packages/flutter diff --git a/packages/flutter/.pubignore b/packages/flutter/.pubignore new file mode 100644 index 0000000..36c5a16 --- /dev/null +++ b/packages/flutter/.pubignore @@ -0,0 +1,7 @@ +# Only ignore development files, NOT native_libraries +.dart_tool/ +pubspec.lock + +# Explicitly include native_libraries (override any parent .gitignore) +!native_libraries/ +!native_libraries/** diff --git a/packages/flutter/CHANGELOG.md b/packages/flutter/CHANGELOG.md new file mode 100644 index 0000000..c7a115f --- /dev/null +++ b/packages/flutter/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.9.84 + +- Initial release. +- Bundles pre-built sqlite-vector binaries for Android, iOS, macOS, Linux, and Windows. +- Provides `loadSqliteVectorExtension()` for use with `sqlite3` and `drift`. diff --git a/packages/flutter/LICENSE b/packages/flutter/LICENSE new file mode 120000 index 0000000..f0608a6 --- /dev/null +++ b/packages/flutter/LICENSE @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/packages/flutter/README.md b/packages/flutter/README.md new file mode 100644 index 0000000..44c3d0d --- /dev/null +++ b/packages/flutter/README.md @@ -0,0 +1,84 @@ +# sqlite_vector + +SQLite Vector is a cross-platform, ultra-efficient SQLite extension that brings vector search capabilities to your embedded database. It works seamlessly on iOS, Android, Windows, Linux, and macOS, using just 30MB of memory by default. With support for Float32, Float16, BFloat16, Int8, UInt8 and 1Bit, and highly optimized distance functions, it's the ideal solution for Edge AI applications. + +## Installation + +``` +dart pub add sqlite_vector +``` + +Requires Dart 3.10+ / Flutter 3.38+. + +## Usage + +### With `sqlite3` + +```dart +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_vector/sqlite_vector.dart'; + +void main() { + // Load once at startup. + sqlite3.loadSqliteVectorExtension(); + + final db = sqlite3.openInMemory(); + + // Create a regular table with a BLOB column for vectors. + db.execute('CREATE TABLE items (id INTEGER PRIMARY KEY, embedding BLOB)'); + + // Insert vectors. + final stmt = db.prepare('INSERT INTO items (embedding) VALUES (vector_as_f32(?))'); + stmt.execute(['[1.0, 2.0, 3.0, 4.0]']); + stmt.dispose(); + + // Initialize the vector index. + db.execute("SELECT vector_init('items', 'embedding', 'type=FLOAT32,dimension=4')"); + + // Find the 2 nearest neighbors. + final results = db.select(''' + SELECT e.id, v.distance FROM items AS e + JOIN vector_full_scan('items', 'embedding', vector_as_f32('[1.0, 2.0, 3.0, 4.0]'), 2) AS v + ON e.id = v.rowid + '''); + + db.dispose(); +} +``` + +### With `drift` + +```dart +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite_vector/sqlite_vector.dart'; +import 'package:drift/native.dart'; + +Sqlite3 loadExtensions() { + sqlite3.loadSqliteVectorExtension(); + return sqlite3; +} + +// Use when creating the database: +NativeDatabase.createInBackground( + File(path), + sqlite3: loadExtensions, +); +``` + +## Supported platforms + +| Platform | Architectures | +|----------|---------------| +| Android | arm64, arm, x64 | +| iOS | arm64 (device + simulator) | +| macOS | arm64, x64 | +| Linux | arm64, x64 | +| Windows | x64 | + +## API + +See the full [sqlite-vector API documentation](https://github.com/sqliteai/sqlite-vector/blob/main/API.md). + +## License + +See [LICENSE](LICENSE). diff --git a/packages/flutter/analysis_options.yaml b/packages/flutter/analysis_options.yaml new file mode 100644 index 0000000..572dd23 --- /dev/null +++ b/packages/flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml diff --git a/packages/flutter/example b/packages/flutter/example new file mode 120000 index 0000000..7a331dc --- /dev/null +++ b/packages/flutter/example @@ -0,0 +1 @@ +../../examples/flutter \ No newline at end of file diff --git a/packages/flutter/hook/build.dart b/packages/flutter/hook/build.dart new file mode 100644 index 0000000..7430ce3 --- /dev/null +++ b/packages/flutter/hook/build.dart @@ -0,0 +1,91 @@ +// Copyright (c) 2025 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE.md). + +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; +import 'package:path/path.dart' as p; + +void main(List args) async { + await build(args, (input, output) async { + if (!input.config.buildCodeAssets) return; + + final codeConfig = input.config.code; + final os = codeConfig.targetOS; + final arch = codeConfig.targetArchitecture; + + final binaryPath = _resolveBinaryPath(os, arch, codeConfig); + if (binaryPath == null) { + throw UnsupportedError( + 'sqlite_vector does not support $os $arch.', + ); + } + + final nativeLibDir = p.join( + input.packageRoot.toFilePath(), + 'native_libraries', + ); + final file = File(p.join(nativeLibDir, binaryPath)); + if (!file.existsSync()) { + throw StateError( + 'Pre-built binary not found: ${file.path}. ' + 'Run the CI pipeline to populate native_libraries/.', + ); + } + + output.assets.code.add( + CodeAsset( + package: input.packageName, + name: 'src/native/sqlite_vector_extension.dart', + linkMode: DynamicLoadingBundled(), + file: file.uri, + ), + ); + }); +} + +String? _resolveBinaryPath(OS os, Architecture arch, CodeConfig config) { + if (os == OS.android) { + return switch (arch) { + Architecture.arm64 => 'android/vector_android_arm64.so', + Architecture.arm => 'android/vector_android_arm.so', + Architecture.x64 => 'android/vector_android_x64.so', + _ => null, + }; + } + + if (os == OS.iOS) { + final sdk = config.iOS.targetSdk; + if (sdk == IOSSdk.iPhoneOS) { + return 'ios/vector_ios_arm64.dylib'; + } + // Simulator: fat binary (arm64 + x64) + return 'ios-sim/vector_ios-sim.dylib'; + } + + if (os == OS.macOS) { + return switch (arch) { + Architecture.arm64 => 'mac/vector_mac_arm64.dylib', + Architecture.x64 => 'mac/vector_mac_x64.dylib', + _ => null, + }; + } + + if (os == OS.linux) { + return switch (arch) { + Architecture.x64 => 'linux/vector_linux_x64.so', + Architecture.arm64 => 'linux/vector_linux_arm64.so', + _ => null, + }; + } + + if (os == OS.windows) { + return switch (arch) { + Architecture.x64 => 'windows/vector_windows_x64.dll', + _ => null, + }; + } + + return null; +} diff --git a/packages/flutter/lib/sqlite_vector.dart b/packages/flutter/lib/sqlite_vector.dart new file mode 100644 index 0000000..bb89890 --- /dev/null +++ b/packages/flutter/lib/sqlite_vector.dart @@ -0,0 +1,6 @@ +// Copyright (c) 2025 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE.md). + +library sqlite_vector; + +export 'src/sqlite_vector.dart'; diff --git a/packages/flutter/lib/src/sqlite_vector.dart b/packages/flutter/lib/src/sqlite_vector.dart new file mode 100644 index 0000000..99bef77 --- /dev/null +++ b/packages/flutter/lib/src/sqlite_vector.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2025 SQLite Cloud, Inc. +// Licensed under the Elastic License 2.0 (see LICENSE.md). + +import 'dart:ffi'; + +import 'package:sqlite3/sqlite3.dart'; + +// @Native resolves from the code asset declared in hook/build.dart. +// The asset ID is 'package:sqlite_vector/src/native/sqlite_vector_extension.dart'. +@Native, Pointer, Pointer)>( + assetId: 'package:sqlite_vector/src/native/sqlite_vector_extension.dart', +) +external int sqlite3_vector_init( + Pointer db, + Pointer pzErrMsg, + Pointer pApi, +); + +extension SqliteVectorExtension on Sqlite3 { + /// Loads the sqlite-vector extension. + /// + /// Call once at app startup. All subsequently opened databases + /// will have vector functions available. + /// + /// Works with both `sqlite3` package and `drift` ORM. + void loadSqliteVectorExtension() { + ensureExtensionLoaded( + SqliteExtension( + Native.addressOf< + NativeFunction< + Int Function(Pointer, Pointer, Pointer)>>( + sqlite3_vector_init, + ).cast(), + ), + ); + } +} diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml new file mode 100644 index 0000000..fc595f9 --- /dev/null +++ b/packages/flutter/pubspec.yaml @@ -0,0 +1,24 @@ +name: sqlite_vector +version: 0.9.84 +description: > + SQLite Vector is a cross-platform, ultra-efficient SQLite extension that + brings vector search capabilities to your embedded database. It works + seamlessly on iOS, Android, Windows, Linux, and macOS, using just 30MB of + memory by default. With support for Float32, Float16, BFloat16, Int8, UInt8 + and 1Bit, and highly optimized distance functions, it's the ideal solution + for Edge AI applications. +homepage: https://github.com/sqliteai/sqlite-vector +repository: https://github.com/sqliteai/sqlite-vector + +environment: + sdk: ^3.10.0 + +dependencies: + code_assets: ^1.0.0 + hooks: ^1.0.0 + path: ^1.8.0 + sqlite3: ^3.0.0 + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/packages/node/package.json b/packages/node/package.json index ab5fb53..4878d01 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -44,12 +44,12 @@ "license": "SEE LICENSE IN LICENSE.md", "repository": { "type": "git", - "url": "https://github.com/sqliteai/sqlite-vector.git", + "url": "git+https://github.com/sqliteai/sqlite-vector.git", "directory": "packages/node" }, "homepage": "https://github.com/sqliteai/sqlite-vector#readme", "bugs": { - "url": "https://github.com/sqliteai/sqlite-vector/issues" + "url": "git+https://github.com/sqliteai/sqlite-vector/issues" }, "engines": { "node": ">=16.0.0" diff --git a/src/sqlite-vector.h b/src/sqlite-vector.h index f512510..aca03f1 100644 --- a/src/sqlite-vector.h +++ b/src/sqlite-vector.h @@ -24,7 +24,7 @@ extern "C" { #endif -#define SQLITE_VECTOR_VERSION "0.9.80" +#define SQLITE_VECTOR_VERSION "0.9.84" SQLITE_VECTOR_API int sqlite3_vector_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi); diff --git a/test/test_vector.c b/test/test_vector.c new file mode 100644 index 0000000..9c47b34 --- /dev/null +++ b/test/test_vector.c @@ -0,0 +1,437 @@ +/* + * test_vector.c + * Comprehensive unit tests for the SQLite Vector extension. + * + * Compiled with -DSQLITE_CORE so sqlite3_vector_init links statically. + * Usage: gcc -DSQLITE_CORE ... -o test_vector && ./test_vector + */ + +#include +#include +#include +#include +#include "sqlite3.h" +#include "sqlite-vector.h" + +/* ---------- Test infrastructure ---------- */ + +static int failures = 0; +static int passes = 0; + +#define ASSERT(cond, msg) do { \ + if (!(cond)) { printf("FAIL: %s\n", msg); failures++; } \ + else { printf("PASS: %s\n", msg); passes++; } \ +} while (0) + +/* Execute SQL that must succeed; returns SQLITE_OK or aborts the test. */ +static int exec_sql(sqlite3 *db, const char *sql) { + char *err = NULL; + int rc = sqlite3_exec(db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + printf(" SQL error (%d): %s\n Statement: %s\n", rc, err ? err : "unknown", sql); + sqlite3_free(err); + } + return rc; +} + +/* ---------- Helper: create, populate, and init a vector table ---------- */ + +/* + * Sets up a table named `tbl` with columns (id INTEGER PRIMARY KEY, v BLOB), + * inserts `n` vectors of the given type converted from JSON via vector_as_(), + * and calls vector_init() with the specified type, distance, and dimension. + * + * `vecs` is an array of JSON strings, e.g. "[1.0, 2.0, 3.0]". + */ +static int setup_table(sqlite3 *db, const char *tbl, const char *type, + const char *distance, int dim, + const char **vecs, int n) { + char sql[2048]; + + /* Create table */ + snprintf(sql, sizeof(sql), "CREATE TABLE \"%s\" (id INTEGER PRIMARY KEY, v BLOB);", tbl); + if (exec_sql(db, sql) != SQLITE_OK) return -1; + + /* Insert vectors */ + for (int i = 0; i < n; i++) { + snprintf(sql, sizeof(sql), + "INSERT INTO \"%s\" (id, v) VALUES (%d, vector_as_%s('%s'));", + tbl, i + 1, type, vecs[i]); + if (exec_sql(db, sql) != SQLITE_OK) return -1; + } + + /* vector_init */ + snprintf(sql, sizeof(sql), + "SELECT vector_init('%s', 'v', 'type=%s,dimension=%d,distance=%s');", + tbl, type, dim, distance); + if (exec_sql(db, sql) != SQLITE_OK) return -1; + + return 0; +} + +/* ---------- Callback helpers for querying results ---------- */ + +typedef struct { + int count; + double distances[64]; +} scan_result; + +static int scan_cb(void *ctx, int ncols, char **vals, char **names) { + (void)names; + scan_result *r = (scan_result *)ctx; + if (r->count < 64 && ncols >= 2 && vals[1]) { + r->distances[r->count] = atof(vals[1]); + } + r->count++; + return 0; +} + +/* ---------- Test: basics ---------- */ + +static void test_basics(sqlite3 *db) { + printf("\n=== Basics ===\n"); + + /* vector_version() */ + { + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(db, "SELECT vector_version();", -1, &stmt, NULL); + ASSERT(rc == SQLITE_OK, "vector_version() prepares"); + if (rc == SQLITE_OK) { + rc = sqlite3_step(stmt); + ASSERT(rc == SQLITE_ROW, "vector_version() returns a row"); + const char *v = (const char *)sqlite3_column_text(stmt, 0); + ASSERT(v != NULL && strlen(v) > 0, "vector_version() returns non-empty text"); + } + sqlite3_finalize(stmt); + } + + /* vector_backend() */ + { + sqlite3_stmt *stmt; + int rc = sqlite3_prepare_v2(db, "SELECT vector_backend();", -1, &stmt, NULL); + ASSERT(rc == SQLITE_OK, "vector_backend() prepares"); + if (rc == SQLITE_OK) { + rc = sqlite3_step(stmt); + ASSERT(rc == SQLITE_ROW, "vector_backend() returns a row"); + const char *v = (const char *)sqlite3_column_text(stmt, 0); + ASSERT(v != NULL && strlen(v) > 0, "vector_backend() returns non-empty text"); + } + sqlite3_finalize(stmt); + } +} + +/* ---------- Test: vector_full_scan for a given (type, distance) pair ---------- */ + +static void test_full_scan(sqlite3 *db, const char *type, const char *distance, + int dim, const char **vecs, int nvecs, + const char *query_vec) { + char tbl[64], sql[1024], msg[256]; + snprintf(tbl, sizeof(tbl), "tfs_%s_%s", type, distance); + + /* lowercase table name for uniqueness */ + for (char *p = tbl; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; + + if (setup_table(db, tbl, type, distance, dim, vecs, nvecs) != 0) { + snprintf(msg, sizeof(msg), "full_scan setup %s/%s", type, distance); + ASSERT(0, msg); + return; + } + + /* DOT distance returns negative dot product, so skip non-negative checks */ + int is_dot = (strcasecmp(distance, "DOT") == 0); + + /* Top-k mode (k=3) */ + { + scan_result r = {0}; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_full_scan('%s', 'v', vector_as_%s('%s'), 3);", + tbl, type, query_vec); + char *err = NULL; + int rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + snprintf(msg, sizeof(msg), "full_scan top-k executes (%s/%s)", type, distance); + ASSERT(rc == SQLITE_OK, msg); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + + snprintf(msg, sizeof(msg), "full_scan top-k returns 3 rows (%s/%s)", type, distance); + ASSERT(r.count == 3, msg); + + /* Distances should be non-negative (DOT returns negative dot product, so skip) */ + if (!is_dot) { + int all_non_neg = 1; + for (int i = 0; i < r.count; i++) { + if (r.distances[i] < 0) all_non_neg = 0; + } + snprintf(msg, sizeof(msg), "full_scan top-k distances >= 0 (%s/%s)", type, distance); + ASSERT(all_non_neg, msg); + } + + /* Distances should be sorted ascending */ + int sorted = 1; + for (int i = 1; i < r.count; i++) { + if (r.distances[i] < r.distances[i - 1]) sorted = 0; + } + snprintf(msg, sizeof(msg), "full_scan top-k distances sorted (%s/%s)", type, distance); + ASSERT(sorted, msg); + } + + /* Streaming mode (no k, use LIMIT) */ + { + scan_result r = {0}; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_full_scan('%s', 'v', vector_as_%s('%s')) LIMIT 5;", + tbl, type, query_vec); + char *err = NULL; + int rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + snprintf(msg, sizeof(msg), "full_scan stream executes (%s/%s)", type, distance); + ASSERT(rc == SQLITE_OK, msg); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + + snprintf(msg, sizeof(msg), "full_scan stream returns rows (%s/%s)", type, distance); + ASSERT(r.count > 0, msg); + + if (!is_dot) { + int all_non_neg = 1; + for (int i = 0; i < r.count; i++) { + if (r.distances[i] < 0) all_non_neg = 0; + } + snprintf(msg, sizeof(msg), "full_scan stream distances >= 0 (%s/%s)", type, distance); + ASSERT(all_non_neg, msg); + } + } +} + +/* ---------- Test: vector_quantize_scan for a given (type, qtype) pair ---------- */ + +static void test_quantize_scan(sqlite3 *db, const char *type, const char *qtype, + int dim, const char **vecs, int nvecs, + const char *query_vec) { + char tbl[64], sql[1024], msg[256]; + snprintf(tbl, sizeof(tbl), "tqs_%s_%s", type, qtype); + + for (char *p = tbl; *p; p++) if (*p >= 'A' && *p <= 'Z') *p += 32; + + /* Use L2 distance (or HAMMING for BIT) */ + const char *distance = (strcasecmp(type, "BIT") == 0) ? "HAMMING" : "L2"; + + if (setup_table(db, tbl, type, distance, dim, vecs, nvecs) != 0) { + snprintf(msg, sizeof(msg), "quantize_scan setup %s/%s", type, qtype); + ASSERT(0, msg); + return; + } + + /* vector_quantize */ + snprintf(sql, sizeof(sql), + "SELECT vector_quantize('%s', 'v', 'qtype=%s');", tbl, qtype); + if (exec_sql(db, sql) != SQLITE_OK) { + snprintf(msg, sizeof(msg), "vector_quantize %s/%s", type, qtype); + ASSERT(0, msg); + return; + } + + /* Top-k mode */ + { + scan_result r = {0}; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_quantize_scan('%s', 'v', vector_as_%s('%s'), 3);", + tbl, type, query_vec); + char *err = NULL; + int rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + snprintf(msg, sizeof(msg), "quantize_scan top-k executes (%s/%s)", type, qtype); + ASSERT(rc == SQLITE_OK, msg); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + + snprintf(msg, sizeof(msg), "quantize_scan top-k returns rows (%s/%s)", type, qtype); + ASSERT(r.count > 0, msg); + } + + /* Streaming mode */ + { + scan_result r = {0}; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_quantize_scan('%s', 'v', vector_as_%s('%s')) LIMIT 5;", + tbl, type, query_vec); + char *err = NULL; + int rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + snprintf(msg, sizeof(msg), "quantize_scan stream executes (%s/%s)", type, qtype); + ASSERT(rc == SQLITE_OK, msg); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + + snprintf(msg, sizeof(msg), "quantize_scan stream returns rows (%s/%s)", type, qtype); + ASSERT(r.count > 0, msg); + } +} + +/* ---------- Test vectors ---------- */ + +/* 4-dimensional float vectors for numeric types */ +static const char *float_vecs[] = { + "[1.0, 0.0, 0.0, 0.0]", + "[0.0, 1.0, 0.0, 0.0]", + "[0.0, 0.0, 1.0, 0.0]", + "[0.0, 0.0, 0.0, 1.0]", + "[1.0, 1.0, 0.0, 0.0]", + "[0.0, 1.0, 1.0, 0.0]", + "[0.0, 0.0, 1.0, 1.0]", + "[1.0, 1.0, 1.0, 0.0]", + "[0.0, 1.0, 1.0, 1.0]", + "[1.0, 1.0, 1.0, 1.0]", +}; +static const int float_nvecs = 10; +static const char *float_query = "[0.5, 0.5, 0.5, 0.5]"; + +/* Integer vectors (0-255 range for U8, -128..127 for I8) */ +static const char *int_vecs[] = { + "[10, 0, 0, 0]", + "[0, 10, 0, 0]", + "[0, 0, 10, 0]", + "[0, 0, 0, 10]", + "[10, 10, 0, 0]", + "[0, 10, 10, 0]", + "[0, 0, 10, 10]", + "[10, 10, 10, 0]", + "[0, 10, 10, 10]", + "[10, 10, 10, 10]", +}; +static const int int_nvecs = 10; +static const char *int_query = "[5, 5, 5, 5]"; + +/* 8-dimensional BIT vectors (0 or 1 values) */ +static const char *bit_vecs[] = { + "[1, 0, 0, 0, 0, 0, 0, 0]", + "[0, 1, 0, 0, 0, 0, 0, 0]", + "[0, 0, 1, 0, 0, 0, 0, 0]", + "[0, 0, 0, 1, 0, 0, 0, 0]", + "[1, 1, 0, 0, 0, 0, 0, 0]", + "[0, 1, 1, 0, 0, 0, 0, 0]", + "[0, 0, 1, 1, 0, 0, 0, 0]", + "[1, 1, 1, 0, 0, 0, 0, 0]", + "[0, 1, 1, 1, 0, 0, 0, 0]", + "[1, 1, 1, 1, 0, 0, 0, 0]", +}; +static const int bit_nvecs = 10; +static const char *bit_query = "[1, 0, 1, 0, 1, 0, 1, 0]"; + +/* ---------- Main ---------- */ + +int main(void) { + sqlite3 *db; + int rc = sqlite3_open(":memory:", &db); + if (rc != SQLITE_OK) { + printf("FAIL: cannot open :memory: database\n"); + return 1; + } + + /* Initialize the vector extension */ + char *errmsg = NULL; + rc = sqlite3_vector_init(db, &errmsg, NULL); + if (rc != SQLITE_OK) { + printf("FAIL: sqlite3_vector_init returned %d: %s\n", rc, errmsg ? errmsg : ""); + sqlite3_close(db); + return 1; + } + + /* 1. Basics */ + test_basics(db); + + /* 2. vector_full_scan — float types × all distances */ + printf("\n=== vector_full_scan ===\n"); + { + const char *float_types[] = {"f32", "f16", "bf16"}; + const char *distances[] = {"L2", "SQUARED_L2", "COSINE", "DOT", "L1"}; + + for (int t = 0; t < 3; t++) { + for (int d = 0; d < 5; d++) { + test_full_scan(db, float_types[t], distances[d], + 4, float_vecs, float_nvecs, float_query); + } + } + + /* Integer types */ + const char *int_types[] = {"i8", "u8"}; + for (int t = 0; t < 2; t++) { + for (int d = 0; d < 5; d++) { + test_full_scan(db, int_types[t], distances[d], + 4, int_vecs, int_nvecs, int_query); + } + } + + /* BIT — only HAMMING */ + test_full_scan(db, "bit", "HAMMING", 8, bit_vecs, bit_nvecs, bit_query); + } + + /* 3. vector_quantize_scan — all vector types × quantization types */ + printf("\n=== vector_quantize_scan ===\n"); + { + const char *qtypes[] = {"UINT8", "INT8", "1BIT"}; + + /* Float types */ + const char *float_types[] = {"f32", "f16", "bf16"}; + for (int t = 0; t < 3; t++) { + for (int q = 0; q < 3; q++) { + test_quantize_scan(db, float_types[t], qtypes[q], + 4, float_vecs, float_nvecs, float_query); + } + } + + /* Integer types */ + const char *int_types[] = {"i8", "u8"}; + for (int t = 0; t < 2; t++) { + for (int q = 0; q < 3; q++) { + test_quantize_scan(db, int_types[t], qtypes[q], + 4, int_vecs, int_nvecs, int_query); + } + } + + /* BIT — quantize with 1BIT */ + test_quantize_scan(db, "bit", "1BIT", 8, bit_vecs, bit_nvecs, bit_query); + } + + /* 4. Backward-compat aliases */ + printf("\n=== Backward-compat aliases ===\n"); + { + /* Set up a table for alias tests */ + const char *tbl = "tfs_alias"; + if (setup_table(db, tbl, "f32", "L2", 4, float_vecs, float_nvecs) == 0) { + /* vector_full_scan_stream */ + { + scan_result r = {0}; + char sql[512]; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_full_scan_stream('%s', 'v', vector_as_f32('%s')) LIMIT 3;", + tbl, float_query); + char *err = NULL; + rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + ASSERT(rc == SQLITE_OK, "vector_full_scan_stream alias works"); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + ASSERT(r.count > 0, "vector_full_scan_stream returns rows"); + } + + /* vector_quantize_scan_stream */ + { + char sql[512]; + snprintf(sql, sizeof(sql), + "SELECT vector_quantize('%s', 'v');", tbl); + exec_sql(db, sql); + + scan_result r = {0}; + snprintf(sql, sizeof(sql), + "SELECT id, distance FROM vector_quantize_scan_stream('%s', 'v', vector_as_f32('%s')) LIMIT 3;", + tbl, float_query); + char *err = NULL; + rc = sqlite3_exec(db, sql, scan_cb, &r, &err); + ASSERT(rc == SQLITE_OK, "vector_quantize_scan_stream alias works"); + if (err) { printf(" err: %s\n", err); sqlite3_free(err); } + ASSERT(r.count > 0, "vector_quantize_scan_stream returns rows"); + } + } + } + + sqlite3_close(db); + + /* Summary */ + printf("\n========================================\n"); + printf("Results: %d passed, %d failed\n", passes, failures); + printf("========================================\n"); + + return failures > 0 ? 1 : 0; +}