Releases: orhanayd/noneDB
v3.1.0
v3.1.0 (2025-12-29)
Major: Spatial Indexing + MongoDB-Style Comparison Operators
This release introduces R-tree spatial indexing for geospatial queries and MongoDB-style comparison operators for advanced filtering.
Part 1: R-tree Spatial Indexing
GeoJSON Support
Full GeoJSON geometry support:
- Point - Single location
[lon, lat] - LineString - Path/route
- Polygon - Area with boundary (supports holes)
- MultiPoint, MultiLineString, MultiPolygon
- GeometryCollection
// Insert GeoJSON data
$db->insert("restaurants", [
'name' => 'Ottoman Kitchen',
'location' => [
'type' => 'Point',
'coordinates' => [28.9784, 41.0082] // [longitude, latitude]
]
]);Spatial Index Management
// Create R-tree index
$db->createSpatialIndex("restaurants", "location");
// Returns: ["success" => true, "indexed" => 150]
// Check if index exists
$db->hasSpatialIndex("restaurants", "location"); // true/false
// List all spatial indexes
$db->getSpatialIndexes("restaurants"); // ["location"]
// Drop index
$db->dropSpatialIndex("restaurants", "location");
// Rebuild index
$db->rebuildSpatialIndex("restaurants", "location");Spatial Query Methods
All distance parameters and results are in meters.
// Find within radius
$nearby = $db->withinDistance("restaurants", "location", 28.97, 41.00, 5000); // 5000 meters
// Find in bounding box
$inArea = $db->withinBBox("restaurants", "location", 28.97, 41.00, 29.00, 41.03);
// Find K nearest
$closest = $db->nearest("restaurants", "location", 28.97, 41.00, 10);
// Find within polygon
$inPolygon = $db->withinPolygon("restaurants", "location", $polygon);Query Builder Integration
$results = $db->query("restaurants")
->withinDistance('location', 28.97, 41.00, 5000) // 5000 meters
->where(['open_now' => true])
->withDistance('location', 28.97, 41.00) // _distance field in meters
->sort('_distance', 'asc')
->limit(10)
->get();Spatial Index File Structure
hash-dbname.nonedb.sidx.location # R-tree spatial index
hash-dbname.nonedb.gsidx.location # Global spatial index (sharded)
Part 2: MongoDB-Style Comparison Operators
Operator Reference
| Operator | Description | Example |
|---|---|---|
$gt |
Greater than | ['age' => ['$gt' => 18]] |
$gte |
Greater than or equal | ['price' => ['$gte' => 100]] |
$lt |
Less than | ['stock' => ['$lt' => 10]] |
$lte |
Less than or equal | ['rating' => ['$lte' => 5]] |
$eq |
Equal (explicit) | ['status' => ['$eq' => 'active']] |
$ne |
Not equal | ['role' => ['$ne' => 'guest']] |
$in |
Value in array | ['category' => ['$in' => ['a', 'b']]] |
$nin |
Value not in array | ['tag' => ['$nin' => ['spam']]] |
$exists |
Field exists | ['email' => ['$exists' => true]] |
$like |
Pattern match | ['name' => ['$like' => '^John']] |
$regex |
Regular expression | ['email' => ['$regex' => '@gmail.com$']] |
$contains |
Array/string contains | ['tags' => ['$contains' => 'featured']] |
Usage Examples
// Range query
$results = $db->query("products")
->where([
'price' => ['$gte' => 100, '$lte' => 500],
'stock' => ['$gt' => 0]
])
->get();
// Multiple operators
$results = $db->query("users")
->where([
'role' => ['$in' => ['admin', 'moderator']],
'status' => ['$ne' => 'banned'],
'email' => ['$exists' => true]
])
->get();
// Combined with spatial queries
$results = $db->query("restaurants")
->withinDistance('location', 28.97, 41.00, 5000) // 5000 meters
->where([
'rating' => ['$gte' => 4.0],
'cuisine' => ['$in' => ['turkish', 'italian']]
])
->get();Part 3: R-tree Performance Optimizations
| Optimization | Description |
|---|---|
| Parent Pointer Map | O(1) parent lookup instead of O(n) tree scan |
| Linear Split Algorithm | O(n) seed selection instead of O(n²) quadratic split |
| Dirty Flag Pattern | Single disk write per batch instead of n writes |
| Distance Memoization | Cached Haversine distance calculations |
| Centroid Caching | Cached geometry centroid calculations |
| Node Size 32 | Fewer tree levels and splits (increased from 16) |
| Adaptive nearest() | Exponential radius expansion for efficient k-NN |
Performance Results
| Operation | 100 | 1K | 5K |
|---|---|---|---|
| createSpatialIndex | 2.4 ms | 81 ms | 423 ms |
| withinDistance (10km) | 3 ms | 32 ms | 166 ms |
| withinBBox | 0.7 ms | 7 ms | 38 ms |
| nearest(10) | 2 ms | 2 ms | 2.4 ms |
Comparison Operator Performance
| Operator | 100 | 1K | 5K |
|---|---|---|---|
$gt, $gte, $lt, $lte |
0.7 ms | 6-7 ms | 33-38 ms |
$in, $nin |
0.7 ms | 6.5-7 ms | 36-37 ms |
$like, $regex |
0.7 ms | 7 ms | 39 ms |
| Complex (4 operators) | 0.7 ms | 7.5 ms | 43 ms |
New Methods
Spatial Index Methods (noneDB)
createSpatialIndex($dbname, $field)- Create R-tree indexhasSpatialIndex($dbname, $field)- Check if index existsgetSpatialIndexes($dbname)- List all spatial indexesdropSpatialIndex($dbname, $field)- Remove indexrebuildSpatialIndex($dbname, $field)- Rebuild indexwithinDistance($dbname, $field, $lon, $lat, $meters)- Find within radius (meters)withinBBox($dbname, $field, $minLon, $minLat, $maxLon, $maxLat)- Find in bboxnearest($dbname, $field, $lon, $lat, $k)- Find K nearestwithinPolygon($dbname, $field, $polygon)- Find in polygonvalidateGeoJSON($geometry)- Validate GeoJSON
Query Builder Methods (noneDBQuery)
withinDistance($field, $lon, $lat, $meters)- Spatial: within radius (meters)withinBBox($field, $minLon, $minLat, $maxLon, $maxLat)- Spatial: within bboxnearest($field, $lon, $lat, $k)- Spatial: K nearestwithinPolygon($field, $polygon)- Spatial: within polygonwithDistance($field, $lon, $lat)- Add_distancefield to results (meters)
Comparison Operators in where()
$gt,$gte,$lt,$lte- Numeric comparisons$eq,$ne- Equality comparisons$in,$nin- Array membership$exists- Field existence$like- Pattern matching (case-insensitive)$regex- Regular expression matching$contains- Array/string contains
Test Results
- 970 tests, 3079 assertions (all passing)
- 42 new comparison operator tests
- 24 new spatial + operator combination tests
- 48 new query documentation tests
- Full sharded spatial index support verified
Documentation
New documentation files in docs/:
QUERY.md- Complete query builder referenceSPATIAL.md- Spatial indexing guideCONFIGURATION.md- Configuration optionsAPI.md- Complete API referenceBENCHMARKS.md- Performance benchmarks
Breaking Changes
None. Spatial indexing is a new feature in v3.1.0.
Note: All spatial distance parameters and results use meters as the unit:
withinDistance()- distance parameter in meters_distancefield in results is in metersnearest()maxDistanceoption is in meters
v3.0.0
What's Changed
noneDB v3.0.0 - Pure JSONL Storage Engine
Summary
Complete storage engine rewrite from JSON array to JSONL with byte-offset indexing. This release achieves O(1) key lookups and massive performance improvements across all operations.
Key Changes
🚀 Performance
| Operation | v2.x → v3.0 | vs SleekDB |
|---|---|---|
| find(key) | O(n) → O(1) | - |
| count() | 536ms → <1ms | 258x faster |
| insert 100K | 2.8s → 1.6s | 9x faster |
| find(filter) 100K | 854ms → 434ms | 79x faster |
💾 New Storage Format
Before: {"data": [{...}, null, {...}]} # JSON array with nulls
After: {"key":0,...}\n{"key":1,...} # JSONL + .jidx index
✨ New Features
- Static cache sharing - 80%+ faster for multi-instance
- Auto-compaction - Triggers at 30% deleted records
- External config file (.nonedb) - Secrets out of source code
- Dev mode - Skip config for development
⚠️ Breaking Changes
- V2 format auto-migrated on first access
.jidxindex file now required- Config file or programmatic config required (or dev mode)
Backwards Compatibility
✅ Tested: v2.2.0 databases auto-migrate to v3.0.0 format seamlessly.
Test Results
- 774 tests, 2157 assertions (all passing)
- Concurrent access verified
- Sharding verified up to 500K records
Full Changelog: v2.2.0...v3.0.0
v2.2.0
v2.2.0 (2025-12-27)
Major: Atomic File Locking
This release implements professional-grade atomic file locking to ensure thread-safe concurrent access. No more lost updates or race conditions!
New Core Methods
Three new private methods handle all file operations atomically:
// Atomic read with shared lock (LOCK_SH)
private function atomicRead($path, $default = null)
// Atomic write with exclusive lock (LOCK_EX)
private function atomicWrite($path, $data, $prettyPrint = false)
// Atomic read-modify-write in single locked operation
private function atomicModify($path, callable $modifier, $default = null)How It Works
┌───────────────────────────────────────────────────────────┐
│ Before v2.2 (Race Condition) │
├───────────────────────────────────────────────────────────┤
│ Process A: read → modify → write │
│ Process B: read → modify → write │
│ Result: Process A's changes LOST! │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ After v2.2 (Atomic Operations) │
├───────────────────────────────────────────────────────────┤
│ Process A: [LOCK → read → modify → write → UNLOCK] │
│ Process B: [wait...] [LOCK → read → modify → ...]│
│ Result: ALL changes preserved! │
└───────────────────────────────────────────────────────────┘
Test Results
| Scenario | Before v2.2 | After v2.2 |
|---|---|---|
| 2 processes × 100 inserts | 46-199 records (up to 77% loss) | 200/200 (0% loss) |
| 5 processes × 50 inserts | 41 records (83.6% loss!) | 250/250 (0% loss) |
Updated Methods
All write operations now use atomic locking:
insert(),update(),delete()insertSharded(),updateSharded(),deleteSharded()readMeta(),writeMeta(),getShardData(),writeShardData()
Configuration
New configuration options for fine-tuning:
private $lockTimeout = 5; // Max seconds to wait for lock
private $lockRetryDelay = 10000; // Microseconds between retry attemptsPerformance Benchmarks Updated
Added 500K record benchmarks. Key highlights:
find(key)stays at 23ms even at 500K records (thanks to sharding)- Full table operations scale linearly (~3-5s for 500K records)
v2.1.0
New Features
Advanced Filter Methods
New chainable filter methods added:
// OR condition
$db->query("users")
->where(["department" => "IT"])
->orWhere(["department" => "Engineering"])
->orWhere(["role" => "admin"])
->get();
// Array membership
$db->query("users")
->whereIn("status", ["active", "pending"])
->whereNotIn("role", ["banned", "suspended"])
->get();
// Negation filters
$db->query("users")
->whereNot(["deleted" => true])
->notLike("email", "test")
->notBetween("age", 0, 18)
->get();New Methods:
orWhere($filters)- OR condition filteringwhereIn($field, $values)- Value in arraywhereNotIn($field, $values)- Value not in arraywhereNot($filters)- Not equal filternotLike($field, $pattern)- Pattern should not matchnotBetween($field, $min, $max)- Value outside range
Full-Text Search
Multi-field search support:
$results = $db->query("products")
->search("wireless keyboard")
->get();
// Search in specific fields
$results = $db->query("users")
->search("john", ["name", "email", "bio"])
->get();Database Joins
Cross-database join support:
$orders = $db->query("orders")
->where(["status" => "completed"])
->join("users", "user_id", "id")
->get();
// Each order now includes related user dataGrouping & Aggregation
GROUP BY and HAVING support:
$categories = $db->query("products")
->groupBy("category")
->having("count", ">", 10)
->having("avg:price", ">", 100)
->get();Having aggregate types:
count- Group countsum:field- Field sumavg:field- Field averagemin:field- Minimum valuemax:field- Maximum value
Field Projection
Select or exclude fields from results:
// Get only specific fields
$users = $db->query("users")
->select(["name", "email", "avatar"])
->get();
// Exclude sensitive fields
$users = $db->query("users")
->except(["password", "token", "secret_key"])
->get();removeFields() Terminal Method
Permanently remove fields from matching records:
$result = $db->query("users")
->where(["status" => "deleted"])
->removeFields(["personal_data", "payment_info"]);
// ["n" => 5, "fields_removed" => ["personal_data", "payment_info"]]Features:
- Works with sharded databases
keyfield is protected (cannot be removed)- Can be combined with all filter methods
Method Aliases
Convenience aliases:
->skip(20) // Same as offset(20)
->orderBy("name", "asc") // Same as sort("name", "asc")Bug Fixes
- whereIn/whereNotIn null handling: Fixed null value filtering by using
array_key_exists()instead ofisset() - join() array/object key handling: Fixed crash when local or foreign key values are arrays or objects - now gracefully returns null for non-scalar keys
Improvements
Test Suite
- 723 tests, 1916 assertions (v2.0: 448 tests, 1005 assertions)
- +275 new tests added
NewChainingMethodsTest.php- 100 tests (new methods)NewMethodsEdgeCasesTest.php- 50 tests (edge cases)NewMethodsShardedTest.php- 18 tests (sharded DB support)RemoveFieldsTest.php- 29 tests (removeFields)TypeEdgeCasesTest.php- 79 tests (type validation & edge cases)
Documentation
README.md- All new methods documentedCLAUDE.md- Developer guide updated- Method Chaining section organized into 5 categories
- 15+ new code examples added
Breaking Changes
None. All v2.0.x APIs remain backward compatible.
Migration Guide
CRITICAL: Before updating, backup your
$secretKeyfrom the oldnoneDB.php. Restore it after update or you'll lose access to your data!
To upgrade from v2.0.x to v2.1.0:
- Backup your
$secretKeyfrom currentnoneDB.php - Update
noneDB.phpfile - Restore your
$secretKeyin the new file - Start using new features (optional)
v2.0.0
🚀 noneDB v2.0.0
A major release bringing method chaining, auto-sharding, and a comprehensive test suite to noneDB.
✨ New Features
Method Chaining (Fluent Interface)
Build complex queries with clean, readable syntax:
// Before (still works)
$results = $db->find("users", ["active" => true]);
$sorted = $db->sort($results, "score", "desc");
$limited = $db->limit($sorted, 10);
// After (new fluent API)
$results = $db->query("users")
->where(["active" => true])
->sort("score", "desc")
->limit(10)
->get();Available Methods:
- Chainable:
where(),like(),between(),sort(),limit(),offset() - Terminal:
get(),first(),last(),count(),exists() - Aggregation:
sum(),avg(),min(),max(),distinct() - Write:
update(),delete()
Auto-Sharding
Automatic sharding for large datasets (10K+ records):
// Sharding happens automatically!
$db->insert("large_db", $millionRecords);
// Check shard info
$info = $db->getShardInfo("large_db");
// ["sharded" => true, "shards" => 100, "totalRecords" => 1000000]
// Key-based lookups are O(1) - only reads relevant shard
$user = $db->find("large_db", ["key" => 500000]);Performance Gains (500K records):
| Operation | Without Sharding | With Sharding |
|---|---|---|
| find(key) | ~772ms | ~16ms |
| Memory | ~1.1GB | ~1MB |
🧪 Test Suite
- 448 tests with 1005 assertions
- Unit, Feature, and Integration tests
- Edge case coverage for all methods
composer test # Run all tests
composer test-verbose # Run with details📚 Documentation
- 11 example files covering all features
- Comprehensive README.md with API reference
- CLAUDE.md developer guide
- CHANGES.md changelog
🐛 Bug Fixes
like()function now safely handles array/object field values
📦 Installation
Composer
composer require orhanayd/nonedbManual
include("noneDB.php");
$db = new noneDB();⚠️ Breaking Changes
None! All v1.x APIs remain fully backward compatible.
🙏 Credits
Built with ❤️ by Orhan Aydogdu
Full Changelog: v1.4...v2.0.0