Skip to content

Releases: orhanayd/noneDB

v3.1.0

29 Dec 08:53
5d488d6

Choose a tag to compare

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 index
  • hasSpatialIndex($dbname, $field) - Check if index exists
  • getSpatialIndexes($dbname) - List all spatial indexes
  • dropSpatialIndex($dbname, $field) - Remove index
  • rebuildSpatialIndex($dbname, $field) - Rebuild index
  • withinDistance($dbname, $field, $lon, $lat, $meters) - Find within radius (meters)
  • withinBBox($dbname, $field, $minLon, $minLat, $maxLon, $maxLat) - Find in bbox
  • nearest($dbname, $field, $lon, $lat, $k) - Find K nearest
  • withinPolygon($dbname, $field, $polygon) - Find in polygon
  • validateGeoJSON($geometry) - Validate GeoJSON

Query Builder Methods (noneDBQuery)

  • withinDistance($field, $lon, $lat, $meters) - Spatial: within radius (meters)
  • withinBBox($field, $minLon, $minLat, $maxLon, $maxLat) - Spatial: within bbox
  • nearest($field, $lon, $lat, $k) - Spatial: K nearest
  • withinPolygon($field, $polygon) - Spatial: within polygon
  • withDistance($field, $lon, $lat) - Add _distance field 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 reference
  • SPATIAL.md - Spatial indexing guide
  • CONFIGURATION.md - Configuration options
  • API.md - Complete API reference
  • BENCHMARKS.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
  • _distance field in results is in meters
  • nearest() maxDistance option is in meters

v3.0.0

28 Dec 19:41
0a4f6fd

Choose a tag to compare

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
  • .jidx index 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

27 Dec 19:37
c6ca84f

Choose a tag to compare

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 attempts

Performance 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

27 Dec 03:02
b03b5ed

Choose a tag to compare

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 filtering
  • whereIn($field, $values) - Value in array
  • whereNotIn($field, $values) - Value not in array
  • whereNot($filters) - Not equal filter
  • notLike($field, $pattern) - Pattern should not match
  • notBetween($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 data

Grouping & 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 count
  • sum:field - Field sum
  • avg:field - Field average
  • min:field - Minimum value
  • max: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
  • key field 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 of isset()
  • 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 documented
  • CLAUDE.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 $secretKey from the old noneDB.php. Restore it after update or you'll lose access to your data!

To upgrade from v2.0.x to v2.1.0:

  1. Backup your $secretKey from current noneDB.php
  2. Update noneDB.php file
  3. Restore your $secretKey in the new file
  4. Start using new features (optional)

v2.0.0

27 Dec 01:42

Choose a tag to compare

🚀 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/nonedb

Manual

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