Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **TEST-001: ZVecException isolation tests for error code strings, constructor, chaining, and error details** (#99)
- Added 4 unit test files for `ZVecException` class:
- `test_exception_error_code_string.phpt` — getErrorCodeString() mapping for codes 0-10, 99, and -1
- `test_exception_constructor.phpt` — parameter combinations (default, message, code, all params, partial details)
- `test_exception_chaining.phpt` — exception chaining with RuntimeException, ZVecException, custom Throwable, deep chaining
- `test_exception_error_details.phpt` — getErrorFile/Line/Function with boundary values, empty strings, unicode, chaining preservation
- Added 1 integration test:
- `test_exception_integration.phpt` — real FFI round-trip errors (INVALID_ARGUMENT, ALREADY_EXISTS, invalid filter, chaining)
- Added `examples/08_error_handling.php` — comprehensive error handling patterns demonstration
- Each test uses `try-finally` with `uniqid()` temp directory and cleanup
- Tests skip properly when native zvec extension is loaded (FFI-only methods)

- **DOC-001: Added class-level PHPDoc to all major classes for IDE tooling** (#60)
- Added class-level PHPDoc blocks to all 15 source files: ZVec, ZVecException, ZVecCollectionOptions, ZVecCollectionStats, ZVecFieldSchema, ZVecIndexParams, ZVecQueryInterface, ZVecVectorQuery, ZVecGroupByVectorQuery, ZVecSchema, ZVecDoc, ZVecRerankedDoc, ZVecRrfReRanker, ZVecWeightedReRanker, ZVecReRanker
- Each block follows a consistent format: one-line purpose, usage paragraph, and `@see` cross-references
Expand Down
125 changes: 125 additions & 0 deletions examples/08_error_handling.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

declare(strict_types=1);

/**
* Example 08: Error Handling with ZVecException
*
* Demonstrates proper error handling patterns:
* - Catching ZVecException from FFI operations
* - Using getErrorCodeString() for human-readable error codes
* - Accessing error details (file, line, function) with verbose errors
* - Exception chaining
* - try-finally cleanup pattern
*/

require_once __DIR__ . '/../src/ZVec.php';

// Enable verbose errors to get file/line/function in exceptions
ZVec::init(
logType: ZVec::LOG_CONSOLE,
logLevel: ZVec::LOG_WARN,
verboseErrors: true,
);

echo "=== ZVecException Error Handling Examples ===\n\n";

// 1. Basic exception catching
echo "1. Basic exception catching:\n";
try {
// Opening non-existent path triggers INVALID_ARGUMENT
$c = ZVec::open('/tmp/nonexistent_' . uniqid());
} catch (ZVecException $e) {
printf(
" Caught ZVecException: code=%d (%s), message=%s\n",
$e->getCode(),
$e->getErrorCodeString(),
$e->getMessage(),
);
}

echo "\n2. Error details (file, line, function):\n";
try {
$c = ZVec::open('/tmp/nonexistent_' . uniqid());
} catch (ZVecException $e) {
printf(
" File: %s\n Line: %d\n Function: %s\n",
$e->getErrorFile() ?? 'N/A',
$e->getErrorLine() ?? 0,
$e->getErrorFunction() ?? 'N/A',
);
}

echo "\n3. All error code strings:\n";
foreach ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99] as $code) {
$e = new ZVecException('test', $code);
printf(" Code %-2d → %s\n", $code, $e->getErrorCodeString());
}

echo "\n4. Exception chaining:\n";
try {
try {
// Inner error
throw new ZVecException('FFI call failed: zvec_collection_open', 3);
} catch (ZVecException $inner) {
// Wrap with more context
throw new ZVecException(
'Failed to open collection',
8,
previous: $inner,
errorFile: $inner->getErrorFile(),
errorLine: $inner->getErrorLine(),
errorFunction: $inner->getErrorFunction(),
);
}
} catch (ZVecException $outer) {
printf(
" Outer: code=%d (%s), message=%s\n",
$outer->getCode(),
$outer->getErrorCodeString(),
$outer->getMessage(),
);
if ($outer->getPrevious() !== null) {
/** @var ZVecException $prev */
$prev = $outer->getPrevious();
printf(
" Inner: code=%d (%s), message=%s\n",
$prev->getCode(),
$prev->getErrorCodeString(),
$prev->getMessage(),
);
}
}

