Repository: Johonsoy/SmartStashDB
Branch: main
Commit: b9d9354f9d6c
Files: 17
Total size: 31.4 KB
Directory structure:
gitextract_9djr5vhe/
├── LICENSE
├── README.md
├── const/
│ ├── Reader.go
│ ├── constant.go
│ └── error.go
├── go.mod
├── main.go
└── storage/
├── SegmentReader.go
├── batch.go
├── chunk.go
├── db.go
├── logrecord.go
├── memtable.go
├── options.go
├── pool.go
├── segmentfile.go
└── tinywal.go
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Johnsoy.zhao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🚀 SmartStashDB: A High-Performance Key-Value Store
Welcome to **SmartStashDB**, a blazing-fast, Go-powered key-value store built from scratch using **LSM-Tree**, **Skip-List**, and **Write-Ahead Logging (WAL)**. Designed for high throughput and low latency, SmartStashDB is perfect for applications demanding scalable, reliable, and efficient data storage.
---
## 🌟 Features
- **High Performance**: Optimized for low-latency reads and writes, leveraging LSM-Tree and Skip-List for efficient data organization.
- **Durability**: Write-Ahead Logging ensures no data is lost, even in the face of crashes.
- **Scalability**: LSM-Tree architecture supports massive datasets with seamless compaction.
- **Memory Efficiency**: Skip-List provides fast in-memory indexing with minimal overhead.
- **Simple API**: Intuitive key-value operations for easy integration.
- **Go-Powered**: Written in Go for concurrency, simplicity, and cross-platform support.
---
## 🛠️ Architecture
SmartStashDB combines cutting-edge data structures and techniques to deliver top-tier performance:
- **LSM-Tree**: Log-Structured Merge-Tree for write-heavy workloads, with background compaction to keep reads fast.
- **Skip-List**: Probabilistic data structure for in-memory indexing, enabling O(log n) lookups.
- **WAL**: Write-Ahead Logging for crash recovery and data durability.
- **Compaction**: Periodic merging of SSTables to optimize storage and query performance.
```
[Client] --> [API: Get/Put/Delete] --> [MemTable (Skip-List)]
|
v
[WAL (Disk)]
|
v
[SSTables (LSM-Tree)]
```
---
## 🚀 Getting Started
### Prerequisites
- **Go**: Version 1.18 or higher
- A passion for high-performance systems! 😎
### Installation
1. Clone the repository:
```bash
git clone https://github.com/johnsoy/SmartStashDB.git
cd SmartStashDB
```
2. Install dependencies:
```bash
go mod tidy
```
3. Build and run:
```bash
go build
./SmartStashDB
```
### Example Usage
```go
package main
import (
"fmt"
"github.com/johnsoy/SmartStashDB"
)
func main() {
// Initialize SmartStashDB
kv, err := SmartStashDB.NewSmartStashDB("./data")
if err != nil {
panic(err)
}
defer kv.Close()
// Put key-value pair
kv.Put([]byte("key1"), []byte("value1"))
// Get value
value, err := kv.Get([]byte("key1"))
if err != nil {
panic(err)
}
fmt.Printf("Key: key1, Value: %s\n", value)
// Delete key
kv.Delete([]byte("key1"))
}
```
---
## 📊 Performance
SmartStashDB is designed for speed and scalability. Preliminary benchmarks (on a standard laptop with SSD):
- **Write Throughput**: ~500,000 ops/sec
- **Read Throughput**: ~600,000 ops/sec
- **Latency**: < 1ms for 99th percentile reads/writes
Run benchmarks yourself:
```bash
go test -bench=.
```
---
## 🛠️ Configuration
Customize SmartStashDB via the `config.yaml` file:
```yaml
data_dir: "./data" # Storage directory
memtable_size: 1048576 # Max MemTable size (bytes)
compaction_interval: 60 # Compaction interval (seconds)
wal_flush_interval: 10 # WAL flush interval (seconds)
```
Load config programmatically:
```go
kv, err := SmartStashDB.NewSmartStashDBWithConfig("config.yaml")
```
---
## 🤝 Contributing
Contributions are welcome! Whether it's bug fixes, new features, or documentation improvements, here's how to get started:
1. Fork the repository.
2. Create a feature branch: `git checkout -b feature/awesome-feature`.
3. Commit your changes: `git commit -m "Add awesome feature"`.
4. Push to the branch: `git push origin feature/awesome-feature`.
5. Open a Pull Request.
Please read our [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
---
## 📜 License
SmartStashDB is licensed under the [MIT License](LICENSE). Feel free to use, modify, and distribute it as you see fit!
---
## 📫 Contact
- **GitHub**: [Johnsoy](https://github.com/Johonsoy)
- **Email**: [15520754767@163.com]
Star ⭐ this repo if you find SmartStashDB awesome, and let's build the fastest KV store together! 🚀
================================================
FILE: const/Reader.go
================================================
package _const
import (
"SmartStashDB/storage"
"io"
)
type Reader struct {
AllSegmentReader []*storage.SegmentReader
Progress int
}
func (r *Reader) Next() ([]byte, *storage.ChunkPosition, error) {
if r.Progress >= len(r.AllSegmentReader) {
return nil, nil, io.EOF
}
data, chunkPos, err := r.AllSegmentReader[r.Progress].Next()
if err == io.EOF {
r.Progress++
return r.Next()
}
return data, chunkPos, err
}
================================================
FILE: const/constant.go
================================================
package _const
const (
B = 1
KB = 1024 * B
MB = 1024 * KB
GB = 1024 * MB
)
const (
// 单个Block 32KB
BlockSize = 32 * KB
)
const (
ChunkHeadSize = 7
)
const (
FirstSegmentFileId = 1
segmentFileModePerm = 0644
)
func ExecDir() string {
return ""
}
================================================
FILE: const/error.go
================================================
package _const
import (
"errors"
)
var (
ErrDatabaseIsUsing = errors.New("the database directory is used by another process")
ErrorDBClosed = errors.New("the database is closed")
ErrorReadOnlyBatch = errors.New("the read-only batch exists")
ErrorBatchCommited = errors.New("the batch commited")
ErrorKeyNotFound = errors.New("key not found")
ErrorKeyIsEmpty = errors.New("the key is empty")
ErrorFileExtError = errors.New("segmentFileExt must not start with '.'")
ErrorDataToLarge = errors.New("data is too large")
ErrorPendingSizeTooLarge = errors.New("pending size is too large")
ErrClosed = errors.New("closed")
)
================================================
FILE: go.mod
================================================
module SmartStashDB
go 1.22
require (
github.com/bwmarrin/snowflake v0.3.0
github.com/dgraph-io/badger v1.6.0
github.com/gofrs/flock v0.8.1
github.com/hashicorp/golang-lru/v2 v2.0.7
)
require (
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
================================================
FILE: main.go
================================================
package main
import (
_const "SmartStashDB/const"
"SmartStashDB/storage"
)
func main() {
options := storage.DefaultOptions
options.DirPath = _const.ExecDir() + "/data"
db, err := storage.OpenDB(options)
if err != nil {
panic(err)
}
defer func() {
_ = db.Close()
}()
key := "adasdsa"
value := "asdbsadsd"
err = db.Put(key, value, nil)
if err != nil {
panic(err)
}
newValue, err := db.Get(key)
if err != nil {
panic(err)
}
print(string(newValue))
}
================================================
FILE: storage/SegmentReader.go
================================================
package storage
import (
_const "SmartStashDB/const"
"io"
)
type SegmentReader struct {
seg *SegmentFile
blockidx uint32
chunkoffset uint32
}
func (s *SegmentReader) Next() ([]byte, *ChunkPosition, error) {
if s.seg.closed {
return nil, nil, io.EOF
}
curChunk := &ChunkPosition{
SegmentFileId: s.seg.segmentFileId,
BlockIndex: s.blockidx,
ChunkOffset: s.chunkoffset,
}
data, nextChunk, err := s.seg.readInternal(curChunk.BlockIndex, curChunk.ChunkOffset)
if err != nil {
return nil, nil, err
}
curChunk.ChunkSize = nextChunk.BlockIndex*_const.BlockSize + nextChunk.ChunkOffset -
(s.chunkoffset*_const.BlockSize + curChunk.ChunkOffset)
s.blockidx = nextChunk.BlockIndex
s.chunkoffset = curChunk.ChunkOffset
return data, curChunk, nil
}
================================================
FILE: storage/batch.go
================================================
package storage
import (
_const "SmartStashDB/const"
"sync"
)
import "github.com/bwmarrin/snowflake"
func makeBatch() interface{} {
node, err := snowflake.NewNode(1)
if err != nil {
panic(err)
}
return &Batch{
options: DefaultBatchOptions,
m: sync.RWMutex{},
batchId: node,
}
}
type Batch struct {
db *DB
pendingWrites map[string]*LogRecord
options BatchOptions
m sync.RWMutex
commited bool
batchId *snowflake.Node
}
func (batch *Batch) reset() {
}
func (batch *Batch) init(readOnly bool, sync bool, db *DB) *Batch {
batch.db = db
batch.options.ReadOnly = readOnly
batch.options.Sync = sync
batch.lock()
return batch
}
func (batch *Batch) lock() {
if batch.options.ReadOnly {
batch.db.m.RLock()
} else {
batch.db.m.Lock()
}
}
func (batch *Batch) writePendingWrites() *Batch {
batch.pendingWrites = make(map[string]*LogRecord)
return batch
}
func (batch *Batch) put(key []byte, value []byte) error {
if len(key) == 0 {
return _const.ErrorKeyIsEmpty
}
if batch.db.Closed {
return _const.ErrorDBClosed
}
if batch.options.ReadOnly {
return _const.ErrorReadOnlyBatch
}
batch.m.Lock()
defer batch.m.Unlock()
batch.pendingWrites[string(key)] = &LogRecord{
Key: key,
Value: value,
Type: LogRecordNormal,
}
return nil
}
func (batch *Batch) unLock() {
if batch.options.ReadOnly {
batch.db.m.RUnlock()
} else {
batch.db.m.Unlock()
}
}
func (batch *Batch) commit(w *WriteOptions) error {
if w == nil {
w = &WriteOptions{
Sync: false,
DisableWal: false,
}
}
defer batch.unLock()
if batch.db.Closed {
return _const.ErrorDBClosed
}
if batch.options.ReadOnly || len(batch.pendingWrites) == 0 {
return nil
}
batch.m.Lock()
defer batch.m.Unlock()
if batch.commited {
return _const.ErrorBatchCommited
}
if err := batch.db.waitMemTableSpace(); err != nil {
return err
}
batchId := batch.batchId.Generate()
if err := batch.db.activeMem.putBatch(batch.pendingWrites, batchId, w); err != nil {
return err
}
batch.commited = true
return nil
}
func (batch *Batch) Get(key []byte) ([]byte, error) {
if len(key) == 0 {
return nil, _const.ErrorKeyIsEmpty
}
if batch.db.Closed {
return nil, _const.ErrorDBClosed
}
if batch.pendingWrites != nil {
batch.m.RLock()
defer batch.m.RUnlock()
if record := batch.pendingWrites[string(key)]; record != nil {
if record.Type == LogRecordDeleted {
return nil, _const.ErrorKeyNotFound
}
return record.Value, nil
}
}
tables := batch.db.getMemTables()
for _, table := range tables {
deleted, value := table.get(key)
if deleted {
return nil, _const.ErrorKeyNotFound
}
if len(value) != 0 {
return value, nil
}
}
return nil, _const.ErrorKeyNotFound
}
func (batch *Batch) delete(key []byte) error {
if len(key) == 0 {
return _const.ErrorKeyIsEmpty
}
if batch.db.Closed {
return _const.ErrorDBClosed
}
if batch.options.ReadOnly {
return _const.ErrorReadOnlyBatch
}
batch.m.Lock()
batch.pendingWrites[string(key)] = &LogRecord{
Key: key,
Type: LogRecordDeleted,
}
batch.m.Unlock()
return nil
}
================================================
FILE: storage/chunk.go
================================================
package storage
type ChunkType = byte
const (
ChunkTypeFull ChunkType = iota
ChunkTypeStart
ChunkTypeMiddle
ChunkTypeEnd
)
type ChunkPosition struct {
SegmentFileId SegmentFileId
BlockIndex uint32
ChunkOffset uint32
ChunkSize uint32
}
================================================
FILE: storage/db.go
================================================
package storage
import (
_const "SmartStashDB/const"
"errors"
"github.com/gofrs/flock"
"os"
"path/filepath"
"sync"
)
const (
FileLockName = "FLOCK"
)
type DB struct {
m sync.RWMutex
activeMem *MemTable // Active memory
immutableMem []*MemTable // Immutable memory
Closed bool
batchPool sync.Pool
}
func (db *DB) Close() error {
db.m.Lock()
defer db.m.Unlock()
for _, table := range db.immutableMem {
err := table.close()
if err != nil {
return err
}
}
if err := db.activeMem.close(); err != nil {
return err
}
db.Closed = true
return nil
}
func (db *DB) Put(key string, value string, options *WriteOptions) error {
batch := db.batchPool.Get().(*Batch)
defer func() {
batch.reset()
db.batchPool.Put(batch)
}()
batch.init(false, false, db).writePendingWrites()
err := batch.put([]byte(key), []byte(value))
if err != nil {
batch.unLock()
return err
}
return batch.commit(options)
}
func (db *DB) waitMemTableSpace() error {
if db.activeMem.isFull() {
return nil
}
db.immutableMem = append(db.immutableMem, db.activeMem)
option := db.activeMem.option
option.id++
table, err := openMemTable(option)
if err != nil {
return err
}
db.activeMem = table
return nil
}
func (db *DB) Get(key string) ([]byte, error) {
batch := db.batchPool.Get().(*Batch)
batch.init(true, false, db)
defer func() {
_ = batch.commit(nil)
batch.reset()
db.batchPool.Put(batch)
}()
return batch.Get([]byte(key))
}
func (db *DB) getMemTables() []*MemTable {
return db.immutableMem
}
func (db *DB) Delete(key []byte, options *WriteOptions) error {
if len(key) == 0 {
return _const.ErrorKeyIsEmpty
}
batch := db.batchPool.Get().(*Batch)
batch.init(false, false, db)
defer func() {
batch.reset()
db.batchPool.Put(batch)
}()
if err := batch.delete(key); err != nil {
batch.unLock()
return err
}
return batch.commit(options)
}
func OpenDB(options Options) (*DB, error) {
// Check if file existed.
if _, err := os.Stat(options.DirPath); err != nil {
if err := os.Mkdir(options.DirPath, os.ModePerm); err != nil {
return nil, err
}
}
lock, err := flock.New(filepath.Join(options.DirPath, FileLockName)).TryLock()
if err != nil {
return nil, err
}
if !lock {
return nil, errors.New("file locked")
}
memTables, err := openAllMemTables(options)
if err != nil {
return nil, err
}
db := &DB{
activeMem: memTables[len(memTables)-1],
immutableMem: memTables,
batchPool: sync.Pool{New: makeBatch},
}
return db, nil
}
================================================
FILE: storage/logrecord.go
================================================
package storage
import (
"encoding/binary"
)
type LogRecordType = byte
const (
LogRecordNormal = iota
LogRecordDeleted
LogRecordBatchEnd
MaxLogRecordLength = 1 + binary.MaxVarintLen64*2
)
type LogRecord struct {
Key []byte
Value []byte
Type LogRecordType
BatchId uint64
}
func NewLogRecord() *LogRecord {
return &LogRecord{}
}
// Encode Serialize LogRecord, header + batchId + keySize + valueSize + key + value /*
func (logRecord *LogRecord) Encode() []byte {
header := make([]byte, MaxLogRecordLength)
header[0] = logRecord.Type
index := 1
index += binary.PutUvarint(header[index:], logRecord.BatchId)
index += binary.PutVarint(header[index:], int64(len(logRecord.Key)))
index += binary.PutVarint(header[index:], int64(len(logRecord.Value)))
value := make([]byte, index+len(logRecord.Key)+len(logRecord.Value))
// copy header.
copy(value, header[:index])
copy(value[index:], logRecord.Key)
copy(value[index+len(logRecord.Key):], logRecord.Value)
return value
}
func (logRecord *LogRecord) Decode(b []byte) {
logRecord.Type = b[0]
index := 1
n := 0
logRecord.BatchId, n = binary.Uvarint(b[index:])
index += n
keyLength, n := binary.Uvarint(b[index:])
index += n
valueLength, n := binary.Uvarint(b[index:])
index += n
key := make([]byte, keyLength)
value := make([]byte, valueLength)
copy(key, b[index:index+int(keyLength)])
index += int(keyLength)
copy(value, b[index:index+int(valueLength)])
logRecord.Key = key
logRecord.Value = value
}
================================================
FILE: storage/memtable.go
================================================
package storage
import (
"fmt"
"github.com/bwmarrin/snowflake"
"github.com/dgraph-io/badger/skl"
"github.com/dgraph-io/badger/y"
"io"
"math"
"os"
"sort"
"sync"
)
const (
initTableId = 1
walFileExt = ".MEM.%d"
)
type MemTable struct {
option memTableOptions
mu sync.RWMutex
skl *skl.Skiplist
tinyWal *TinyWAL
}
type memTableOptions struct {
sklMemSize uint32 // skip-list memory size.
id int // skip-list memory id.
walDir string // file dir.
walCacheSize uint32 // wal cache size.
walIsSync bool // whether to flush the disk immediately.
walBytesPerSync uint32 // how bytes to flush the disk.
}
func openAllMemTables(options WalOptions) ([]*MemTable, error) {
dir, err := os.ReadDir(options.DirPath)
if err != nil {
return nil, err
}
var tableIds []int
for _, file := range dir {
if file.IsDir() {
continue
}
var id int
var prefix int
_, err = fmt.Sscanf(file.Name(), "memtable_%d"+walFileExt, &prefix, &id)
if err != nil {
continue
}
tableIds = append(tableIds, id)
}
if len(tableIds) == 0 {
tableIds = append(tableIds, initTableId)
}
sort.Ints(tableIds)
tables := make([]*MemTable, len(tableIds))
for i, id := range tableIds {
table, err := openMemTable(memTableOptions{
sklMemSize: options.MemTableSize,
id: id,
walDir: options.DirPath,
walIsSync: options.Sync,
walBytesPerSync: options.BytesPerSync,
})
if err != nil {
return nil, err
}
tables[i] = table
}
return tables, nil
}
func openMemTable(option memTableOptions) (*MemTable, error) {
skipList := skl.NewSkiplist(int64(option.sklMemSize * 2))
table := &MemTable{
option: option,
skl: skipList,
}
wal, err := OpenTinyWAL(WalOptions{
DirPath: option.walDir,
MemTableSize: math.MaxInt32,
segmentFileExt: fmt.Sprintf(walFileExt, option.id),
Sync: option.walIsSync,
BytesPerSync: option.walBytesPerSync,
BlockCache: option.walCacheSize,
})
if err != nil {
return nil, err
}
table.tinyWal = wal
indexRecords := make(map[uint64][]*LogRecord)
reader := wal.NewReader()
for {
data, _, err := reader.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
record := NewLogRecord()
record.Decode(data)
if record.Type == LogRecordBatchEnd {
batchId, err := snowflake.ParseBytes(record.Key)
if err != nil {
return nil, err
}
for _, idxRecord := range indexRecords[uint64(batchId)] {
table.skl.Put(y.KeyWithTs(idxRecord.Key, 0),
y.ValueStruct{
Meta: idxRecord.Type,
Value: idxRecord.Value,
})
}
delete(indexRecords, uint64(batchId))
} else {
indexRecords[record.BatchId] = append(indexRecords[record.BatchId], record)
}
}
return table, nil
}
func (mt *MemTable) get(key []byte) (bool, []byte) {
mt.mu.RLock()
defer mt.mu.RUnlock()
valueStruct := mt.skl.Get(y.KeyWithTs(key, 0))
deleted := valueStruct.Meta == LogRecordDeleted
return deleted, valueStruct.Value
}
func (mt *MemTable) isFull() bool {
return mt.skl.MemSize() >= int64(mt.option.sklMemSize)
}
func (mt *MemTable) putBatch(records map[string]*LogRecord, batchId snowflake.ID, options *WriteOptions) error {
if options == nil || options.DisableWal {
for _, record := range records {
record.BatchId = uint64(batchId)
if err := mt.tinyWal.PendingWrites(record.Encode()); err != nil {
return err
}
}
record := NewLogRecord()
record.Key = batchId.Bytes()
record.Type = LogRecordBatchEnd
if err := mt.tinyWal.PendingWrites(record.Encode()); err != nil {
return err
}
if err := mt.tinyWal.WriteAll(); err != nil {
return err
}
if options != nil && options.Sync && mt.option.walIsSync {
if err := mt.tinyWal.Sync(); err != nil {
return err
}
}
}
mt.mu.Lock()
for key, record := range records {
mt.skl.Put(y.KeyWithTs([]byte(key), 0),
y.ValueStruct{
Meta: record.Type,
Value: record.Value,
})
}
mt.mu.Unlock()
return nil
}
func (mt *MemTable) close() error {
if mt.skl != nil {
return mt.tinyWal.close()
}
return nil
}
================================================
FILE: storage/options.go
================================================
package storage
import (
_const "SmartStashDB/const"
"os"
)
type WalOptions struct {
DirPath string
MemTableSize uint64
segmentFileExt string
Sync bool
BytesPerSync uint64
BlockCache uint32
}
type BatchOptions struct {
ReadOnly bool
Sync bool
}
var DefaultOptions = WalOptions{
DirPath: tempDBDir(),
MemTableSize: 64 * _const.MB,
BlockCache: 0,
Sync: false,
BytesPerSync: 0,
}
var DefaultBatchOptions = BatchOptions{
ReadOnly: false,
Sync: true,
}
func tempDBDir() string {
temp, _ := os.MkdirTemp("", "db-temp")
return temp
}
type WriteOptions struct {
Sync bool
DisableWal bool
}
================================================
FILE: storage/pool.go
================================================
package storage
import (
"bytes"
"sync"
)
type bufferPool struct {
buffer sync.Pool
}
func (p *bufferPool) Get() *bytes.Buffer {
return p.buffer.Get().(*bytes.Buffer)
}
func (p *bufferPool) Put(buffer *bytes.Buffer) {
p.buffer.Put(buffer)
}
var DefaultBuffer = newBufferPool()
func newBufferPool() *bufferPool {
return &bufferPool{
buffer: sync.Pool{
New: func() any { return new(bytes.Buffer) },
},
}
}
================================================
FILE: storage/segmentfile.go
================================================
package storage
import (
_const "SmartStashDB/const"
"bytes"
"encoding/binary"
"fmt"
lru "github.com/hashicorp/golang-lru/v2"
"hash/crc32"
"os"
"path/filepath"
)
type SegmentFileId = uint32
type SegmentFile struct {
segmentFileId SegmentFileId
fd *os.File
lastBlockIndex uint32
lastBlockSize uint32
header []byte
closed bool
localCache *lru.Cache[uint32, []byte]
}
func (f *SegmentFile) readInternal(index uint32, offset uint32) ([]byte, *ChunkPosition, error) {
return nil, nil, nil
}
func (f *SegmentFile) NewSegmentReader() *SegmentReader {
return &SegmentReader{
seg: f,
blockidx: 0,
chunkoffset: 0,
}
}
func (f *SegmentFile) Close() error {
if f.closed {
return nil
}
f.closed = true
return f.fd.Close()
}
func (f *SegmentFile) Size() int64 {
return int64(f.lastBlockIndex*_const.BlockSize + f.lastBlockSize)
}
func (f *SegmentFile) Sync() error {
return f.fd.Sync()
}
func (f *SegmentFile) Write(data []byte) (*ChunkPosition, error) {
if f.closed {
return nil, _const.ErrClosed
}
index := f.lastBlockIndex
size := f.lastBlockSize
var err error
buffer := DefaultBuffer.Get()
defer func() {
DefaultBuffer.Put(buffer)
}()
writeBuffer, err := f.writeBuffer(data, buffer)
if err != nil {
f.lastBlockIndex = index
f.lastBlockSize = size
return nil, err
}
err = f.writeBuffer2File(buffer)
if err != nil {
f.lastBlockIndex = index
f.lastBlockSize = size
return nil, err
}
return writeBuffer, nil
}
func (f *SegmentFile) WriteAll(writes [][]byte) (position []*ChunkPosition, err error) {
if f.closed {
return nil, _const.ErrClosed
}
index := f.lastBlockIndex
lastBlockSize := f.lastBlockSize
buffer := DefaultBuffer.Get()
defer func() {
if err != nil {
f.lastBlockIndex = index
f.lastBlockSize = lastBlockSize
}
DefaultBuffer.Put(buffer)
}()
positions := make([]*ChunkPosition, len(writes))
for i, data := range writes {
pos, err := f.writeBuffer(data, buffer)
if err != nil {
return nil, err
}
positions[i] = pos
}
if err := f.writeBuffer2File(buffer); err != nil {
return nil, err
}
return positions, nil
}
func (f *SegmentFile) writeBuffer(bytes []byte, buffer *bytes.Buffer) (*ChunkPosition, error) {
if f.closed {
return nil, _const.ErrClosed
}
padding := uint32(0)
// Pre-grow the buffer for better performance
totalWriteSize := len(bytes) + int(_const.ChunkHeadSize)*2 // Estimate needed size
buffer.Grow(totalWriteSize)
if f.lastBlockSize+_const.ChunkHeadSize >= _const.BlockSize {
size := _const.BlockSize - f.lastBlockSize
_, err := buffer.Write(make([]byte, size))
if err != nil {
return nil, err
}
padding += size
f.lastBlockIndex++
f.lastBlockSize = 0
}
position := &ChunkPosition{
SegmentFileId: f.segmentFileId,
BlockIndex: f.lastBlockIndex,
ChunkOffset: f.lastBlockSize,
}
dataLen := uint32(len(bytes))
if f.lastBlockSize+_const.ChunkHeadSize <= _const.BlockSize {
err := f.appendChunk2Buffer(buffer, bytes, ChunkTypeFull)
if err != nil {
return nil, err
}
position.ChunkSize = dataLen + _const.ChunkHeadSize
} else {
// Split data into many chunks across blocks
var (
remainingDataSize = dataLen
curBlockSize = f.lastBlockSize
chunkNum uint32 = 0
)
for remainingDataSize > 0 {
chunkType := ChunkTypeMiddle
if remainingDataSize == dataLen {
chunkType = ChunkTypeStart
}
freeSize := _const.BlockSize - curBlockSize - _const.ChunkHeadSize
if freeSize >= remainingDataSize {
freeSize = remainingDataSize
chunkType = ChunkTypeEnd
}
err := f.appendChunk2Buffer(buffer, bytes[dataLen-remainingDataSize:dataLen-remainingDataSize+freeSize], chunkType)
if err != nil {
return nil, err
}
chunkNum++
remainingDataSize -= freeSize
curBlockSize = (curBlockSize + _const.ChunkHeadSize + freeSize) % _const.BlockSize
}
position.ChunkSize = chunkNum*_const.ChunkHeadSize + dataLen
}
return position, nil
}
func (f *SegmentFile) writeBuffer2File(buffer *bytes.Buffer) error {
if f.lastBlockSize > _const.BlockSize {
panic("lastBlockSize exceeded BlockSize")
}
_, err := f.fd.Write(buffer.Bytes())
return err
}
func (f *SegmentFile) appendChunk2Buffer(buffer *bytes.Buffer, data []byte, cType ChunkType) error {
// 设置header中的长度
binary.LittleEndian.PutUint16(f.header[4:6], uint16(len(data)))
// 设置header中的类型
f.header[6] = cType
// 对 len + type + data 求 checksum
sum := crc32.ChecksumIEEE(f.header[4:])
sum = crc32.Update(sum, crc32.IEEETable, data)
// 设置header中的校验和
binary.LittleEndian.PutUint32(f.header[:4], sum)
//将一个完整的chunk写入buf中(header + payload 就是一个chunk)
_, err := buffer.Write(f.header[:])
if err != nil {
return err
}
_, err = buffer.Write(data)
if err != nil {
return err
}
return nil
}
func segmentFileName(dir, ext string, id uint32) string {
return filepath.Join(dir, fmt.Sprintf("%010d"+ext, id))
}
func openSegmentFile(dir string, ext string, id uint32, localCache *lru.Cache[uint32, []byte]) (*SegmentFile, error) {
path := segmentFileName(dir, ext, id)
fd, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
_ = fd.Close()
}
}()
stat, err := fd.Stat()
if err != nil {
return nil, err
}
size := stat.Size()
return &SegmentFile{
segmentFileId: id,
fd: fd,
lastBlockIndex: uint32(size / _const.BlockSize),
lastBlockSize: uint32(size % _const.BlockSize),
header: make([]byte, _const.ChunkHeadSize),
localCache: localCache,
}, nil
}
================================================
FILE: storage/tinywal.go
================================================
package storage
import (
_const "SmartStashDB/const"
"errors"
"fmt"
lru "github.com/hashicorp/golang-lru/v2"
"io/fs"
"os"
"sort"
"strings"
"sync"
)
type TinyWAL struct {
option WalOptions
mutex sync.RWMutex
activeSegment *SegmentFile
immutableSegment map[SegmentFileId]*SegmentFile
localCache *lru.Cache[uint32, []byte]
byteWrite uint64
pendingWritesLock sync.Mutex
pendingWrites [][]byte
pendingWritesSize uint64
}
func (w *TinyWAL) close() error {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.localCache != nil {
w.localCache.Purge()
}
for _, segment := range w.immutableSegment {
if segment != nil {
if err := segment.Close(); err != nil {
return err
}
}
}
w.immutableSegment = nil
return w.activeSegment.Close()
}
func OpenTinyWAL(option WalOptions) (*TinyWAL, error) {
if strings.HasPrefix(option.segmentFileExt, ".") {
return nil, errors.New(option.segmentFileExt + " is not allowed")
}
err := os.MkdirAll(option.DirPath, fs.ModePerm)
if err != nil {
return nil, err
}
tinyWAL := &TinyWAL{
option: option,
immutableSegment: make(map[SegmentFileId]*SegmentFile),
activeSegment: nil,
}
if option.BlockCache > 0 {
blockNum := option.BlockCache / _const.BlockSize
if option.BlockCache%_const.BlockSize != 0 {
blockNum++
}
tinyWAL.localCache, err = lru.New[uint32, []byte](int(blockNum))
if err != nil {
return nil, err
}
}
dir, err := os.ReadDir(option.DirPath)
if err != nil {
return nil, err
}
var segmentFileIds []int
for _, file := range dir {
if file.IsDir() {
continue
}
segmentFileId := 0
_, err := fmt.Scanf(file.Name(), "%d"+option.segmentFileExt, &segmentFileId)
if err != nil {
continue
}
segmentFileIds = append(segmentFileIds, segmentFileId)
}
if len(segmentFileIds) == 0 {
segment, err := openSegmentFile(option.DirPath, option.segmentFileExt, _const.FirstSegmentFileId, tinyWAL.localCache)
if err != nil {
return nil, err
}
tinyWAL.activeSegment = segment
} else {
sort.Ints(segmentFileIds)
for i, fileId := range segmentFileIds {
segment, err := openSegmentFile(option.DirPath, option.segmentFileExt, uint32(fileId), tinyWAL.localCache)
if err != nil {
return nil, err
}
if i == len(segmentFileIds)-1 {
tinyWAL.activeSegment = segment
} else {
tinyWAL.immutableSegment[uint32(fileId)] = segment
}
}
}
return tinyWAL, nil
}
func (w *TinyWAL) PendingWrites(data []byte) error {
w.pendingWritesLock.Lock()
defer w.pendingWritesLock.Unlock()
w.maxWriteSize(int64(len(data)))
return nil
}
func (w *TinyWAL) WriteAll() ([]*ChunkPosition, error) {
if len(w.pendingWrites) == 0 {
return make([]*ChunkPosition, 0), nil
}
w.mutex.Lock()
defer func() {
w.ClearPendingWrites()
w.mutex.Unlock()
}()
if w.pendingWritesSize > w.option.MemTableSize {
return nil, _const.ErrorPendingSizeTooLarge
}
if uint64(w.activeSegment.Size())+w.pendingWritesSize > w.option.MemTableSize {
err := w.replaceActiveSegmentFile()
if err != nil {
return nil, err
}
}
all, err := w.activeSegment.WriteAll(w.pendingWrites)
if err != nil {
return nil, err
}
return all, nil
}
func (w *TinyWAL) Sync() error {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.activeSegment.Sync()
}
func (w *TinyWAL) maxWriteSize(size int64) int64 {
chunks := (size + _const.BlockSize - 1) / _const.BlockSize // 计算正确的块数(向上取整)
total := chunks * _const.ChunkHeadSize // 总块头大小
newHeadSize := _const.ChunkHeadSize + size // 基础头+数据大小
return newHeadSize + total
}
func (w *TinyWAL) NewReader() *_const.Reader {
w.mutex.RLock()
defer w.mutex.RUnlock()
var readers []*SegmentReader
for _, segment := range w.immutableSegment {
readers = append(readers, segment.NewSegmentReader())
}
readers = append(readers, w.activeSegment.NewSegmentReader())
sort.Slice(readers, func(i, j int) bool { return readers[i].seg.segmentFileId < readers[j].seg.segmentFileId })
return &_const.Reader{
AllSegmentReader: readers,
Progress: 0,
}
}
func (w *TinyWAL) Write(data []byte) (*ChunkPosition, error) {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.maxWriteSize(int64(len(data))) > int64(w.option.MemTableSize) {
return nil, _const.ErrorDataToLarge
}
if w.isFull(int64(len(data))) {
if err := w.replaceActiveSegmentFile(); err != nil {
return nil, err
}
}
position, err := w.activeSegment.Write(data)
if err != nil {
return nil, err
}
w.byteWrite += uint64(position.ChunkSize)
isSync := w.option.Sync
if !isSync && w.byteWrite > w.option.BytesPerSync {
isSync = true
}
if isSync {
err := w.activeSegment.Sync()
if err != nil {
return nil, err
}
w.byteWrite = 0
}
return position, err
}
func (w *TinyWAL) isFull(delta int64) bool {
return w.activeSegment.Size()+w.maxWriteSize(delta) > int64(w.option.MemTableSize)
}
func (w *TinyWAL) replaceActiveSegmentFile() error {
err := w.activeSegment.Sync()
if err != nil {
return err
}
w.byteWrite = 0
file, err := openSegmentFile(w.option.DirPath, w.option.segmentFileExt, w.activeSegment.segmentFileId+1, w.localCache)
if err != nil {
return err
}
w.immutableSegment[w.activeSegment.segmentFileId] = w.activeSegment
w.activeSegment = file
return nil
}
func (w *TinyWAL) ClearPendingWrites() {
}
gitextract_9djr5vhe/
├── LICENSE
├── README.md
├── const/
│ ├── Reader.go
│ ├── constant.go
│ └── error.go
├── go.mod
├── main.go
└── storage/
├── SegmentReader.go
├── batch.go
├── chunk.go
├── db.go
├── logrecord.go
├── memtable.go
├── options.go
├── pool.go
├── segmentfile.go
└── tinywal.go
SYMBOL INDEX (90 symbols across 13 files)
FILE: const/Reader.go
type Reader (line 8) | type Reader struct
method Next (line 13) | func (r *Reader) Next() ([]byte, *storage.ChunkPosition, error) {
FILE: const/constant.go
constant B (line 4) | B = 1
constant KB (line 5) | KB = 1024 * B
constant MB (line 6) | MB = 1024 * KB
constant GB (line 7) | GB = 1024 * MB
constant BlockSize (line 12) | BlockSize = 32 * KB
constant ChunkHeadSize (line 16) | ChunkHeadSize = 7
constant FirstSegmentFileId (line 20) | FirstSegmentFileId = 1
constant segmentFileModePerm (line 21) | segmentFileModePerm = 0644
function ExecDir (line 24) | func ExecDir() string {
FILE: main.go
function main (line 8) | func main() {
FILE: storage/SegmentReader.go
type SegmentReader (line 8) | type SegmentReader struct
method Next (line 14) | func (s *SegmentReader) Next() ([]byte, *ChunkPosition, error) {
FILE: storage/batch.go
function makeBatch (line 9) | func makeBatch() interface{} {
type Batch (line 21) | type Batch struct
method reset (line 30) | func (batch *Batch) reset() {
method init (line 34) | func (batch *Batch) init(readOnly bool, sync bool, db *DB) *Batch {
method lock (line 42) | func (batch *Batch) lock() {
method writePendingWrites (line 50) | func (batch *Batch) writePendingWrites() *Batch {
method put (line 55) | func (batch *Batch) put(key []byte, value []byte) error {
method unLock (line 77) | func (batch *Batch) unLock() {
method commit (line 86) | func (batch *Batch) commit(w *WriteOptions) error {
method Get (line 120) | func (batch *Batch) Get(key []byte) ([]byte, error) {
method delete (line 155) | func (batch *Batch) delete(key []byte) error {
FILE: storage/chunk.go
constant ChunkTypeFull (line 6) | ChunkTypeFull ChunkType = iota
constant ChunkTypeStart (line 7) | ChunkTypeStart
constant ChunkTypeMiddle (line 8) | ChunkTypeMiddle
constant ChunkTypeEnd (line 9) | ChunkTypeEnd
type ChunkPosition (line 12) | type ChunkPosition struct
FILE: storage/db.go
constant FileLockName (line 13) | FileLockName = "FLOCK"
type DB (line 16) | type DB struct
method Close (line 24) | func (db *DB) Close() error {
method Put (line 41) | func (db *DB) Put(key string, value string, options *WriteOptions) err...
method waitMemTableSpace (line 56) | func (db *DB) waitMemTableSpace() error {
method Get (line 71) | func (db *DB) Get(key string) ([]byte, error) {
method getMemTables (line 82) | func (db *DB) getMemTables() []*MemTable {
method Delete (line 86) | func (db *DB) Delete(key []byte, options *WriteOptions) error {
function OpenDB (line 103) | func OpenDB(options Options) (*DB, error) {
FILE: storage/logrecord.go
constant LogRecordNormal (line 10) | LogRecordNormal = iota
constant LogRecordDeleted (line 11) | LogRecordDeleted
constant LogRecordBatchEnd (line 12) | LogRecordBatchEnd
constant MaxLogRecordLength (line 13) | MaxLogRecordLength = 1 + binary.MaxVarintLen64*2
type LogRecord (line 16) | type LogRecord struct
method Encode (line 28) | func (logRecord *LogRecord) Encode() []byte {
method Decode (line 50) | func (logRecord *LogRecord) Decode(b []byte) {
function NewLogRecord (line 23) | func NewLogRecord() *LogRecord {
FILE: storage/memtable.go
constant initTableId (line 16) | initTableId = 1
constant walFileExt (line 18) | walFileExt = ".MEM.%d"
type MemTable (line 21) | type MemTable struct
method get (line 145) | func (mt *MemTable) get(key []byte) (bool, []byte) {
method isFull (line 155) | func (mt *MemTable) isFull() bool {
method putBatch (line 159) | func (mt *MemTable) putBatch(records map[string]*LogRecord, batchId sn...
method close (line 198) | func (mt *MemTable) close() error {
type memTableOptions (line 31) | type memTableOptions struct
function openAllMemTables (line 40) | func openAllMemTables(options WalOptions) ([]*MemTable, error) {
function openMemTable (line 87) | func openMemTable(option memTableOptions) (*MemTable, error) {
FILE: storage/options.go
type WalOptions (line 8) | type WalOptions struct
type BatchOptions (line 17) | type BatchOptions struct
function tempDBDir (line 35) | func tempDBDir() string {
type WriteOptions (line 40) | type WriteOptions struct
FILE: storage/pool.go
type bufferPool (line 8) | type bufferPool struct
method Get (line 12) | func (p *bufferPool) Get() *bytes.Buffer {
method Put (line 16) | func (p *bufferPool) Put(buffer *bytes.Buffer) {
function newBufferPool (line 22) | func newBufferPool() *bufferPool {
FILE: storage/segmentfile.go
type SegmentFile (line 16) | type SegmentFile struct
method readInternal (line 32) | func (f *SegmentFile) readInternal(index uint32, offset uint32) ([]byt...
method NewSegmentReader (line 37) | func (f *SegmentFile) NewSegmentReader() *SegmentReader {
method Close (line 45) | func (f *SegmentFile) Close() error {
method Size (line 53) | func (f *SegmentFile) Size() int64 {
method Sync (line 57) | func (f *SegmentFile) Sync() error {
method Write (line 61) | func (f *SegmentFile) Write(data []byte) (*ChunkPosition, error) {
method WriteAll (line 88) | func (f *SegmentFile) WriteAll(writes [][]byte) (position []*ChunkPosi...
method writeBuffer (line 121) | func (f *SegmentFile) writeBuffer(bytes []byte, buffer *bytes.Buffer) ...
method writeBuffer2File (line 189) | func (f *SegmentFile) writeBuffer2File(buffer *bytes.Buffer) error {
method appendChunk2Buffer (line 197) | func (f *SegmentFile) appendChunk2Buffer(buffer *bytes.Buffer, data []...
function segmentFileName (line 220) | func segmentFileName(dir, ext string, id uint32) string {
function openSegmentFile (line 224) | func openSegmentFile(dir string, ext string, id uint32, localCache *lru....
FILE: storage/tinywal.go
type TinyWAL (line 15) | type TinyWAL struct
method close (line 28) | func (w *TinyWAL) close() error {
method PendingWrites (line 118) | func (w *TinyWAL) PendingWrites(data []byte) error {
method WriteAll (line 126) | func (w *TinyWAL) WriteAll() ([]*ChunkPosition, error) {
method Sync (line 153) | func (w *TinyWAL) Sync() error {
method maxWriteSize (line 160) | func (w *TinyWAL) maxWriteSize(size int64) int64 {
method NewReader (line 167) | func (w *TinyWAL) NewReader() *_const.Reader {
method Write (line 185) | func (w *TinyWAL) Write(data []byte) (*ChunkPosition, error) {
method isFull (line 217) | func (w *TinyWAL) isFull(delta int64) bool {
method replaceActiveSegmentFile (line 221) | func (w *TinyWAL) replaceActiveSegmentFile() error {
method ClearPendingWrites (line 236) | func (w *TinyWAL) ClearPendingWrites() {
function OpenTinyWAL (line 47) | func OpenTinyWAL(option WalOptions) (*TinyWAL, error) {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (36K chars).
[
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2025 Johnsoy.zhao\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 4306,
"preview": "# 🚀 SmartStashDB: A High-Performance Key-Value Store\n\nWelcome to **SmartStashDB**, a blazing-fast, Go-powered key-value "
},
{
"path": "const/Reader.go",
"chars": 434,
"preview": "package _const\n\nimport (\n\t\"SmartStashDB/storage\"\n\t\"io\"\n)\n\ntype Reader struct {\n\tAllSegmentReader []*storage.SegmentReade"
},
{
"path": "const/constant.go",
"chars": 263,
"preview": "package _const\n\nconst (\n\tB = 1\n\tKB = 1024 * B\n\tMB = 1024 * KB\n\tGB = 1024 * MB\n)\n\nconst (\n\t// 单个Block 32KB\n\tBlockSize = "
},
{
"path": "const/error.go",
"chars": 706,
"preview": "package _const\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrDatabaseIsUsing = errors.New(\"the database directory is used by an"
},
{
"path": "go.mod",
"chars": 405,
"preview": "module SmartStashDB\n\ngo 1.22\n\nrequire (\n\tgithub.com/bwmarrin/snowflake v0.3.0\n\tgithub.com/dgraph-io/badger v1.6.0\n\tgithu"
},
{
"path": "main.go",
"chars": 478,
"preview": "package main\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"SmartStashDB/storage\"\n)\n\nfunc main() {\n\toptions := storage.Default"
},
{
"path": "storage/SegmentReader.go",
"chars": 783,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"io\"\n)\n\ntype SegmentReader struct {\n\tseg *SegmentFile\n\tb"
},
{
"path": "storage/batch.go",
"chars": 3152,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"sync\"\n)\nimport \"github.com/bwmarrin/snowflake\"\n\nfunc makeBatch("
},
{
"path": "storage/chunk.go",
"chars": 255,
"preview": "package storage\n\ntype ChunkType = byte\n\nconst (\n\tChunkTypeFull ChunkType = iota\n\tChunkTypeStart\n\tChunkTypeMiddle\n\tChunkT"
},
{
"path": "storage/db.go",
"chars": 2537,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"errors\"\n\t\"github.com/gofrs/flock\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync"
},
{
"path": "storage/logrecord.go",
"chars": 1508,
"preview": "package storage\n\nimport (\n\t\"encoding/binary\"\n)\n\ntype LogRecordType = byte\n\nconst (\n\tLogRecordNormal = iota\n\tLogRecordDel"
},
{
"path": "storage/memtable.go",
"chars": 4153,
"preview": "package storage\n\nimport (\n\t\"fmt\"\n\t\"github.com/bwmarrin/snowflake\"\n\t\"github.com/dgraph-io/badger/skl\"\n\t\"github.com/dgraph"
},
{
"path": "storage/options.go",
"chars": 664,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"os\"\n)\n\ntype WalOptions struct {\n\tDirPath string\n\tMemTabl"
},
{
"path": "storage/pool.go",
"chars": 424,
"preview": "package storage\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n)\n\ntype bufferPool struct {\n\tbuffer sync.Pool\n}\n\nfunc (p *bufferPool) Get() *"
},
{
"path": "storage/segmentfile.go",
"chars": 5618,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\tlru \"github.com/hashicorp/gol"
},
{
"path": "storage/tinywal.go",
"chars": 5375,
"preview": "package storage\n\nimport (\n\t_const \"SmartStashDB/const\"\n\t\"errors\"\n\t\"fmt\"\n\tlru \"github.com/hashicorp/golang-lru/v2\"\n\t\"io/f"
}
]
About this extraction
This page contains the full source code of the Johonsoy/SmartStashDB GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (31.4 KB), approximately 9.4k tokens, and a symbol index with 90 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.