Applies to: SharpCoreDB v1.7.0+ (.NET 10 / C# 14)
⚠️ Note: Single-File vs Directory modeThis document covers storage engine modes (Columnar vs Page-Based) within a database. It does not cover the separate distinction between Directory mode (
Databaseclass) and Single-File mode (SingleFileDatabase,.scdbfiles).Both modes share the same
SqlParserengine for DML and SELECT, and both support the same core DDL operations (CREATE TABLE,DROP TABLE,ALTER TABLE ADD/DROP/RENAME COLUMN,CREATE INDEX,DROP INDEX).→ See
SINGLE_FILE_SQL_LIMITATIONS.mdfor the full feature matrix.
SharpCoreDB supports two primary table storage modes, specified via the STORAGE clause in CREATE TABLE:
-- Columnar (default): Optimized for append-heavy analytics workloads
CREATE TABLE logs (ts DATETIME, msg TEXT) STORAGE = COLUMNAR
-- Page-Based: Optimized for OLTP with frequent updates/deletes
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT) STORAGE = PAGE_BASEDIf omitted, the default is Columnar (unless overridden by DatabaseConfig.StorageEngineType).
| Criteria | Columnar | Page-Based |
|---|---|---|
| Insert-heavy, append-only | ✅ Best | Good |
| Frequent UPDATE/DELETE | ✅ Best | |
| Analytics / full table scan | ✅ Best | Good |
| Point lookups by PK | Good | ✅ Best |
| Tables without PRIMARY KEY | ✅ Works |
DELETE and UPDATE work correctly. The PK B-tree index is used to locate storage positions for logical deletion (index removal). Physical space is reclaimed during compaction.
History: Prior to v1.7.0, DELETE on Columnar tables without a PRIMARY KEY was a silent no-op — rows were not removed, no error was raised. This was a critical data integrity bug.
As of v1.7.0, Columnar DELETE without a PK falls back to a full storage scan (engine.GetAllRecords()) to locate matching rows. This works correctly but is O(n) for every delete operation.
For tables that require DELETE or UPDATE operations, either:
-
Define a PRIMARY KEY — enables efficient index-based row location:
CREATE TABLE snapshots (id TEXT PRIMARY KEY, stream_id TEXT, version LONG, data TEXT)
-
Use
PAGE_BASEDstorage — supports efficient in-place delete without a PK:CREATE TABLE snapshots (stream_id TEXT, version LONG, data TEXT) STORAGE = PAGE_BASED
-
Avoid delete-heavy patterns on Columnar tables without a PK — the full-scan fallback works but is slow for large tables.
Select(where)→ finds matching rows via hash indexes- PK B-tree index → resolves storage positions
- Remove from PK index + hash indexes (logical delete)
- Physical space reclaimed during auto-compaction
engine.GetAllRecords()→ full storage scanDeserializeRowFromSpan()+EvaluateSimpleWhere()→ filter matches- Remove from hash indexes using storage position
- Physical space reclaimed during auto-compaction
engine.GetAllRecords()→ full storage scanDeserializeRowFromSpan()+EvaluateSimpleWhere()→ filter matchesengine.Delete()→ marks page slot as physically deleted- Freed pages tracked for reuse
A potential enhancement is to auto-generate an implicit _rowid column (similar to SQLite's rowid) for Columnar tables that lack an explicit PRIMARY KEY. This would:
- Enable efficient delete/update without user-defined PKs
- Maintain backward compatibility (hidden from
SELECT *) - Require careful handling in INSERT (auto-increment), schema serialization, and migration
This is tracked as a future improvement. For now, the full-scan fallback ensures correctness.