echo "\n5. try-finally cleanup pattern with error tracking:\n";
$path = __DIR__ . '/../test_dbs/example_08_' . uniqid();
try {
$schema = new ZVecSchema('error_demo');
$schema->setMaxDocCountPerSegment(1000)
->addInt64('id', nullable: false)
->addVectorFp32('v', dimension: 4);

$c = ZVec::create($path, $schema);
echo " Collection created successfully\n";

$doc = new ZVecDoc('doc1');
$doc->setInt64('id', 1);
$doc->setVectorFp32('v', [0.1, 0.2, 0.3, 0.4]);
$c->insert($doc);
echo " Document inserted\n";

$c->close();
echo " Collection closed\n";
} catch (ZVecException $e) {
printf(
" ERROR: code=%d (%s) — %s\n",
$e->getCode(),
$e->getErrorCodeString(),
$e->getMessage(),
);
} finally {
exec("rm -rf " . escapeshellarg($path));
echo " Cleanup completed\n";
}

echo "\nAll examples completed.\n";
114 changes: 114 additions & 0 deletions tests/test_exception_chaining.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
--TEST--
ZVecException: exception chaining with previous Throwable
--SKIPIF--
<?php if (extension_loaded('zvec')) die('skip These methods are FFI-only, not available with native zvec extension'); ?>
<?php if (!extension_loaded('ffi')) die('skip FFI extension not available'); ?>
--FILE--
<?php
require_once __DIR__ . '/../src/ZVec.php';

// Test 1: Chain a RuntimeException
$prev = new RuntimeException('Database connection failed');
$e = new ZVecException('Query execution error', 8, $prev);

if ($e->getPrevious() !== $prev) {
echo "FAIL: Previous exception not set correctly\n";
exit(1);
}
if ($e->getPrevious()->getMessage() !== 'Database connection failed') {
echo "FAIL: Previous exception message mismatch\n";
exit(1);
}
echo "RuntimeException chaining OK\n";

// Test 2: Chain another ZVecException
$inner = new ZVecException('Inner FFI error', 3);
$outer = new ZVecException('Operation failed', 8, $inner);

if ($outer->getPrevious() !== $inner) {
echo "FAIL: Inner exception not set correctly\n";
exit(1);
}
if ($outer->getPrevious()->getCode() !== 3) {
echo "FAIL: Inner exception code mismatch\n";
exit(1);
}
echo "ZVecException chaining OK\n";

// Test 3: Chain a custom Throwable
$custom = new \Error('Custom fatal error');
$e = new ZVecException('Wrapper error', 5, $custom);

if ($e->getPrevious() !== $custom) {
echo "FAIL: Custom Throwable not set correctly\n";
exit(1);
}
if ($e->getPrevious()->getMessage() !== 'Custom fatal error') {
echo "FAIL: Custom Throwable message mismatch\n";
exit(1);
}
echo "Custom Throwable chaining OK\n";

// Test 4: No previous exception (default null)
$e = new ZVecException('Simple error', 1);
if ($e->getPrevious() !== null) {
echo "FAIL: Previous should be null when not set\n";
exit(1);
}
echo "No previous exception OK\n";

// Test 5: Deep chaining (3 levels)
$level3 = new ZVecException('Level 3: C-level error', 3);
$level2 = new ZVecException('Level 2: FFI wrapper error', 5, $level3);
$level1 = new ZVecException('Level 1: PHP operation failed', 8, $level2);

if ($level1->getPrevious() !== $level2) {
echo "FAIL: Level 1→2 chain broken\n";
exit(1);
}
if ($level1->getPrevious()->getPrevious() !== $level3) {
echo "FAIL: Level 2→3 chain broken\n";
exit(1);
}
if ($level1->getPrevious()->getPrevious()->getPrevious() !== null) {
echo "FAIL: Level 3 should have no previous\n";
exit(1);
}
echo "Deep chaining (3 levels) OK\n";

// Test 6: getCode() on chained exceptions
$inner = new ZVecException('Inner error', 3);
$outer = new ZVecException('Outer error', 8, $inner);

if ($outer->getCode() !== 8) {
echo "FAIL: Outer code should be 8\n";
exit(1);
}
if ($outer->getPrevious()->getCode() !== 3) {
echo "FAIL: Inner code should be 3\n";
exit(1);
}
echo "Chained exception codes OK\n";

// Test 7: getMessage() on chained exceptions
if ($outer->getMessage() !== 'Outer error') {
echo "FAIL: Outer message mismatch\n";
exit(1);
}
if ($outer->getPrevious()->getMessage() !== 'Inner error') {
echo "FAIL: Inner message mismatch\n";
exit(1);
}
echo "Chained exception messages OK\n";

echo "All exception chaining tests passed\n";
?>
--EXPECT--
RuntimeException chaining OK
ZVecException chaining OK
Custom Throwable chaining OK
No previous exception OK
Deep chaining (3 levels) OK
Chained exception codes OK
Chained exception messages OK
All exception chaining tests passed
Loading
Loading