From 7ca0f961121607d4260dcf179a23e196a1deec6f Mon Sep 17 00:00:00 2001 From: Elio Neto Date: Sat, 7 Mar 2026 21:51:50 -0300 Subject: [PATCH 01/11] Add documentation badge to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2f58d1..7306905 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ๐Ÿ—„๏ธ ApexStore - + +[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://elioneto.github.io/ApexStore/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Rust](https://img.shields.io/badge/rust-1.70%2B-orange.svg)](https://www.rust-lang.org/) [![Version](https://img.shields.io/badge/version-1.4.0-blue.svg)](https://github.com/ElioNeto/ApexStore/releases) From 4661f023705deedae96d21070d7349b56d33fb81 Mon Sep 17 00:00:00 2001 From: Elio Neto Date: Sat, 7 Mar 2026 21:56:18 -0300 Subject: [PATCH 02/11] Update README.md --- README.md | 509 +++++++++++------------------------------------------- 1 file changed, 102 insertions(+), 407 deletions(-) diff --git a/README.md b/README.md index 7306905..9291da4 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,72 @@ - # ๐Ÿ—„๏ธ ApexStore - -[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://elioneto.github.io/ApexStore/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Rust](https://img.shields.io/badge/rust-1.70%2B-orange.svg)](https://www.rust-lang.org/) -[![Version](https://img.shields.io/badge/version-1.4.0-blue.svg)](https://github.com/ElioNeto/ApexStore/releases) -[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://www.docker.com/) +

+ ApexStore Logo +

+ +

ApexStore

+ +

+ High-performance, embedded Key-Value engine built with Rust ๐Ÿฆ€ +
+ Implementing LSM-Tree architecture with a focus on SOLID principles, observability, and performance. +

+ +

+ Documentation + License + Rust Version + Version + Docker +

-A high-performance, embedded key-value store written in Rust, implementing the **Log-Structured Merge-Tree (LSM-Tree)** architecture. Built with SOLID principles for production-grade reliability, testability, and maintainability. +--- ## ๐ŸŽฏ Overview -ApexStore is a modern, Rust-based storage engine designed for write-heavy workloads. It combines the durability of write-ahead logging with the efficiency of LSM-Tree architecture, providing: +ApexStore is a modern, Rust-based storage engine designed for write-heavy workloads. It combines the durability of write-ahead logging (WAL) with the efficiency of **Log-Structured Merge-Tree (LSM-Tree)** architecture. + +Built from the ground up using **SOLID principles**, it provides a production-grade storage solution that is easy to reason about, test, and maintain, while delivering the performance expected from a systems-level language. + +## โš–๏ธ Why ApexStore? + +While industry giants like RocksDB or LevelDB focus on extreme complexity, ApexStore offers: + +- **Educational Clarity**: A clean, modular implementation of LSM-Tree that serves as a blueprint for high-performance systems. +- **Strict SOLID Compliance**: Leveraging Rust's ownership model to enforce clear boundaries between MemTable, WAL, and SSTable layers. +- **Observability First**: Built-in real-time metrics for memory, disk usage, and WAL health. +- **Modern Defaults**: Native LZ4 compression, Bloom Filters, and 35+ tunable parameters via environment variables. + +## ๐Ÿ“Š Performance Benchmarks + +*Measured on AMD Ryzen 9 5900X, NVMe SSD (v1.4.0)* -- **High Write Throughput**: Optimized for write-intensive applications with in-memory buffering and sequential disk writes -- **Data Durability**: Write-ahead log (WAL) ensures zero data loss on crashes -- **Efficient Storage**: Block-based compression with LZ4 reduces storage footprint by 2-4x -- **Flexible Configuration**: 35+ tunable parameters via environment variablesโ€”no recompilation needed -- **Production Ready**: Comprehensive error handling, metrics, and monitoring capabilities +| Operation | Throughput | Visual | +|-----------|------------|--------| +| **In-Memory Writes** | ~500k ops/s | โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 100% | +| **Writes (with WAL)** | ~100k ops/s | โ–ˆโ–ˆโ–ˆ 20% | +| **Batch Writes** | ~1M ops/s | โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 200% | +| **MemTable Hits** | ~1.2M ops/s | โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 240% | +| **SSTable Reads** | ~50k ops/s | โ–ˆ 10% | + +> **Note:** The performance difference between In-Memory and WAL writes highlights the fsync overhead, which can be optimized via `WAL_SYNC_MODE`. ## โœจ Key Features -### Storage Engine -- **MemTable**: In-memory BTreeMap with configurable size limits for fast writes -- **Write-Ahead Log (WAL)**: ACID-compliant durability with configurable sync modes -- **SSTable V2**: Block-based storage format with: - - Sparse indexing for O(log N) lookups - - LZ4 compression for space efficiency - - Bloom filters to avoid unnecessary disk I/O - - Comprehensive metadata tracking -- **Automatic Flushing**: Seamless transition from memory to disk when thresholds are reached -- **Crash Recovery**: Automatic WAL replay on startup - -### Access Patterns -- **Interactive CLI**: REPL interface for development and debugging -- **REST API**: Full HTTP API with JSON payloads for production use -- **Batch Operations**: Efficient bulk inserts and updates -- **Search Capabilities**: Prefix and substring search (with iterator improvements coming in v2.0) - -### Advanced Features -- **Feature Flags System**: Dynamic runtime configuration with optimistic locking -- **Statistics & Monitoring**: Real-time metrics for memory, disk, and WAL usage -- **Environment-Based Config**: 35+ parameters organized by category: - - Server HTTP (12 params): networking, threading, timeouts - - LSM Engine (8 params): storage, caching, indexing - - Compaction (5 params): future-ready configuration - - Advanced Tuning (6 params): I/O, memory pools, mmap - - Monitoring (4 params): logging, metrics, telemetry +### ๐Ÿ› ๏ธ Storage Engine +- **MemTable**: In-memory BTreeMap with configurable size limits. +- **Write-Ahead Log (WAL)**: ACID-compliant durability with configurable sync modes. +- **SSTable V2**: Block-based storage with Sparse Indexing and LZ4 Compression. +- **Bloom Filters**: Drastically reduces unnecessary disk I/O for read operations. +- **Crash Recovery**: Automatic WAL replay on startup to ensure zero data loss. + +### ๐Ÿ”Œ Access Patterns +- **Interactive CLI**: REPL interface for development and debugging. +- **REST API**: Full HTTP API with JSON payloads for microservices. +- **Batch Operations**: Efficient bulk inserts and updates. +- **Search Capabilities**: Prefix and substring search (Optimized iterators coming in v2.0). ## ๐Ÿ—๏ธ Architecture -The engine follows a modular SOLID architecture where each component has a single responsibility: +The engine follows a modular architecture where each component has a single responsibility: ```mermaid graph TB @@ -96,426 +113,104 @@ graph TB style SST fill:#9cf,stroke:#333,stroke-width:2px ``` -### Data Flow: Write Path - -```mermaid -sequenceDiagram - participant Client - participant Engine - participant WAL - participant MemTable - participant Builder - participant SSTable - - Client->>Engine: put(key, value) - Engine->>WAL: append(record) - WAL-->>Engine: โœ“ persisted - Engine->>MemTable: insert(key, value) - - alt MemTable Full - Engine->>Builder: new(config, timestamp) - loop For each entry - Engine->>Builder: add(key, record) - end - Builder->>Builder: compress blocks (LZ4) - Builder->>SSTable: write(blocks + metadata + footer) - Builder-->>Engine: SSTable path - Engine->>MemTable: clear() - Engine->>WAL: truncate() - end - - Engine-->>Client: โœ“ success -``` - -### Data Flow: Read Path - -```mermaid -sequenceDiagram - participant Client - participant Engine - participant MemTable - participant SSTable - participant BloomFilter - - Client->>Engine: get(key) - Engine->>MemTable: lookup(key) - - alt Key in MemTable - MemTable-->>Engine: value - Engine-->>Client: value - else Not in MemTable - loop For each SSTable (newest first) - Engine->>BloomFilter: might_contain(key) - alt Bloom says "no" - BloomFilter-->>Engine: โœ— skip - else Bloom says "maybe" - Engine->>SSTable: binary_search_blocks(key) - alt Key found - SSTable->>SSTable: decompress_block() - SSTable-->>Engine: value - Engine-->>Client: value - end - end - end - Engine-->>Client: โœ— not found - end -``` - ## ๐Ÿš€ Quick Start ### Prerequisites +- **Rust 1.70+**: Install via [rustup.rs](https://rustup.rs/) -- **Rust 1.70+**: Install via [rustup](https://rustup.rs/) - ```bash - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - ``` - -### Installation - +### Installation & Run ```bash -# Clone the repository -git clone https://github.com/ElioNeto/ApexStore.git -cd ApexStore - -# Build the project -cargo build --release -``` - -### Usage +# Clone and enter +git clone https://github.com/ElioNeto/ApexStore.git && cd ApexStore -#### Interactive CLI Mode - -```bash -# Start the REPL +# Build and Start REPL cargo run --release # Available commands: -# > put key value -# > get key -# > delete key +# > put user:1 "John Doe" +# > get user:1 # > stats -# > help -# > exit ``` -#### API Server Mode - -```bash -# Copy environment template (optional) -cp .env.example .env - -# Customize settings (optional) -nano .env - -# Start the server -cargo run --release --features api --bin apexstore-server -``` - -The server will start at `http://0.0.0.0:8080` by default. - ## ๐Ÿณ Docker Deployment -### Quick Start with Docker Compose (Recommended) +Run ApexStore as a standalone API server: ```bash -# Clone the repository -git clone https://github.com/ElioNeto/ApexStore.git -cd ApexStore - -# Copy environment file and customize (optional) -cp .env.example .env - -# Start ApexStore +# Start with Docker Compose docker-compose up -d -# View logs -docker-compose logs -f apexstore - -# Stop the service -docker-compose down -``` - -The API will be available at `http://localhost:8080`. - -### Standalone Docker Commands - -```bash -# Build the Docker image -docker build -t apexstore:latest . - -# Run the container -docker run -d \ - --name apexstore-server \ - -p 8080:8080 \ - -v apexstore-data:/data \ - -e MEMTABLE_MAX_SIZE=16777216 \ - -e BLOCK_CACHE_SIZE_MB=64 \ - apexstore:latest - -# View logs -docker logs -f apexstore-server - -# Stop the container -docker stop apexstore-server -``` - -### Docker Configuration - -You can pass environment variables to configure ApexStore: - -```bash +# Manual run with custom config docker run -d \ --name apexstore-server \ -p 8080:8080 \ - -v apexstore-data:/data \ - -e HOST=0.0.0.0 \ - -e PORT=8080 \ -e MEMTABLE_MAX_SIZE=33554432 \ - -e BLOCK_SIZE=8192 \ - -e BLOCK_CACHE_SIZE_MB=128 \ - -e BLOOM_FALSE_POSITIVE_RATE=0.005 \ - -e API_AUTH_ENABLED=true \ - apexstore:latest -``` - -### Health Check - -Docker includes automatic health checks: - -```bash -# Check container health status -docker ps - -# Manual health check -curl http://localhost:8080/health -``` - -### Data Persistence - -ApexStore data is persisted in a Docker volume: - -```bash -# List volumes -docker volume ls | grep apexstore - -# Inspect volume -docker volume inspect apexstore-data - -# Backup data -docker run --rm -v apexstore-data:/data -v $(pwd):/backup alpine \ - tar czf /backup/apexstore-backup.tar.gz -C /data . - -# Restore data -docker run --rm -v apexstore-data:/data -v $(pwd):/backup alpine \ - tar xzf /backup/apexstore-backup.tar.gz -C /data + -v apexstore-data:/data \ + elioneto/apexstore:latest ``` -## ๐ŸŒ REST API - -### Core Operations - -| Method | Endpoint | Description | Example | -|--------|----------|-------------|----------| -| `POST` | `/keys` | Insert or update a key | `{"key": "user:1", "value": "Alice"}` | -| `GET` | `/keys/{key}` | Retrieve a value by key | `/keys/user:1` | -| `DELETE` | `/keys/{key}` | Delete a key (tombstone) | `/keys/user:1` | -| `POST` | `/keys/batch` | Batch insert/update | `[{"key": "k1", "value": "v1"}, ...]` | - -### Search & Monitoring +## ๐ŸŒ REST API Examples | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | `/keys/search/prefix?q=user:` | Prefix search | -| `GET` | `/keys/search/substring?q=alice` | Substring search | +| `POST` | `/keys` | Insert/Update: `{"key": "k1", "value": "v1"}` | +| `GET` | `/keys/{key}` | Retrieve value | | `GET` | `/stats/all` | Full telemetry (Memory, Disk, WAL) | -| `GET` | `/stats/memory` | MemTable statistics | -| `GET` | `/stats/disk` | SSTable statistics | - -### Feature Flags - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `GET` | `/features` | List all feature flags | -| `POST` | `/features/{id}` | Create or update flag | `{"enabled": true}` | -| `GET` | `/features/{id}` | Get flag status | - -## โš™๏ธ Configuration - -ApexStore uses environment variables for configuration. No recompilation needed! - -### Quick Configuration Examples - -#### Stress Testing Profile -```bash -# .env -MAX_JSON_PAYLOAD_SIZE=104857600 # 100MB -MEMTABLE_MAX_SIZE=16777216 # 16MB -BLOCK_CACHE_SIZE_MB=256 -SERVER_WORKERS=16 -``` - -#### High Write Throughput -```bash -MEMTABLE_MAX_SIZE=8388608 # 8MB -COMPACTION_STRATEGY=tiered -WAL_SYNC_MODE=async_batch -BLOCK_SIZE=8192 -``` - -#### Memory Constrained -```bash -MEMTABLE_MAX_SIZE=2097152 # 2MB -BLOCK_CACHE_SIZE_MB=32 -SPARSE_INDEX_INTERVAL=32 -BLOOM_FALSE_POSITIVE_RATE=0.02 -``` - -For detailed configuration options, see [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md). ## ๐Ÿ“ Project Structure -Organized following **SOLID principles**: - ``` ApexStore/ โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ core/ # Domain logic (SRP) -โ”‚ โ”‚ โ”œโ”€โ”€ engine.rs # LSM Engine orchestration -โ”‚ โ”‚ โ”œโ”€โ”€ memtable.rs # In-memory storage -โ”‚ โ”‚ โ””โ”€โ”€ log_record.rs # Data model -โ”‚ โ”œโ”€โ”€ storage/ # Persistence (DIP) -โ”‚ โ”‚ โ”œโ”€โ”€ wal.rs # Write-Ahead Log -โ”‚ โ”‚ โ”œโ”€โ”€ sstable.rs # SSTable reader/manager -โ”‚ โ”‚ โ””โ”€โ”€ builder.rs # SSTable V2 builder -โ”‚ โ”œโ”€โ”€ infra/ # Cross-cutting concerns -โ”‚ โ”‚ โ”œโ”€โ”€ codec.rs # Serialization (Bincode) -โ”‚ โ”‚ โ”œโ”€โ”€ error.rs # Error handling -โ”‚ โ”‚ โ””โ”€โ”€ config.rs # Configuration -โ”‚ โ”œโ”€โ”€ api/ # HTTP transport (Actix-Web) -โ”‚ โ”‚ โ”œโ”€โ”€ handlers.rs # REST endpoints -โ”‚ โ”‚ โ”œโ”€โ”€ server.rs # Server setup -โ”‚ โ”‚ โ””โ”€โ”€ config.rs # Server config -โ”‚ โ”œโ”€โ”€ cli/ # Interactive interface -โ”‚ โ”‚ โ””โ”€โ”€ repl.rs # REPL implementation -โ”‚ โ””โ”€โ”€ features/ # Business domain -โ”‚ โ””โ”€โ”€ flags.rs # Feature flag management -โ”œโ”€โ”€ docs/ # Documentation -โ”‚ โ”œโ”€โ”€ CONFIGURATION.md # Configuration guide -โ”‚ โ”œโ”€โ”€ CONTRIBUTING.md # Contribution guidelines -โ”‚ โ””โ”€โ”€ SETUP.md # Development setup -โ”œโ”€โ”€ tests/ # Integration tests -โ”œโ”€โ”€ docker-compose.yml # Docker Compose configuration -โ”œโ”€โ”€ Dockerfile # Container build instructions -โ”œโ”€โ”€ .env.example # Configuration template -โ”œโ”€โ”€ Cargo.toml # Dependencies -โ”œโ”€โ”€ CHANGELOG.md # Version history -โ””โ”€โ”€ README.md # This file +โ”‚ โ”œโ”€โ”€ core/ # LSM Engine, MemTable, Domain logic +โ”‚ โ”œโ”€โ”€ storage/ # WAL, SSTable V2, Block Builder +โ”‚ โ”œโ”€โ”€ infra/ # Codec, Error Handling, Config +โ”‚ โ”œโ”€โ”€ api/ # Actix-Web Server & Handlers +โ”‚ โ””โ”€โ”€ cli/ # REPL Implementation +โ”œโ”€โ”€ docs/ # Detailed documentation & Architecture +โ”œโ”€โ”€ tests/ # Integration test suite +โ””โ”€โ”€ Dockerfile # Multi-stage build ``` -## ๐Ÿงช Testing +## ๐Ÿงช Testing & Quality ```bash -# Run all tests -cargo test - -# Run with output -cargo test -- --nocapture - -# Run specific test -cargo test test_builder_basic - -# Check code quality -cargo clippy -- -D warnings - -# Format code -cargo fmt +cargo test # Run all tests +cargo clippy -- -D warnings # Linting +cargo fmt # Formatting ``` -## ๐Ÿ“Š Performance Characteristics - -### Write Performance -- **Sequential Writes**: ~500k ops/sec (in-memory MemTable) -- **With WAL**: ~100k ops/sec (fsync overhead) -- **Batch Writes**: Up to 1M ops/sec - -### Read Performance -- **MemTable Hits**: ~1M ops/sec (BTreeMap lookup) -- **SSTable Reads**: ~50k ops/sec (with Bloom filter) -- **Cold Reads**: ~10k ops/sec (disk I/O) - -### Storage Efficiency -- **Compression Ratio**: 2-4x with LZ4 -- **Memory Overhead**: ~100 bytes per MemTable entry -- **Disk Amplification**: ~2-3x (before compaction) - -*Note: Benchmarks on AMD Ryzen 9 5900X, NVMe SSD. Your mileage may vary.* - ## ๐Ÿ—บ๏ธ Roadmap -### โœ… Completed (v1.0 - v1.4) -- [x] Core LSM engine with MemTable and WAL -- [x] SSTable V2 with sparse indexing and compression -- [x] REST API with feature flags -- [x] Comprehensive configuration system -- [x] Interactive CLI -- [x] Bloom filters for read optimization -- [x] Statistics and monitoring -- [x] Global block cache -- [x] Docker support with health checks - -### ๐Ÿšง In Progress (v1.5) -- [ ] Storage iterators for range queries -- [ ] Concurrent read optimization -- [ ] Comprehensive benchmark suite - -### ๐Ÿ”ฎ Future (v2.0+) -- [ ] Compaction strategies (Leveled, Tiered, Lazy Leveling) -- [ ] Authentication & authorization -- [ ] Data integrity validation (CRC32 checksums) -- [ ] Multi-instance support -- [ ] Secondary indexes -- [ ] Snapshot isolation -- [ ] Replication support -- [ ] Distributed consensus (Raft) - -See [`ROADMAP.md`](ROADMAP.md) for detailed timeline. +- [x] SSTable V2 with compression & Bloom Filters +- [x] REST API & Feature Flags +- [x] Global Block Cache +- [ ] **v1.5**: Storage iterators for range queries +- [ ] **v1.6**: Concurrent read optimization +- [ ] **v2.0**: Leveled/Tiered Compaction Strategies ## ๐Ÿค Contributing -Contributions are welcome! Please read our [Contributing Guidelines](docs/CONTRIBUTING.md) before submitting PRs. - -### Quick Contribution Workflow +Contributions are what make the open-source community an amazing place! Please check our [Contributing Guidelines](docs/CONTRIBUTING.md). -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Make your changes -4. Run tests and linter (`cargo test && cargo clippy`) -5. Commit your changes (`git commit -m 'feat: add amazing feature'`) -6. Push to your branch (`git push origin feature/amazing-feature`) -7. Open a Pull Request +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'feat: add amazing feature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request ## ๐Ÿ“„ License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## ๐Ÿ™ Acknowledgments - -- **RocksDB**: Inspiration for LSM-Tree implementation -- **LevelDB**: SSTable format reference -- **Rust Community**: Amazing ecosystem and tooling +Distributed under the MIT License. See `LICENSE` for more information. ## ๐Ÿ“ง Contact -- **Author**: Elio Neto -- **Email**: netoo.elio@hotmail.com -- **GitHub**: [@ElioNeto](https://github.com/ElioNeto) -- **Project**: [ApexStore](https://github.com/ElioNeto/ApexStore) -- **Demo**: [lsm-admin-dev.up.railway.app](https://lsm-admin-dev.up.railway.app/) +**Elio Neto** - [GitHub](https://github.com/ElioNeto) - netoo.elio@hotmail.com +**Demo**: [lsm-admin-dev.up.railway.app](https://lsm-admin-dev.up.railway.app/) ## ๐ŸŒŸ Star History -If you find this project useful, please consider giving it a star! โญ +[![Star History Chart](https://api.star-history.com/svg?repos=ElioNeto/ApexStore&type=Date)](https://star-history.com/#ElioNeto/ApexStore&Date) --- - -**Built with ๐Ÿฆ€ Rust and โค๏ธ for high-performance storage systems** +

Built with ๐Ÿฆ€ Rust and โค๏ธ for high-performance storage systems

From e69fcf3a2619482213ebae6800aee011754cb1b9 Mon Sep 17 00:00:00 2001 From: Elio Date: Sat, 7 Mar 2026 21:59:19 -0300 Subject: [PATCH 03/11] feat: add logo --- docs/assets/logo.png | Bin 0 -> 37006 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/logo.png diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b540b732e358fad695a16f21bfd7233cf1a682e7 GIT binary patch literal 37006 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 Date: Sat, 7 Mar 2026 22:04:18 -0300 Subject: [PATCH 04/11] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9291da4..7000439 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Rust Version Version Docker + CI

--- From db424a867a9caeb3f0a75594d907e3370d90caa0 Mon Sep 17 00:00:00 2001 From: Elio Neto Date: Sat, 7 Mar 2026 22:13:51 -0300 Subject: [PATCH 05/11] Translate CLI texts to English and update logo to ApexStore --- src/cli/mod.rs | 180 ++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 073186b..f69d4b4 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,30 +3,30 @@ use std::io::{self, Write}; use std::path::PathBuf; pub fn main() -> Result<(), Box> { - // Configurar tracing + // Configure tracing tracing_subscriber::fmt() .with_target(false) .with_level(true) .init(); - println!(" ___ _____ __"); - println!(" / | ____ ___ _ ______/ ___// /_____ Jl"); - println!(r" / /| | / __ \/ _ \ |/_/___/\__ \/ __/ __ \/ __/"); - println!(r" / ___ |/ /_/ / __/> < ___/ / /_/ /_/ / /_"); - println!(r"/_/ |_/ .___/\___/_/|_| /____/\__/\____/\__/"); + println!(" ___ _____ __ "); + println!(" / | ____ ___ _ __ / ___// /_____ ________ "); + println!(r" / /| | / __ \/ _ \| |/_/ \__ \/ __/ __ \/ ___/ _ \"); + println!(r" / ___ |/ /_/ / __/> < ___/ / /_/ /_/ / / / __/"); + println!(r"/_/ |_/ .___/\___/_/|_| /____/\__/\____/_/ \___/ "); println!(" /_/ High-Performance LSM-Tree Engine\n"); - // Configuraรงรฃo + // Configuration let config = LsmConfig::builder() .dir_path(PathBuf::from("./.lsm_data")) - .memtable_max_size(4 * 1024) // 4KB para testes + .memtable_max_size(4 * 1024) // 4KB for tests .build()?; - println!("๐Ÿ“‚ Diretรณrio de dados: {}", config.core.dir_path.display()); + println!("๐Ÿ“‚ Data directory: {}", config.core.dir_path.display()); - println!("Inicializando engine..."); + println!("Initializing engine..."); let engine = LsmEngine::new(config)?; - println!("โœ“ Engine inicializado com sucesso!\n"); + println!("โœ“ Engine initialized successfully!\n"); print_help(); println!(); @@ -50,21 +50,21 @@ pub fn main() -> Result<(), Box> { match command.as_str() { "SET" => { if parts.len() < 3 { - println!("โŒ Uso: SET "); + println!("โŒ Usage: SET "); continue; } let key = parts[1].to_string(); let value = parts[2].as_bytes().to_vec(); match engine.set(key.clone(), value) { - Ok(_) => println!("โœ“ SET '{}' executado com sucesso", key), - Err(e) => println!("โŒ Erro: {}", e), + Ok(_) => println!("โœ“ SET '{}' executed successfully", key), + Err(e) => println!("โŒ Error: {}", e), } } "GET" => { if parts.len() < 2 { - println!("โŒ Uso: GET "); + println!("โŒ Usage: GET "); continue; } let key = parts[1]; @@ -74,21 +74,21 @@ pub fn main() -> Result<(), Box> { let value_str = String::from_utf8_lossy(&value); println!("โœ“ '{}' = '{}'", key, value_str); } - Ok(None) => println!("โš  Chave '{}' nรฃo encontrada", key), - Err(e) => println!("โŒ Erro: {}", e), + Ok(None) => println!("โš  Key '{}' not found", key), + Err(e) => println!("โŒ Error: {}", e), } } "DELETE" | "DEL" => { if parts.len() < 2 { - println!("โŒ Uso: DELETE "); + println!("โŒ Usage: DELETE "); continue; } let key = parts[1].to_string(); match engine.delete(key.clone()) { - Ok(_) => println!("โœ“ DELETE '{}' executado (tombstone criado)", key), - Err(e) => println!("โŒ Erro: {}", e), + Ok(_) => println!("โœ“ DELETE '{}' executed (tombstone created)", key), + Err(e) => println!("โŒ Error: {}", e), } } @@ -98,9 +98,9 @@ pub fn main() -> Result<(), Box> { match engine.stats_all() { Ok(stats) => match serde_json::to_string_pretty(&stats) { Ok(json) => println!("{}", json), - Err(e) => println!("โŒ Erro ao serializar JSON: {}", e), + Err(e) => println!("โŒ Error serializing JSON: {}", e), }, - Err(e) => println!("โŒ Erro: {}", e), + Err(e) => println!("โŒ Error: {}", e), } } else { println!("{}", engine.stats()); @@ -109,7 +109,7 @@ pub fn main() -> Result<(), Box> { "SEARCH" => { if parts.len() < 2 { - println!("โŒ Uso: SEARCH [--prefix]"); + println!("โŒ Usage: SEARCH [--prefix]"); continue; } @@ -125,16 +125,16 @@ pub fn main() -> Result<(), Box> { match results { Ok(records) => { if records.is_empty() { - println!("โš  Nenhum registro encontrado"); + println!("โš  No records found"); } else { - println!("โœ“ {} registro(s) encontrado(s):\n", records.len()); + println!("โœ“ {} record(s) found:\n", records.len()); for (key, value) in records { let value_str = String::from_utf8_lossy(&value); println!(" {} = {}", key, value_str); } } } - Err(e) => println!("โŒ Erro: {}", e), + Err(e) => println!("โŒ Error: {}", e), } } @@ -150,7 +150,7 @@ pub fn main() -> Result<(), Box> { } "EXIT" | "QUIT" | "Q" => { - println!("๐Ÿ‘‹ Encerrando LSM-Tree CLI..."); + println!("๐Ÿ‘‹ Closing LSM-Tree CLI..."); break; } @@ -162,7 +162,7 @@ pub fn main() -> Result<(), Box> { if parts.len() >= 3 && parts[1].to_uppercase() == "SET" { // BATCH SET let file_path = parts[2]; - println!("Importando de {}...", file_path); + println!("Importing from {}...", file_path); match std::fs::read_to_string(file_path) { Ok(content) => { @@ -182,37 +182,37 @@ pub fn main() -> Result<(), Box> { match engine.set(key.to_string(), value.as_bytes().to_vec()) { Ok(_) => count += 1, Err(e) => { - println!("โš  Erro na linha {}: {}", line_num + 1, e); + println!("โš  Error on line {}: {}", line_num + 1, e); errors += 1; } } } else { println!( - "โš  Linha {} invรกlida (formato esperado: key=value)", + "โš  Invalid line {} (expected format: key=value)", line_num + 1 ); errors += 1; } } - println!("โœ“ {} registro(s) importado(s)", count); + println!("โœ“ {} record(s) imported", count); if errors > 0 { - println!("โš  {} erro(s) encontrado(s)", errors); + println!("โš  {} error(s) found", errors); } } - Err(e) => println!("โŒ Erro ao ler arquivo: {}", e), + Err(e) => println!("โŒ Error reading file: {}", e), } } else if parts.len() >= 2 { // BATCH (existing functionality) let count: usize = match parts[1].parse() { Ok(n) => n, Err(_) => { - println!("โŒ Count invรกlido"); + println!("โŒ Invalid count"); continue; } }; - println!("Inserindo {} registros...", count); + println!("Inserting {} records...", count); let start = std::time::Instant::now(); for i in 0..count { @@ -222,16 +222,16 @@ pub fn main() -> Result<(), Box> { } let elapsed = start.elapsed(); - println!("โœ“ {} registros inseridos em {:.2?}", count, elapsed); - println!(" Taxa: {:.0} ops/s", count as f64 / elapsed.as_secs_f64()); + println!("โœ“ {} records inserted in {:.2?}", count, elapsed); + println!(" Rate: {:.0} ops/s", count as f64 / elapsed.as_secs_f64()); } else { - println!("โŒ Uso: BATCH | BATCH SET "); + println!("โŒ Usage: BATCH | BATCH SET "); } } "SCAN" => { if parts.len() < 2 { - println!("โŒ Uso: SCAN "); + println!("โŒ Usage: SCAN "); continue; } let prefix = parts[1]; @@ -240,10 +240,10 @@ pub fn main() -> Result<(), Box> { match engine.search_prefix(prefix) { Ok(records) => { if records.is_empty() { - println!("โš  Nenhum registro encontrado com prefixo '{}'", prefix); + println!("โš  No records found with prefix '{}'", prefix); } else { println!( - "โœ“ {} registro(s) com prefixo '{}':\n", + "โœ“ {} record(s) with prefix '{}':\n", records.len(), prefix ); @@ -253,19 +253,19 @@ pub fn main() -> Result<(), Box> { } } } - Err(e) => println!("โŒ Erro: {}", e), + Err(e) => println!("โŒ Error: {}", e), } } "ALL" => { - println!("Listando todos os registros...\n"); + println!("Listing all records...\n"); match engine.scan() { Ok(records) => { if records.is_empty() { - println!("โš  Banco de dados vazio"); + println!("โš  Database is empty"); } else { println!("โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”"); - println!("โ”‚ Chave โ”‚ Valor โ”‚"); + println!("โ”‚ Key โ”‚ Value โ”‚"); println!("โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค"); for (key, value) in records { @@ -286,32 +286,32 @@ pub fn main() -> Result<(), Box> { println!("โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"); } } - Err(e) => println!("โŒ Erro ao escanear: {}", e), + Err(e) => println!("โŒ Error scanning: {}", e), } } "KEYS" => match engine.keys() { Ok(keys) => { if keys.is_empty() { - println!("โš  Nenhuma chave encontrada"); + println!("โš  No keys found"); } else { - println!("Total de chaves: {}\n", keys.len()); + println!("Total keys: {}\n", keys.len()); for (i, key) in keys.iter().enumerate() { println!(" {}. {}", i + 1, key); } } } - Err(e) => println!("โŒ Erro: {}", e), + Err(e) => println!("โŒ Error: {}", e), }, "COUNT" => match engine.count() { - Ok(count) => println!("โœ“ Total de registros ativos: {}", count), - Err(e) => println!("โŒ Erro: {}", e), + Ok(count) => println!("โœ“ Total active records: {}", count), + Err(e) => println!("โŒ Error: {}", e), }, _ => { - println!("โŒ Comando desconhecido: '{}'", command); - println!(" Digite HELP para ver comandos disponรญveis"); + println!("โŒ Unknown command: '{}'", command); + println!(" Type HELP to see available commands"); } } } @@ -320,36 +320,36 @@ pub fn main() -> Result<(), Box> { } fn print_help() { - println!("Comandos disponรญveis:"); - println!(" SET - Insere ou atualiza um par chave-valor"); - println!(" GET - Recupera o valor de uma chave"); - println!(" DELETE - Remove uma chave (cria tombstone)"); - println!(" SEARCH [--prefix] - Busca registros (opcionalmente por prefixo)"); - println!(" SCAN - Lista registros com prefixo especรญfico"); - println!(" ALL - Lista todos os registros do banco"); - println!(" KEYS - Lista apenas as chaves"); - println!(" COUNT - Conta registros ativos"); - println!(" STATS [ALL] - Exibe estatรญsticas (bรกsicas ou detalhadas)"); - println!(" BATCH - Insere N registros de teste"); - println!(" BATCH SET - Importa registros de arquivo"); - println!(" DEMO - Executa demonstraรงรฃo de features"); - println!(" CLEAR - Limpa a tela"); - println!(" HELP ou ? - Exibe esta ajuda"); - println!(" EXIT, QUIT ou Q - Sai do programa"); + println!("Available commands:"); + println!(" SET - Insert or update a key-value pair"); + println!(" GET - Retrieve the value of a key"); + println!(" DELETE - Remove a key (creates tombstone)"); + println!(" SEARCH [--prefix] - Search records (optionally by prefix)"); + println!(" SCAN - List records with specific prefix"); + println!(" ALL - List all database records"); + println!(" KEYS - List only the keys"); + println!(" COUNT - Count active records"); + println!(" STATS [ALL] - Display statistics (basic or detailed)"); + println!(" BATCH - Insert N test records"); + println!(" BATCH SET - Import records from file"); + println!(" DEMO - Run feature demonstration"); + println!(" CLEAR - Clear the screen"); + println!(" HELP or ? - Display this help"); + println!(" EXIT, QUIT or Q - Exit the program"); } fn run_demo(engine: &LsmEngine) -> Result<(), Box> { println!("\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"); - println!("โ•‘ DEMO AUTOMรTICA โ•‘"); + println!("โ•‘ AUTOMATIC DEMO โ•‘"); println!("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n"); - println!("1. Inserindo dados de exemplo..."); + println!("1. Inserting sample data..."); engine.set("user:alice".to_string(), b"Alice Silva".to_vec())?; engine.set("user:bob".to_string(), b"Bob Santos".to_vec())?; engine.set("user:charlie".to_string(), b"Charlie Costa".to_vec())?; - println!(" โœ“ 3 usuรกrios inseridos\n"); + println!(" โœ“ 3 users inserted\n"); - println!("2. Lendo dados..."); + println!("2. Reading data..."); if let Some(v) = engine.get("user:alice")? { println!(" user:alice = {}", String::from_utf8_lossy(&v)); } @@ -358,66 +358,66 @@ fn run_demo(engine: &LsmEngine) -> Result<(), Box> { } println!(); - println!("3. Atualizando user:alice..."); + println!("3. Updating user:alice..."); engine.set("user:alice".to_string(), b"Alice Silva Santos".to_vec())?; if let Some(v) = engine.get("user:alice")? { println!( - " user:alice = {} (atualizado)", + " user:alice = {} (updated)", String::from_utf8_lossy(&v) ); } println!(); - println!("4. Deletando user:bob..."); + println!("4. Deleting user:bob..."); engine.delete("user:bob".to_string())?; match engine.get("user:bob")? { - Some(_) => println!(" โŒ Erro: ainda existe"), - None => println!(" โœ“ user:bob deletado com sucesso"), + Some(_) => println!(" โŒ Error: still exists"), + None => println!(" โœ“ user:bob deleted successfully"), } println!(); - println!("5. Forรงando mรบltiplas escritas para flush..."); + println!("5. Forcing multiple writes to trigger flush..."); for i in 0..10 { engine.set( format!("product:{}", i), format!( - "Product {} - Descriรงรฃo longa para forรงar flush automรกtico", + "Product {} - Long description to force automatic flush", i ) .into_bytes(), )?; } - println!(" โœ“ 10 produtos inseridos\n"); + println!(" โœ“ 10 products inserted\n"); - println!("6. Testando novos comandos..."); + println!("6. Testing new commands..."); println!(" - SEARCH user:"); match engine.search("user:") { - Ok(results) => println!(" Encontrados {} registros", results.len()), - Err(e) => println!(" Erro: {}", e), + Ok(results) => println!(" Found {} records", results.len()), + Err(e) => println!(" Error: {}", e), } println!(" - SEARCH user: --prefix"); match engine.search_prefix("user:") { - Ok(results) => println!(" Encontrados {} registros", results.len()), - Err(e) => println!(" Erro: {}", e), + Ok(results) => println!(" Found {} records", results.len()), + Err(e) => println!(" Error: {}", e), } println!(); - println!("7. Estatรญsticas finais (bรกsicas):"); + println!("7. Final statistics (basic):"); println!("{}", engine.stats()); - println!("\n8. Estatรญsticas detalhadas:"); + println!("\n8. Detailed statistics:"); match engine.stats_all() { Ok(stats) => { if let Ok(json) = serde_json::to_string_pretty(&stats) { println!("{}", json); } } - Err(e) => println!(" Erro: {}", e), + Err(e) => println!(" Error: {}", e), } println!("\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"); - println!("โ•‘ DEMO CONCLUรDA โ•‘"); + println!("โ•‘ DEMO COMPLETED โ•‘"); println!("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n"); Ok(()) From f4cc0fff689b716001c4708f32988c86b70e7e1a Mon Sep 17 00:00:00 2001 From: Elio Date: Sat, 7 Mar 2026 22:22:14 -0300 Subject: [PATCH 06/11] fix: logo cli --- src/cli/mod.rs | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f69d4b4..bb67e9e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -9,9 +9,9 @@ pub fn main() -> Result<(), Box> { .with_level(true) .init(); - println!(" ___ _____ __ "); - println!(" / | ____ ___ _ __ / ___// /_____ ________ "); - println!(r" / /| | / __ \/ _ \| |/_/ \__ \/ __/ __ \/ ___/ _ \"); + println!(" ___ _____ __ "); + println!(" / | ____ ___ _ __ / ___// /_____ _____ ___ "); + println!(r" / /| | / __ \/ _ \| |/_/ \__ \/ __/ __ \/ ___// _ \"); println!(r" / ___ |/ /_/ / __/> < ___/ / /_/ /_/ / / / __/"); println!(r"/_/ |_/ .___/\___/_/|_| /____/\__/\____/_/ \___/ "); println!(" /_/ High-Performance LSM-Tree Engine\n"); @@ -242,11 +242,7 @@ pub fn main() -> Result<(), Box> { if records.is_empty() { println!("โš  No records found with prefix '{}'", prefix); } else { - println!( - "โœ“ {} record(s) with prefix '{}':\n", - records.len(), - prefix - ); + println!("โœ“ {} record(s) with prefix '{}':\n", records.len(), prefix); for (key, value) in records { let value_str = String::from_utf8_lossy(&value); println!(" {} = {}", key, value_str); @@ -361,10 +357,7 @@ fn run_demo(engine: &LsmEngine) -> Result<(), Box> { println!("3. Updating user:alice..."); engine.set("user:alice".to_string(), b"Alice Silva Santos".to_vec())?; if let Some(v) = engine.get("user:alice")? { - println!( - " user:alice = {} (updated)", - String::from_utf8_lossy(&v) - ); + println!(" user:alice = {} (updated)", String::from_utf8_lossy(&v)); } println!(); @@ -380,11 +373,7 @@ fn run_demo(engine: &LsmEngine) -> Result<(), Box> { for i in 0..10 { engine.set( format!("product:{}", i), - format!( - "Product {} - Long description to force automatic flush", - i - ) - .into_bytes(), + format!("Product {} - Long description to force automatic flush", i).into_bytes(), )?; } println!(" โœ“ 10 products inserted\n"); From 97d9f9d5eabe7b4bde34c69ddbcb57330f719b16 Mon Sep 17 00:00:00 2001 From: Elio Neto Date: Sat, 7 Mar 2026 22:45:04 -0300 Subject: [PATCH 07/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7000439..7c9deaf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Documentation License Rust Version - Version + Version Docker CI

From 1ae9d24ba628a66c353621f38364561ba0397c74 Mon Sep 17 00:00:00 2001 From: Elio Date: Sat, 7 Mar 2026 23:50:45 -0300 Subject: [PATCH 08/11] feat: publish --- Cargo.lock | 2 +- Cargo.toml | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f839185..97d00b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,7 +271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "apexstore" +name = "apex-store-rs" version = "2.1.0" dependencies = [ "actix-cors", diff --git a/Cargo.toml b/Cargo.toml index c25eea9..ff97d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,36 @@ [package] -name = "apexstore" +name = "apex-store-rs" # Alterado para ser รบnico no crates.io version = "2.1.0" edition = "2021" authors = ["Elio Neto "] license = "MIT" repository = "https://github.com/ElioNeto/ApexStore" -description = "A high-performance LSM-tree storage engine with SSTable V2 format" +homepage = "https://github.com/ElioNeto/ApexStore" +documentation = "https://elioneto.github.io/ApexStore/" +readme = "README.md" +description = "A high-performance, embedded LSM-tree storage engine with SSTable V2 format, LZ4 compression, and Bloom Filters." +keywords = ["database", "lsm-tree", "key-value", "storage-engine", "embedded"] +categories = ["database-implementations", "data-structures"] +# Impede que arquivos de banco de dados locais sejam enviados no pacote +exclude = [ + "data/*", + "target/*", + "*.sst", + "*.log", + ".env", + "docs/assets/*" +] [[bin]] name = "apexstore-server" path = "src/bin/server.rs" [[bin]] -name = "cli" +name = "apexstore-cli" # Renomeado de 'cli' para evitar conflitos globais path = "src/bin/cli.rs" [lib] -name = "apexstore" +name = "apexstore" # Mantido para que vocรช use `use apexstore;` no seu cรณdigo path = "src/lib.rs" [features] @@ -55,6 +69,7 @@ criterion = { version = "0.5", features = ["html_reports"] } opt-level = 3 lto = true codegen-units = 1 +panic = "abort" # Opcional: reduz o tamanho do binรกrio [profile.bench] -inherits = "release" +inherits = "release" \ No newline at end of file From 562bb4666726914d96bf684babfcb70ebb4b5933 Mon Sep 17 00:00:00 2001 From: Elio Date: Sun, 8 Mar 2026 14:11:26 -0300 Subject: [PATCH 09/11] fix(cli): modify parts variable to collect data corretly --- src/cli/mod.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index bb67e9e..ac08f8d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -33,7 +33,7 @@ pub fn main() -> Result<(), Box> { // REPL Loop loop { - print!("lsm> "); + print!("ApexStore (CLI): "); io::stdout().flush()?; let mut input = String::new(); @@ -44,7 +44,7 @@ pub fn main() -> Result<(), Box> { continue; } - let parts: Vec<&str> = input.splitn(4, ' ').collect(); + let parts: Vec<&str> = input.splitn(3, ' ').collect(); let command = parts[0].to_uppercase(); match command.as_str() { @@ -55,6 +55,7 @@ pub fn main() -> Result<(), Box> { } let key = parts[1].to_string(); let value = parts[2].as_bytes().to_vec(); + //let value = parts[2..].join(" ").as_bytes().to_vec(); match engine.set(key.clone(), value) { Ok(_) => println!("โœ“ SET '{}' executed successfully", key), @@ -144,13 +145,16 @@ pub fn main() -> Result<(), Box> { "CLEAR" => { print!("\x1B[2J\x1B[1;1H"); // Clear screen ANSI code - println!("โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"); - println!("โ•‘ LSM-Tree Key-Value Store - Interactive CLI โ•‘"); - println!("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n"); + println!(" ___ _____ __ "); + println!(" / | ____ ___ _ __ / ___// /_____ _____ ___ "); + println!(r" / /| | / __ \/ _ \| |/_/ \__ \/ __/ __ \/ ___// _ \"); + println!(r" / ___ |/ /_/ / __/> < ___/ / /_/ /_/ / / / __/"); + println!(r"/_/ |_/ .___/\___/_/|_| /____/\__/\____/_/ \___/ "); + println!(" /_/ High-Performance LSM-Tree Engine\n"); } "EXIT" | "QUIT" | "Q" => { - println!("๐Ÿ‘‹ Closing LSM-Tree CLI..."); + println!("๐Ÿ‘‹ Closing ApexStore... See you later"); break; } From e81a3739ed7a709717ecd68d4968db9d18251a66 Mon Sep 17 00:00:00 2001 From: Elio Date: Sun, 8 Mar 2026 14:28:00 -0300 Subject: [PATCH 10/11] feat(#76): fixes #76 --- CHANGELOG_v1.5.md | 225 ---------------------------------------------- 1 file changed, 225 deletions(-) delete mode 100644 CHANGELOG_v1.5.md diff --git a/CHANGELOG_v1.5.md b/CHANGELOG_v1.5.md deleted file mode 100644 index 932afe9..0000000 --- a/CHANGELOG_v1.5.md +++ /dev/null @@ -1,225 +0,0 @@ -# Changelog v1.5 - CLI Command Parity - -## Release Date -March 6, 2026 - -## Overview -This release equalizes the CLI command set with REST API endpoints, providing feature parity between both interfaces and enhancing the user experience. - -## New Features - -### 1. STATS ALL - Detailed Statistics -**Command:** `STATS ALL` - -**Description:** Displays comprehensive statistics in JSON format including: -- MemTable records and size -- SSTable files, records, and disk usage -- WAL size -- Total record count -- Configuration parameters - -**Example:** -```bash -lsm> STATS ALL -{ - "mem_records": 3, - "mem_kb": 2, - "sst_files": 2, - "sst_records": 47, - "sst_kb": 156, - "wal_kb": 1, - "total_records": 50, - "memtable_max_size": 4 -} -``` - -**REST API Equivalent:** `GET /stats/all` - -### 2. SEARCH - Advanced Search with Prefix Support -**Command:** `SEARCH [--prefix]` - -**Description:** Searches for records matching a query with two modes: -- **Substring mode** (default): Finds all keys containing the query string -- **Prefix mode** (with `--prefix` flag): Finds all keys starting with the query string (optimized) - -**Examples:** -```bash -# Substring search -lsm> SEARCH user -โœ“ 3 registro(s) encontrado(s): - user:alice = Alice Silva - user:bob = Bob Santos - user:charlie = Charlie Costa - -# Prefix search (faster for hierarchical keys) -lsm> SEARCH user: --prefix -โœ“ 3 registro(s) encontrado(s): - user:alice = Alice Silva - user:bob = Bob Santos - user:charlie = Charlie Costa -``` - -**REST API Equivalent:** `GET /keys/search?q=query&prefix=true` - -### 3. BATCH SET - Bulk Import from Files -**Command:** `BATCH SET ` - -**Description:** Imports records from a text file in `key=value` format. - -**File Format:** -``` -# Comments start with # -user:alice=Alice Silva -user:bob=Bob Santos -product:1=Laptop Dell XPS 15 -config:theme=dark -``` - -**Features:** -- Supports comments (lines starting with `#`) -- Skips empty lines -- Automatic key/value trimming -- Error reporting with line numbers -- Progress feedback - -**Example:** -```bash -lsm> BATCH SET examples/batch_data.txt -Importando de examples/batch_data.txt... -โœ“ 23 registro(s) importado(s) -``` - -**REST API Equivalent:** `POST /keys/batch` - -### 4. SCAN Improvement -**Command:** `SCAN ` - -**Description:** Updated to use the optimized `search_prefix` method instead of showing "not implemented" warning. - -**Example:** -```bash -lsm> SCAN user: -โœ“ 3 registro(s) com prefixo 'user:': - user:alice = Alice Silva - user:bob = Bob Santos - user:charlie = Charlie Costa -``` - -## Improvements - -### Enhanced Help System -Updated `print_help()` function to include all new commands with proper formatting: -``` -STATS [ALL] - Exibe estatรญsticas (bรกsicas ou detalhadas) -SEARCH [--prefix] - Busca registros (opcionalmente por prefixo) -BATCH SET - Importa registros de arquivo -``` - -### Enhanced DEMO Command -Updated demo to showcase all new features: -- Tests SEARCH in both modes -- Displays STATS ALL output -- Demonstrates prefix scanning - -## Documentation - -### New Files -- **`docs/CLI_GUIDE.md`**: Comprehensive CLI documentation - - All command syntax and examples - - Best practices - - Troubleshooting guide - - Complete workflow examples - -- **`examples/batch_data.txt`**: Sample data file - - Demonstrates file format for BATCH SET - - Includes various data types (users, products, config) - - Comments explaining usage - -### Updated Files -- **`src/cli/mod.rs`**: Complete CLI implementation -- **`CHANGELOG_v1.5.md`**: This changelog - -## Breaking Changes -None. All existing commands remain unchanged and backward compatible. - -## Migration Guide -No migration needed. New commands are additive. - -## Implementation Details - -### Command Parsing -- Updated `splitn` to handle 4 parts for complex commands -- Maintains backward compatibility with existing command syntax - -### Error Handling -- Graceful handling of file read errors -- Line-by-line error reporting for BATCH SET -- Validates file format and provides helpful error messages - -### Performance -- SEARCH with `--prefix` flag uses optimized BTree iteration -- BATCH SET processes files line-by-line (memory efficient) -- STATS ALL uses existing engine methods (minimal overhead) - -## Testing - -### Manual Testing Completed -- [x] STATS ALL produces valid JSON -- [x] SEARCH works in both substring and prefix modes -- [x] BATCH SET imports data correctly -- [x] File format validation works properly -- [x] Error messages are helpful and accurate -- [x] SCAN command uses optimized search -- [x] All commands maintain backward compatibility - -### Test Files -- `examples/batch_data.txt` - Sample data for testing - -## CLI vs REST API Command Mapping - -| CLI Command | REST API Endpoint | Status | -|-------------|------------------|--------| -| `SET ` | `POST /keys` | โœ… Parity | -| `GET ` | `GET /keys/{key}` | โœ… Parity | -| `DELETE ` | `DELETE /keys/{key}` | โœ… Parity | -| `STATS` | `GET /stats` | โœ… Parity | -| `STATS ALL` | `GET /stats/all` | โœ… **New** | -| `SEARCH ` | `GET /keys/search?q=...` | โœ… **New** | -| `SEARCH --prefix` | `GET /keys/search?q=...&prefix=true` | โœ… **New** | -| `BATCH SET ` | `POST /keys/batch` | โœ… **New** | -| `KEYS` | `GET /keys` | โœ… Parity | -| `ALL` | `GET /scan` | โœ… Parity | -| `COUNT` | N/A (CLI only) | โœ… CLI-only | -| `SCAN ` | `GET /scan?prefix=...` | โœ… Improved | - -## Known Limitations - -1. **Token Management**: CLI does not support token management commands (low priority - better suited for REST API) -2. **Feature Flags**: CLI does not yet support feature flag management (planned for future release) -3. **Large Files**: BATCH SET loads entire file into memory (acceptable for typical use cases) - -## Future Enhancements (v1.6+) - -### Phase 2: Feature Management (Medium Priority) -- [ ] `FEATURES` - List all feature flags -- [ ] `FEATURE SET ` - Toggle feature flags - -### Phase 3: Advanced Features (Low Priority) -- [ ] `EXPORT ` - Export data to file -- [ ] `IMPORT [--format json|txt]` - Import with format detection -- [ ] `TOKEN` commands - CLI-based token management - -## Contributors -- Elio Neto (@ElioNeto) - -## Related Issues -- Closes #65 - Equalize CLI Commands with REST API Endpoints - -## References -- [CLI Guide](docs/CLI_GUIDE.md) -- [REST API Documentation](docs/API.md) -- [Configuration Guide](docs/CONFIGURATION.md) - ---- - -**Release Notes:** This release focuses on developer experience by providing CLI feature parity with the REST API. All Phase 1 (High Priority) commands have been implemented, tested, and documented. From 42b4da689f6e9f77b88509a53ddc2c8089eb5c93 Mon Sep 17 00:00:00 2001 From: Elio Date: Sun, 8 Mar 2026 14:36:40 -0300 Subject: [PATCH 11/11] fix: workflow --- .github/workflows/feature-fix-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/feature-fix-workflow.yml b/.github/workflows/feature-fix-workflow.yml index 6470756..961f865 100644 --- a/.github/workflows/feature-fix-workflow.yml +++ b/.github/workflows/feature-fix-workflow.yml @@ -199,7 +199,7 @@ jobs: if [ "$ISSUE_STATE" = "OPEN" ]; then # Get recent commits for this branch - RECENT_COMMITS=$(git log "origin/develop..${{ github.ref_name }}" --pretty=format:'- %s (%h)' | head -3) + RECENT_COMMITS=$(git log "origin/develop..${{ github.ref_name }}"-n 3 --pretty=format:'- %s (%h)') gh issue comment "$ISSUE_NUM" --body "๐Ÿ”„ **Update from \`${{ github.ref_name }}\`**