├── go.sum ├── go.mod ├── config └── config.go ├── database ├── models │ └── models.go ├── db │ └── database.go ├── collections │ └── collections.go ├── document │ └── document.go └── utils │ └── utils.go ├── main.go └── README.md /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module Build-your-own-database 2 | 3 | go 1.23.4 4 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // config.go 2 | 3 | package config 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | ) 9 | 10 | var ( 11 | // BasePath is the root directory where databases will be stored 12 | BasePath = getEnv("BASE_PATH", "C:\\Users\\bhargav\\OneDrive\\Desktop\\DatabaseStorage") 13 | ) 14 | 15 | // getEnv is a helper function to read environment variables with a default fallback 16 | func getEnv(key string, defaultValue string) string { 17 | value, exists := os.LookupEnv(key) 18 | if !exists { 19 | return defaultValue 20 | } 21 | return value 22 | } 23 | 24 | // Validate checks if the required configurations are set correctly 25 | func Validate() error { 26 | // You can add any other checks or validations for configuration 27 | if BasePath == "" { 28 | return fmt.Errorf("BASE_PATH is not configured") 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /database/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "sync" 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | 13 | // GoDB is the central database manager 14 | type GoDB struct { 15 | Databases map[string]*Database // Stores all databases 16 | Mutex sync.RWMutex // Protects access to Databases 17 | } 18 | 19 | // Database represents a database in the system 20 | type Database struct { 21 | Name string `json:"name"` // Name of the database 22 | Path string `json:"path"` // Path where the database is stored 23 | Collections map[string]*Collection `json:"collections"` // List of collections in the database 24 | Mutex sync.RWMutex // Protects access to Collections 25 | } 26 | 27 | // Collection represents a collection inside a database 28 | type Collection struct { 29 | Name string `json:"name"` 30 | Path string `json:"path"` 31 | Documents map[string]*Document `json:"documents"` 32 | 33 | mu sync.Mutex `json:"-"` // Prevent mutex from being serialized 34 | } 35 | 36 | 37 | // Document represents an individual document inside a collection 38 | type Document struct { 39 | ID string `json:"id"` // Document ID 40 | Name string `json:"name"` 41 | Data map[string]interface{} `json:"data"` // Key-value data 42 | Path string `json:"path"` // Path to the file on disk (optional) 43 | } 44 | 45 | // KeyValue represents a single key-value pair 46 | type KeyValue struct { 47 | Key string `json:"key"` // Key name 48 | Value interface{} `json:"value"` // Value associated with the key 49 | } 50 | 51 | // Response represents a generic response for operations 52 | type Response struct { 53 | Success bool `json:"success"` // Indicates success or failure 54 | Message string `json:"message"` // Descriptive message 55 | Data interface{} `json:"data"` // Any additional data (optional) 56 | } 57 | 58 | 59 | func (d *Document) Add(key string, value interface{}) error { 60 | if _, exists := d.Data[key]; exists { 61 | return fmt.Errorf("key '%s' already exists", key) 62 | } 63 | d.Data[key] = value 64 | return d.save() 65 | } 66 | 67 | func (d *Document) Find(key string) (interface{}, bool) { 68 | val, ok := d.Data[key] 69 | return val, ok 70 | } 71 | 72 | func (d *Document) Update(key string, value interface{}) error { 73 | if _, exists := d.Data[key]; !exists { 74 | return fmt.Errorf("key '%s' not found", key) 75 | } 76 | d.Data[key] = value 77 | return d.save() 78 | } 79 | 80 | func (d *Document) DeleteKey(key string) error { 81 | if _, exists := d.Data[key]; !exists { 82 | return fmt.Errorf("key '%s' not found", key) 83 | } 84 | delete(d.Data, key) 85 | return d.save() 86 | } 87 | 88 | func (d *Document) Rename(newID string) error { 89 | newPath := filepath.Join(filepath.Dir(d.Path), newID+".json") 90 | if err := os.Rename(d.Path, newPath); err != nil { 91 | return err 92 | } 93 | d.ID = newID 94 | d.Path = newPath 95 | return d.save() 96 | } 97 | 98 | func (d *Document) save() error { 99 | data, err := json.MarshalIndent(d, "", " ") 100 | if err != nil { 101 | return err 102 | } 103 | return os.WriteFile(d.Path, data, 0644) 104 | } 105 | -------------------------------------------------------------------------------- /database/db/database.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "Build-your-own-database/config" 10 | "Build-your-own-database/database/models" 11 | ) 12 | 13 | type DBManager struct { 14 | goDB *models.GoDB 15 | basePath string 16 | mu sync.RWMutex 17 | } 18 | 19 | func NewDBManager() *DBManager { 20 | manager := &DBManager{ 21 | goDB: &models.GoDB{ 22 | Databases: make(map[string]*models.Database), 23 | }, 24 | basePath: config.BasePath, 25 | } 26 | manager.loadDatabases() 27 | return manager 28 | } 29 | 30 | func (dbm *DBManager) loadDatabases() { 31 | dbm.mu.Lock() 32 | defer dbm.mu.Unlock() 33 | 34 | entries, err := os.ReadDir(dbm.basePath) 35 | if err != nil { 36 | fmt.Println("Error reading basePath:", err) 37 | return 38 | } 39 | 40 | for _, entry := range entries { 41 | if entry.IsDir() { 42 | dbName := entry.Name() 43 | dbPath := filepath.Join(dbm.basePath, dbName) 44 | 45 | dbm.goDB.Mutex.Lock() 46 | dbm.goDB.Databases[dbName] = &models.Database{ 47 | Name: dbName, 48 | Path: dbPath, 49 | Collections: make(map[string]*models.Collection), 50 | } 51 | dbm.goDB.Mutex.Unlock() 52 | 53 | fmt.Println("Loaded database:", dbName) 54 | } 55 | } 56 | } 57 | 58 | func (dbm *DBManager) CreateDatabase(name string) (*models.Database, error) { 59 | dbm.mu.Lock() 60 | defer dbm.mu.Unlock() 61 | 62 | dbm.goDB.Mutex.RLock() 63 | _, exists := dbm.goDB.Databases[name] 64 | dbm.goDB.Mutex.RUnlock() 65 | 66 | if exists { 67 | return nil, fmt.Errorf("database '%s' already exists", name) 68 | } 69 | 70 | dbPath := filepath.Join(dbm.basePath, name) 71 | if err := os.MkdirAll(dbPath, os.ModePerm); err != nil { 72 | return nil, fmt.Errorf("failed to create database '%s': %v", name, err) 73 | } 74 | 75 | db := &models.Database{ 76 | Name: name, 77 | Path: dbPath, 78 | Collections: make(map[string]*models.Collection), 79 | } 80 | 81 | dbm.goDB.Mutex.Lock() 82 | dbm.goDB.Databases[name] = db 83 | dbm.goDB.Mutex.Unlock() 84 | 85 | fmt.Println("Database created:", name) 86 | return db, nil 87 | } 88 | 89 | func (dbm *DBManager) UseDatabase(name string) (*models.Database, error) { 90 | dbm.mu.RLock() 91 | defer dbm.mu.RUnlock() 92 | 93 | dbm.goDB.Mutex.RLock() 94 | db, exists := dbm.goDB.Databases[name] 95 | dbm.goDB.Mutex.RUnlock() 96 | 97 | if exists { 98 | fmt.Println("Using database:", name) 99 | return db, nil 100 | } 101 | 102 | dbPath := filepath.Join(dbm.basePath, name) 103 | if _, err := os.Stat(dbPath); os.IsNotExist(err) { 104 | return nil, fmt.Errorf("database '%s' does not exist", name) 105 | } 106 | 107 | db = &models.Database{ 108 | Name: name, 109 | Path: dbPath, 110 | Collections: make(map[string]*models.Collection), 111 | } 112 | 113 | dbm.goDB.Mutex.Lock() 114 | dbm.goDB.Databases[name] = db 115 | dbm.goDB.Mutex.Unlock() 116 | 117 | fmt.Println("Using database:", name) 118 | return db, nil 119 | } 120 | 121 | func (dbm *DBManager) DeleteDatabase(name string) error { 122 | dbm.mu.Lock() 123 | defer dbm.mu.Unlock() 124 | 125 | dbm.goDB.Mutex.Lock() 126 | db, exists := dbm.goDB.Databases[name] 127 | dbm.goDB.Mutex.Unlock() 128 | 129 | if !exists { 130 | dbPath := filepath.Join(dbm.basePath, name) 131 | if _, err := os.Stat(dbPath); os.IsNotExist(err) { 132 | return fmt.Errorf("database '%s' does not exist", name) 133 | } 134 | db = &models.Database{ 135 | Name: name, 136 | Path: filepath.Join(dbm.basePath, name), 137 | } 138 | } 139 | 140 | if err := os.RemoveAll(db.Path); err != nil { 141 | return fmt.Errorf("failed to delete database '%s': %v", name, err) 142 | } 143 | 144 | dbm.goDB.Mutex.Lock() 145 | delete(dbm.goDB.Databases, name) 146 | dbm.goDB.Mutex.Unlock() 147 | 148 | fmt.Println("Database deleted:", name) 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "Build-your-own-database/database/collections" 7 | "Build-your-own-database/database/db" 8 | "Build-your-own-database/database/document" 9 | ) 10 | 11 | func main() { 12 | // Initialize DB Manager 13 | dbManager := db.NewDBManager() 14 | 15 | dbName := "test_db" 16 | colName := "test_collection" 17 | 18 | // Step 1: Create Database 19 | _, err := dbManager.CreateDatabase(dbName) 20 | if err != nil { 21 | fmt.Println("Error creating database:", err) 22 | return 23 | } 24 | fmt.Println("✅ Database created") 25 | 26 | // Step 2: Use Database 27 | selectedDB, err := dbManager.UseDatabase(dbName) 28 | if err != nil { 29 | fmt.Println("Error selecting database:", err) 30 | return 31 | } 32 | fmt.Println("✅ Using database:", dbName) 33 | 34 | // Step 3: Collection Manager 35 | colManager := collections.NewCollectionManager(selectedDB) 36 | 37 | // Step 4: Create Collection 38 | _, err = colManager.CreateCollection(colName) 39 | if err != nil { 40 | fmt.Println("Error creating collection:", err) 41 | return 42 | } 43 | fmt.Println("✅ Collection created") 44 | 45 | // Step 5: Use Collection 46 | collection, err := colManager.UseCollection(colName) 47 | if err != nil { 48 | fmt.Println("Error using collection:", err) 49 | return 50 | } 51 | fmt.Println("✅ Using collection:", colName) 52 | 53 | // Step 6: Document Manager 54 | docManager := documents.NewDocumentManager(collection) 55 | 56 | // Step 6.1: Create a document 57 | _, err = docManager.CreateDocument("doc1", map[string]interface{}{ 58 | "name": "Alice", 59 | "age": 24, 60 | }) 61 | if err != nil { 62 | fmt.Println("❌ Error creating doc1:", err) 63 | return 64 | } 65 | fmt.Println("✅ Created document: doc1") 66 | 67 | // Step 6.2: Use the document 68 | doc, err := docManager.UseDocument("doc1") 69 | if err != nil { 70 | fmt.Println("❌ Error using doc1:", err) 71 | return 72 | } 73 | 74 | // Step 6.3: Add field 75 | if err := doc.Add("email", "alice@example.com"); err != nil { 76 | fmt.Println("❌ Add failed:", err) 77 | } else { 78 | fmt.Println("✅ Added email field") 79 | } 80 | 81 | // Step 6.4: Find field 82 | if val, ok := doc.Find("email"); ok { 83 | fmt.Println("✅ Found email:", val) 84 | } else { 85 | fmt.Println("❌ Email not found") 86 | } 87 | 88 | // Step 6.5: Update field 89 | if err := doc.Update("email", "alice@new.com"); err != nil { 90 | fmt.Println("❌ Update failed:", err) 91 | } else { 92 | fmt.Println("✅ Updated email") 93 | } 94 | 95 | // Step 6.6: Delete a key 96 | if err := doc.DeleteKey("age"); err != nil { 97 | fmt.Println("❌ Delete key failed:", err) 98 | } else { 99 | fmt.Println("✅ Deleted age key") 100 | } 101 | 102 | // Step 6.7: Rename document 103 | if err := docManager.RenameDocument("doc1", "doc1_renamed"); err != nil { 104 | fmt.Println("❌ Rename failed:", err) 105 | } else { 106 | fmt.Println("✅ Renamed document to doc1_renamed") 107 | } 108 | 109 | // Step 6.8: Find document by value 110 | results := docManager.FindDocument("name", "Alice") 111 | fmt.Printf("✅ Found %d doc(s) with name=Alice\n", len(results)) 112 | for _, d := range results { 113 | fmt.Printf("→ %s: %+v\n", d.Name, d.Data) 114 | } 115 | 116 | // Step 6.9: Delete the document 117 | if err := docManager.DeleteDocument("doc1_renamed"); err != nil { 118 | fmt.Println("❌ Document deletion failed:", err) 119 | } else { 120 | fmt.Println("✅ Deleted document: doc1_renamed") 121 | } 122 | 123 | // Step 7: Delete Collection 124 | if err := colManager.DeleteCollection(colName); err != nil { 125 | fmt.Println("❌ Collection deletion failed:", err) 126 | } else { 127 | fmt.Println("✅ Deleted collection:", colName) 128 | } 129 | 130 | // Step 8: Delete Database 131 | if err := dbManager.DeleteDatabase(dbName); err != nil { 132 | fmt.Println("❌ Database deletion failed:", err) 133 | } else { 134 | fmt.Println("✅ Deleted database:", dbName) 135 | } 136 | 137 | fmt.Println("🎉 All operations completed successfully!") 138 | } 139 | -------------------------------------------------------------------------------- /database/collections/collections.go: -------------------------------------------------------------------------------- 1 | package collections 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | "Build-your-own-database/config" 11 | "Build-your-own-database/database/models" 12 | ) 13 | 14 | // CollectionManager handles operations related to collections within a database 15 | type CollectionManager struct { 16 | db *models.Database 17 | colMux sync.RWMutex 18 | } 19 | 20 | // NewCollectionManager initializes a CollectionManager for a given database 21 | func NewCollectionManager(db *models.Database) *CollectionManager { 22 | return &CollectionManager{db: db} 23 | } 24 | 25 | // CreateCollection creates a new collection inside the database and persists it 26 | func (cm *CollectionManager) CreateCollection(name string) (*models.Collection, error) { 27 | cm.colMux.Lock() 28 | defer cm.colMux.Unlock() 29 | 30 | // Check if the collection already exists in memory 31 | if _, exists := cm.db.Collections[name]; exists { 32 | return nil, fmt.Errorf("collection '%s' already exists", name) 33 | } 34 | 35 | // Define the collection path using config.BasePath 36 | colPath := filepath.Join(config.BasePath, cm.db.Name, name) 37 | 38 | // Ensure the collection directory is created 39 | if err := os.MkdirAll(colPath, os.ModePerm); err != nil { 40 | return nil, fmt.Errorf("failed to create collection directory '%s': %v", name, err) 41 | } 42 | 43 | // Create collection object 44 | collection := &models.Collection{ 45 | Name: name, 46 | Documents: make(map[string]*models.Document), 47 | Path: colPath, 48 | } 49 | 50 | // Persist collection metadata 51 | if err := cm.saveCollection(collection); err != nil { 52 | return nil, fmt.Errorf("failed to save collection metadata: %v", err) 53 | } 54 | 55 | // Store in memory 56 | cm.db.Collections[name] = collection 57 | 58 | fmt.Println("Collection created:", name) 59 | return collection, nil 60 | } 61 | 62 | // UseCollection retrieves an existing collection, loading from disk if necessary 63 | func (cm *CollectionManager) UseCollection(name string) (*models.Collection, error) { 64 | cm.colMux.RLock() 65 | collection, exists := cm.db.Collections[name] 66 | cm.colMux.RUnlock() 67 | 68 | if exists { 69 | fmt.Println("Using collection from memory:", name) 70 | return collection, nil 71 | } 72 | 73 | // Lock for write since we're modifying the map 74 | cm.colMux.Lock() 75 | defer cm.colMux.Unlock() 76 | 77 | // Double check in case another thread loaded it in the meantime 78 | if collection, exists = cm.db.Collections[name]; exists { 79 | fmt.Println("Using collection from memory (after recheck):", name) 80 | return collection, nil 81 | } 82 | 83 | // Load collection from disk 84 | loadedCollection, err := cm.loadCollection(name) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | cm.db.Collections[name] = loadedCollection 90 | fmt.Println("Loaded collection from disk:", name) 91 | return loadedCollection, nil 92 | } 93 | 94 | // DeleteCollection removes a collection from the database and disk 95 | func (cm *CollectionManager) DeleteCollection(name string) error { 96 | cm.colMux.Lock() 97 | defer cm.colMux.Unlock() 98 | 99 | // Check if collection exists 100 | if _, exists := cm.db.Collections[name]; !exists { 101 | return fmt.Errorf("collection '%s' does not exist", name) 102 | } 103 | 104 | // Define collection path 105 | colPath := filepath.Join(config.BasePath, cm.db.Name, name) 106 | 107 | // Delete collection directory from disk 108 | if err := os.RemoveAll(colPath); err != nil { 109 | return fmt.Errorf("failed to delete collection '%s' from disk: %v", name, err) 110 | } 111 | 112 | // Remove from memory 113 | delete(cm.db.Collections, name) 114 | 115 | fmt.Println("Collection deleted:", name) 116 | return nil 117 | } 118 | 119 | // saveCollection writes the collection metadata to a JSON file 120 | func (cm *CollectionManager) saveCollection(collection *models.Collection) error { 121 | metadataPath := filepath.Join(collection.Path, "metadata.json") 122 | file, err := os.Create(metadataPath) 123 | if err != nil { 124 | return err 125 | } 126 | defer file.Close() 127 | 128 | encoder := json.NewEncoder(file) 129 | return encoder.Encode(collection) 130 | } 131 | 132 | // loadCollection reads a collection from its metadata file 133 | func (cm *CollectionManager) loadCollection(name string) (*models.Collection, error) { 134 | colPath := filepath.Join(config.BasePath, cm.db.Name, name) 135 | metadataPath := filepath.Join(colPath, "metadata.json") 136 | 137 | file, err := os.Open(metadataPath) 138 | if err != nil { 139 | if os.IsNotExist(err) { 140 | return nil, fmt.Errorf("collection '%s' does not exist on disk", name) 141 | } 142 | return nil, err 143 | } 144 | defer file.Close() 145 | 146 | var collection models.Collection 147 | decoder := json.NewDecoder(file) 148 | if err := decoder.Decode(&collection); err != nil { 149 | return nil, err 150 | } 151 | 152 | // Ensure Documents map is initialized 153 | if collection.Documents == nil { 154 | collection.Documents = make(map[string]*models.Document) 155 | } 156 | 157 | // Set the path after loading 158 | collection.Path = colPath 159 | 160 | return &collection, nil 161 | } 162 | -------------------------------------------------------------------------------- /database/document/document.go: -------------------------------------------------------------------------------- 1 | package documents 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "Build-your-own-database/database/models" 13 | ) 14 | 15 | type DocumentManager struct { 16 | collection *models.Collection 17 | docMux sync.RWMutex 18 | } 19 | 20 | // Constructor 21 | func NewDocumentManager(collection *models.Collection) *DocumentManager { 22 | return &DocumentManager{ 23 | collection: collection, 24 | } 25 | } 26 | 27 | // Generate a random ID (internal use only) 28 | func generateRandomID() string { 29 | bytes := make([]byte, 8) 30 | _, _ = rand.Read(bytes) 31 | return hex.EncodeToString(bytes) 32 | } 33 | 34 | // 1. CreateDocument (by name) 35 | func (dm *DocumentManager) CreateDocument(name string, data map[string]interface{}) (*models.Document, error) { 36 | dm.docMux.Lock() 37 | defer dm.docMux.Unlock() 38 | 39 | // Check if name already exists 40 | for _, doc := range dm.collection.Documents { 41 | if doc.Name == name { 42 | return nil, fmt.Errorf("document with name '%s' already exists", name) 43 | } 44 | } 45 | 46 | id := generateRandomID() 47 | docPath := filepath.Join(dm.collection.Path, id+".json") 48 | doc := &models.Document{ 49 | ID: id, 50 | Name: name, 51 | Data: data, 52 | Path: docPath, 53 | } 54 | 55 | dm.collection.Documents[id] = doc 56 | 57 | // Save to disk 58 | file, err := os.Create(docPath) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to create document file: %v", err) 61 | } 62 | defer file.Close() 63 | 64 | if err := json.NewEncoder(file).Encode(doc); err != nil { 65 | return nil, fmt.Errorf("failed to encode document: %v", err) 66 | } 67 | 68 | fmt.Println("Created document:", name) 69 | return doc, nil 70 | } 71 | 72 | // 2. UseDocument (by name) 73 | func (dm *DocumentManager) UseDocument(name string) (*models.Document, error) { 74 | dm.docMux.RLock() 75 | for _, doc := range dm.collection.Documents { 76 | if doc.Name == name { 77 | dm.docMux.RUnlock() 78 | fmt.Println("Using document from memory:", name) 79 | return doc, nil 80 | } 81 | } 82 | dm.docMux.RUnlock() 83 | 84 | // Not in memory? Load from disk 85 | files, err := os.ReadDir(dm.collection.Path) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed to read collection directory: %v", err) 88 | } 89 | 90 | for _, file := range files { 91 | if filepath.Ext(file.Name()) == ".json" { 92 | path := filepath.Join(dm.collection.Path, file.Name()) 93 | f, err := os.Open(path) 94 | if err != nil { 95 | continue 96 | } 97 | 98 | var doc models.Document 99 | if err := json.NewDecoder(f).Decode(&doc); err == nil { 100 | f.Close() 101 | if doc.Name == name { 102 | doc.Path = path 103 | dm.docMux.Lock() 104 | dm.collection.Documents[doc.ID] = &doc 105 | dm.docMux.Unlock() 106 | fmt.Println("Loaded document from disk:", name) 107 | return &doc, nil 108 | } 109 | } 110 | f.Close() 111 | } 112 | } 113 | 114 | return nil, fmt.Errorf("document '%s' does not exist", name) 115 | } 116 | 117 | // 3. DeleteDocument (by name) 118 | func (dm *DocumentManager) DeleteDocument(name string) error { 119 | dm.docMux.Lock() 120 | defer dm.docMux.Unlock() 121 | 122 | for id, doc := range dm.collection.Documents { 123 | if doc.Name == name { 124 | if err := os.Remove(doc.Path); err != nil { 125 | return fmt.Errorf("failed to delete document file: %v", err) 126 | } 127 | delete(dm.collection.Documents, id) 128 | fmt.Println("Deleted document:", name) 129 | return nil 130 | } 131 | } 132 | 133 | return fmt.Errorf("document '%s' does not exist", name) 134 | } 135 | 136 | // 4. RenameDocument (by name) 137 | func (dm *DocumentManager) RenameDocument(oldName, newName string) error { 138 | dm.docMux.Lock() 139 | defer dm.docMux.Unlock() 140 | 141 | // Check if newName already exists 142 | for _, d := range dm.collection.Documents { 143 | if d.Name == newName { 144 | return fmt.Errorf("document '%s' already exists", newName) 145 | } 146 | } 147 | 148 | for _, doc := range dm.collection.Documents { 149 | if doc.Name == oldName { 150 | doc.Name = newName 151 | 152 | // Save with updated name 153 | file, err := os.Create(doc.Path) 154 | if err != nil { 155 | return fmt.Errorf("failed to update renamed doc: %v", err) 156 | } 157 | defer file.Close() 158 | if err := json.NewEncoder(file).Encode(doc); err != nil { 159 | return fmt.Errorf("failed to encode renamed doc: %v", err) 160 | } 161 | 162 | fmt.Printf("Renamed document '%s' to '%s'\n", oldName, newName) 163 | return nil 164 | } 165 | } 166 | 167 | return fmt.Errorf("document '%s' not found", oldName) 168 | } 169 | 170 | // 5. FindDocument (by key-value inside data) 171 | func (dm *DocumentManager) FindDocument(key string, val interface{}) []*models.Document { 172 | dm.docMux.RLock() 173 | defer dm.docMux.RUnlock() 174 | 175 | var results []*models.Document 176 | for _, doc := range dm.collection.Documents { 177 | if v, ok := doc.Data[key]; ok && v == val { 178 | results = append(results, doc) 179 | } 180 | } 181 | 182 | fmt.Printf("Found %d document(s) matching %s = %v\n", len(results), key, val) 183 | return results 184 | } 185 | -------------------------------------------------------------------------------- /database/utils/utils.go: -------------------------------------------------------------------------------- 1 | package keyvalues 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | "Build-your-own-database/config" 10 | ) 11 | 12 | // Base path for storing databases (replace with your actual base path) 13 | var basePath = config.BasePath 14 | 15 | // Mutex for concurrency handling 16 | var mu sync.Mutex 17 | 18 | // SetKeyValue sets a key-value pair in a specified document 19 | func SetKeyValue(dbName, docName, key string, value interface{}) error { 20 | mu.Lock() // Lock the function to ensure concurrency safety 21 | defer mu.Unlock() // Ensure unlocking after the function completes 22 | 23 | // Construct the full path to the document 24 | docPath := filepath.Join(basePath, dbName, docName+".json") 25 | 26 | // Check if the document exists 27 | if _, err := os.Stat(docPath); os.IsNotExist(err) { 28 | return fmt.Errorf("document '%s' does not exist in database '%s'", docName, dbName) 29 | } 30 | 31 | // Open the document file for reading 32 | file, err := os.Open(docPath) 33 | if err != nil { 34 | return fmt.Errorf("failed to open document '%s': %v", docName, err) 35 | } 36 | defer file.Close() 37 | 38 | // Decode the existing document content 39 | content := make(map[string]interface{}) 40 | decoder := json.NewDecoder(file) 41 | err = decoder.Decode(&content) 42 | if err != nil { 43 | return fmt.Errorf("failed to decode document '%s': %v", docName, err) 44 | } 45 | 46 | // Set the key-value pair 47 | content[key] = value 48 | 49 | // Write the updated content back to the file 50 | file, err = os.Create(docPath) // Overwrite the file 51 | if err != nil { 52 | return fmt.Errorf("failed to write to document '%s': %v", docName, err) 53 | } 54 | defer file.Close() 55 | 56 | encoder := json.NewEncoder(file) 57 | err = encoder.Encode(content) 58 | if err != nil { 59 | return fmt.Errorf("failed to encode content to document '%s': %v", docName, err) 60 | } 61 | 62 | fmt.Printf("Key '%s' set to '%v' in document '%s'.\n", key, value, docName) 63 | return nil 64 | } 65 | 66 | // GetKeyValue retrieves the value of a key in a specified document 67 | func GetKeyValue(dbName, docName, key string) (interface{}, error) { 68 | mu.Lock() // Lock the function to ensure concurrency safety 69 | defer mu.Unlock() // Ensure unlocking after the function completes 70 | 71 | // Construct the full path to the document 72 | docPath := filepath.Join(basePath, dbName, docName+".json") 73 | 74 | // Check if the document exists 75 | if _, err := os.Stat(docPath); os.IsNotExist(err) { 76 | return nil, fmt.Errorf("document '%s' does not exist in database '%s'", docName, dbName) 77 | } 78 | 79 | // Open the document file for reading 80 | file, err := os.Open(docPath) 81 | if err != nil { 82 | return nil, fmt.Errorf("failed to open document '%s': %v", docName, err) 83 | } 84 | defer file.Close() 85 | 86 | // Decode the existing document content 87 | content := make(map[string]interface{}) 88 | decoder := json.NewDecoder(file) 89 | err = decoder.Decode(&content) 90 | if err != nil { 91 | return nil, fmt.Errorf("failed to decode document '%s': %v", docName, err) 92 | } 93 | 94 | // Retrieve the value for the given key 95 | value, exists := content[key] 96 | if !exists { 97 | return nil, fmt.Errorf("key '%s' does not exist in document '%s'", key, docName) 98 | } 99 | 100 | return value, nil 101 | } 102 | // DeleteKeyValue removes a key-value pair from a specified document without using delete(). 103 | func DeleteKeyValue(dbName, docName, key string) error { 104 | mu.Lock() // Lock the function to ensure concurrency safety 105 | defer mu.Unlock() // Ensure unlocking after the function completes 106 | 107 | // Construct the full path to the document 108 | docPath := filepath.Join(basePath, dbName, docName+".json") 109 | 110 | // Check if the document exists 111 | if _, err := os.Stat(docPath); os.IsNotExist(err) { 112 | return fmt.Errorf("document '%s' does not exist in database '%s'", docName, dbName) 113 | } 114 | 115 | // Open the document file for reading 116 | file, err := os.Open(docPath) 117 | if err != nil { 118 | return fmt.Errorf("failed to open document '%s': %v", docName, err) 119 | } 120 | defer file.Close() 121 | 122 | // Decode the existing document content 123 | content := make(map[string]interface{}) 124 | decoder := json.NewDecoder(file) 125 | err = decoder.Decode(&content) 126 | if err != nil { 127 | return fmt.Errorf("failed to decode document '%s': %v", docName, err) 128 | } 129 | 130 | // Check if the key exists 131 | _, exists := content[key] 132 | if !exists { 133 | return fmt.Errorf("key '%s' does not exist in document '%s'", key, docName) 134 | } 135 | 136 | // Create a new map without the key 137 | newContent := make(map[string]interface{}) 138 | for k, v := range content { 139 | if k != key { 140 | newContent[k] = v 141 | } 142 | } 143 | 144 | // Write the updated content back to the file 145 | file, err = os.Create(docPath) // Overwrite the file 146 | if err != nil { 147 | return fmt.Errorf("failed to write to document '%s': %v", docName, err) 148 | } 149 | defer file.Close() 150 | 151 | encoder := json.NewEncoder(file) 152 | err = encoder.Encode(newContent) 153 | if err != nil { 154 | return fmt.Errorf("failed to encode content to document '%s': %v", docName, err) 155 | } 156 | 157 | fmt.Printf("Key '%s' deleted from document '%s'.\n", key, docName) 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Build Your Own Database 4 | 📦 A lightweight, file-based database built from scratch using Go – **no external DBs**, **no APIs**, just pure Go code! 5 | Everything is handled via the `main.go` file, which acts as the client interface for interacting with the database system. 6 | 7 | --- 8 | 9 | ## 🧠 What Is This? 10 | 11 | This project is a simple yet powerful key-value document database system implemented entirely in Go. It allows users to: 12 | 13 | - Create/delete **databases** 14 | - Create/delete **documents** inside databases 15 | - **Access**, **modify**, **rename**, and **delete** key-value pairs inside documents 16 | - **Fetch documents** using their **name** 17 | - Perform all operations from the **command line**, with a focus on modular, extensible architecture 18 | 19 | All data is stored **locally in files** and persists between sessions. Internally, the system uses **UUIDs**, but users interact with it using **names** for ease of access. 20 | 21 | --- 22 | 23 | ## ✨ Features 24 | 25 | - ✅ Create & delete databases 26 | - ✅ Create & delete documents by name 27 | - ✅ Add/update/delete key-value pairs in a document 28 | - ✅ Fetch documents by name 29 | - ✅ Rename document names 30 | - ✅ File-based storage (JSON) 31 | - ✅ Concurrency-safe using Go mutexes 32 | - ✅ Modular code structure 33 | 34 | --- 35 | 36 | ## 🛠 Technologies Used 37 | 38 | - **Go** – Core language 39 | - **JSON** – For persistent document storage 40 | - **UUID** – For unique internal document IDs 41 | - **Mutex** – For handling concurrent operations safely 42 | 43 | --- 44 | 45 | ## 📁 Project Structure 46 | 47 | ``` 48 | ├── config/ 49 | │ └── config.go # Configuration for base file path 50 | ├── database/ 51 | │ ├── db/ 52 | │ │ └── database.go # Functions for DB creation/deletion 53 | │ ├── document/ 54 | │ │ └── document.go # Document creation/deletion, renaming 55 | │ │ └── document.go # Add/update/delete key-value pairs 56 | │ ├── models/ 57 | │ │ └── models.go # Data models for DB and documents 58 | │ └── utils/ 59 | │ └── utils.go # File and helper utilities 60 | ├── main.go # CLI entry point for all operations 61 | ├── go.mod # Go module definition 62 | ├── go.sum # Go dependency checksum 63 | └── README.md # You're reading it! 64 | ``` 65 | 66 | --- 67 | 68 | ## 🚀 Getting Started 69 | 70 | ```bash 71 | git clone https://github.com/bhargav-yarlagadda/build-your-own-database.git 72 | cd build-your-own-database 73 | ``` 74 | 75 | Install dependencies: 76 | 77 | ```bash 78 | go mod tidy 79 | ``` 80 | 81 | Run the project: 82 | **main.go acts as a client that interacts with the db. take a glimpse of main.go to understand the working of the db.** 83 | ```bash 84 | go run main.go 85 | ``` 86 | 87 | > **Note**: You can modify the default file storage path by updating `config/config.go`. 88 | 89 | --- 90 | # Refactoring `dbManager.go` into `document_manager.go` and `collection_manager.go` 91 | 92 | ## Motivation 93 | The original `dbManager.go` file was handling logic for both collections and documents in a single place. This violated the **Single Responsibility Principle** and made the codebase harder to maintain and extend. 94 | 95 | ## Changes Made 96 | 97 | ### 1. Created New Files 98 | - `document_manager.go`: Handles all document-specific operations. 99 | - `collection_manager.go`: Handles all collection-specific operations. 100 | 101 | --- 102 | 103 | ### 2. Document Refactor Highlights 104 | 105 | #### ✅ New Struct: `DocumentManager` 106 | - Encapsulates all document-specific CRUD operations. 107 | 108 | #### ✅ Thread-Safety 109 | - Introduced `sync.RWMutex` (`docMux`) to ensure concurrent read/write safety while accessing or modifying documents. 110 | 111 | #### ✅ Clear Method Separation 112 | Each method is focused on a single task: 113 | - `CreateDocument` 114 | - `UseDocument` 115 | - `UpdateDocument` 116 | - `DeleteDocument` 117 | - `FetchDocument` 118 | - `RenameDocument` 119 | - `DeleteKey` 120 | 121 | #### ✅ File I/O Improvements 122 | - Used `json.NewEncoder`/`Decoder` consistently. 123 | - Ensured proper file closing using `defer`. 124 | 125 | #### ✅ Error Handling 126 | - Added meaningful error messages. 127 | - Ensured consistency in error format and logging. 128 | 129 | #### ✅ In-Memory Caching 130 | - When a document is used, it is loaded into memory if not already present. 131 | 132 | --- 133 | 134 | ### 3. Collection Refactor Highlights 135 | 136 | #### ✅ New Struct: `CollectionManager` 137 | - Handles collection-level logic such as: 138 | - `CreateCollection` 139 | - `DeleteCollection` 140 | - `ListCollections` 141 | - `RenameCollection` 142 | - `LoadCollection` from disk 143 | 144 | #### ✅ Directory Structure 145 | - Each collection has its own subdirectory inside `./data`. 146 | 147 | #### ✅ Improved Initialization 148 | - Clean separation between initializing a collection and working with documents inside it. 149 | 150 | #### ✅ Mutex for Collection Safety 151 | - Collection-level operations are also thread-safe using `sync.Mutex`. 152 | 153 | --- 154 | ## Benefits 155 | - **Cleaner Code Structure**: Collections and documents now handled separately. 156 | - **Easier to Maintain**: Logical grouping of responsibilities. 157 | - **Better Concurrency**: Thread-safe reads and writes. 158 | - **Improved Readability**: Self-explanatory function names and simplified logic. 159 | 160 | ## To Do 161 | - Add unit tests for `DocumentManager` and `CollectionManager` 162 | - Extend functionality with indexing or search support 163 | - Integrate logging system instead of `fmt.Println` 164 | 165 | ## Example Usage 166 | ```go 167 | collection := collectionManager.CreateCollection("users") 168 | 169 | docManager := NewDocumentManager(collection) 170 | docManager.CreateDocument("user1", map[string]interface{}{"name": "John"}) 171 | ``` 172 | 173 | --- 174 | ✅ Refactored, modular, and scalable! 175 | 176 | 177 | 178 | 179 | 180 | --- 181 | 182 | ## 📌 To-Do's 183 | 184 | - Implement Distributed File Storage 185 | - Add CLI interface with flags (optional) 186 | - Enable nested key support 187 | - Add document versioning (optional history) 188 | - Unit tests for each module 189 | 190 | --- 191 | 192 | ## 🙌 Contribute 193 | 194 | Fork the repo, make your changes, and raise a PR! 195 | Suggestions and improvements are always welcome. 😊 196 | 197 | --- 198 | 199 | Let me know if you'd like me to generate badges, screenshots, or usage gifs for a finishing touch! 200 | --------------------------------------------------------------------------------