├── .gitignore ├── LICENSE ├── README.md ├── database.go ├── database_test.go ├── datastores ├── disk │ ├── disk.go │ ├── disk_test.go │ ├── table_file.go │ ├── tablefile_test.go │ ├── testdata │ │ └── contacts.bak │ └── testutils_test.go └── ram │ ├── ram.go │ ├── ram_test.go │ ├── table.go │ ├── table_test.go │ └── testutils_test.go ├── dberr └── dberr.go ├── examples ├── crud │ ├── crud.go │ ├── data │ │ ├── comments.json │ │ ├── episodes_default.txt │ │ └── hosts.json │ └── models │ │ ├── comments.go │ │ ├── episodes.go │ │ └── hosts.go └── dbadmin │ ├── compact │ ├── compact.go │ └── data │ │ └── .keep │ └── dbadmin │ ├── data │ └── .keep │ └── dbadmin.go ├── go.mod ├── hare.jpg ├── testdata └── contacts.bak └── testutils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/crud/data/episodes.json 2 | test_data/test.json 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 James Cribbs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hare - A nimble little database management system written in Go 4 | ==== 5 | 6 | Hare is a pure Go database management system that stores each table as 7 | a text file of line-delimited JSON. Each line of JSON represents a 8 | record. It is a good fit for applications that require a simple embedded DBMS. 9 | 10 | ## Table of Contents 11 | 12 | - [Getting Started](#getting-started) 13 | - [Installing](#installing) 14 | - [Usage](#usage) 15 | - [Features](#features) 16 | 17 | ## Getting Started 18 | 19 | ### Installing 20 | 21 | To start using Hare, install Go and run `go get`: 22 | 23 | ```sh 24 | $ go get github.com/jameycribbs/hare 25 | ``` 26 | 27 | 28 | ### Usage 29 | 30 | #### Setting up Hare to use your JSON file(s) 31 | 32 | A directory of JSON files is represented by a hare.Database. Each JSON file 33 | needs a struct with it's members cooresponding to the JSON field names. 34 | Additionally, you need to implement 3 simple boilerplate methods on that 35 | struct that allow it to satisfy the hare.Record interface. 36 | 37 | A good way to structure this is to put this boilerplate code in a "models" 38 | package in your project. You can find an example of this boilerplate code in the 39 | examples/crud/models/episodes.go file. 40 | 41 | Now you are ready to go! 42 | 43 | Let's say you have a "data" directory with a file in it called "contacts.json". 44 | 45 | The top-level object in Hare is a `Database`. It represents the directory on 46 | your disk where the JSON files are located. 47 | 48 | To open your database, you first need a new instance of a datastore. In this 49 | example, we are using the `Disk` datastore: 50 | 51 | ```go 52 | ds, err := disk.New("./data", ".json") 53 | ``` 54 | Hare also has the `Ram` datastore for in-memory databases. 55 | 56 | Now, you will pass the datastore to Hare's New function and it will return 57 | a `Database` instance: 58 | ```go 59 | db, err := hare.New(ds) 60 | ``` 61 | 62 | 63 | #### Creating a record 64 | 65 | To add a record, you can use the `Insert` method: 66 | 67 | ```go 68 | recID, err := db.Insert("contacts", &models.Contact{FirstName: "John", LastName: "Doe", Phone: "888-888-8888", Age: 21}) 69 | ``` 70 | 71 | 72 | #### Finding a record 73 | 74 | To find a record if you know the record ID, you can use the `Find` method: 75 | 76 | ```go 77 | var c models.Contact 78 | 79 | err = db.Find("contacts", 1, &c) 80 | ``` 81 | 82 | 83 | #### Updating a record 84 | 85 | To update a record, you can use the `Update` method: 86 | 87 | ```go 88 | c.Age = 22 89 | 90 | err = db.Update("contacts", &c) 91 | ``` 92 | 93 | 94 | #### Deleting a record 95 | 96 | To delete a record, you can use the `Delete` method: 97 | 98 | ```go 99 | err = db.Delete("contacts", 3) 100 | ``` 101 | 102 | 103 | #### Querying 104 | 105 | To query the database, you can write your query expression in pure Go and pass 106 | it to your model's QueryContacts function as a closure. You would need to create 107 | the QueryContacts function for your model as part of setup. You can find an 108 | example of what this function should look like in examples/models/episodes.go. 109 | 110 | ```go 111 | results, err := models.QueryContacts(db, func(c models.Contact) bool { 112 | return c.firstname == "John" && c.lastname == "Doe" 113 | }, 0) 114 | ``` 115 | 116 | 117 | #### Associations 118 | 119 | You can create associations (similar to "belongs_to" in Rails, but with less 120 | features). For example, you could create another table called "relationships" with 121 | the fields "id" and "type" (i.e. "Spouse", "Sister", "Co-worker", etc.). Next, 122 | you would add a "relationship_id" field to the contacts table and you would also add 123 | an embeded Relationship struct. Finally, in the Contact models "AfterFind" method, 124 | which is automatically called by Hare everytime the "Find" method is executed, you 125 | would add code to look-up the associated relationship and populate the embedded 126 | Relationship struct. Take a look at the crud.go file in the "examples" directory 127 | for an example of how this is done. 128 | 129 | You can also mimic a "has_many" association, using a similar technique. Take a 130 | look at the files in the examples directory for how to do that. 131 | 132 | 133 | #### Database Administration 134 | 135 | There are also built-in methods you can run against the database 136 | to create a new table or delete an existing table. Take a look at the 137 | examples/dbadmin/dbadmin.go file for examples of how these can be used. 138 | 139 | When Hare updates an existing record, if the changed record's length is 140 | less than the old record's length, Hare will overwrite the old data 141 | and pad the extra space on the line with all "X"s. 142 | 143 | If the changed record's length is greater than the old record's length, 144 | Hare will write the changed record at the end of the file and overwrite 145 | the old record with all "X"s. 146 | 147 | Similarly, when Hare deletes a record, it simply overwrites the record 148 | with all "X"s. 149 | 150 | Eventually, you will want to remove these obsolete records. For an 151 | example of how to do this, take a look at the examples/dbadmin/compact.go 152 | file. 153 | 154 | 155 | ## Features 156 | 157 | * Records for each table are stored in a newline-delimited JSON file. 158 | 159 | * Mutexes are used for table locking. You can have multiple readers 160 | or one writer for that table at one time, as long as all processes 161 | share the same Database connection. 162 | 163 | * Querying is done using Go itself. No need to use a DSL. 164 | 165 | * An AfterFind callback is run automatically, everytime a record is 166 | read, allowing you to do creative things like auto-populate 167 | associations, etc. 168 | 169 | * When using the `Disk` datastore, the database is not read into 170 | memory, but is queried from disk, so no need to worry about a large 171 | dataset filling up memory. Of course, if your database is THAT 172 | big, you should probably be using a real DBMS, instead of Hare! 173 | 174 | * Two different back-end datastores to choose from: `Disk` or `Ram`. 175 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | //Package hare implements a simple DBMS that stores it's data 2 | //in newline-delimited json files. 3 | package hare 4 | 5 | import ( 6 | "encoding/json" 7 | "sync" 8 | 9 | "github.com/jameycribbs/hare/dberr" 10 | ) 11 | 12 | // Record interface defines the methods a struct representing 13 | // a table record must implement. 14 | type Record interface { 15 | SetID(int) 16 | GetID() int 17 | AfterFind(*Database) error 18 | } 19 | 20 | type datastorage interface { 21 | Close() error 22 | CreateTable(string) error 23 | DeleteRec(string, int) error 24 | GetLastID(string) (int, error) 25 | IDs(string) ([]int, error) 26 | InsertRec(string, int, []byte) error 27 | ReadRec(string, int) ([]byte, error) 28 | RemoveTable(string) error 29 | TableExists(string) bool 30 | TableNames() []string 31 | UpdateRec(string, int, []byte) error 32 | } 33 | 34 | // Database struct is the main struct for the Hare package. 35 | type Database struct { 36 | store datastorage 37 | locks map[string]*sync.RWMutex 38 | lastIDs map[string]int 39 | } 40 | 41 | // New takes a datastorage and returns a pointer to a 42 | // Database struct. 43 | func New(ds datastorage) (*Database, error) { 44 | db := &Database{store: ds} 45 | db.locks = make(map[string]*sync.RWMutex) 46 | db.lastIDs = make(map[string]int) 47 | 48 | for _, tableName := range db.store.TableNames() { 49 | db.locks[tableName] = &sync.RWMutex{} 50 | 51 | lastID, err := db.store.GetLastID(tableName) 52 | if err != nil { 53 | return nil, err 54 | } 55 | db.lastIDs[tableName] = lastID 56 | } 57 | 58 | return db, nil 59 | } 60 | 61 | // Close closes the associated datastore. 62 | func (db *Database) Close() error { 63 | for _, lock := range db.locks { 64 | lock.Lock() 65 | } 66 | 67 | if err := db.store.Close(); err != nil { 68 | return err 69 | } 70 | 71 | for _, lock := range db.locks { 72 | lock.Unlock() 73 | } 74 | 75 | db.store = nil 76 | db.locks = nil 77 | db.lastIDs = nil 78 | 79 | return nil 80 | } 81 | 82 | // CreateTable takes a table name and creates and 83 | // initializes a new table. 84 | func (db *Database) CreateTable(tableName string) error { 85 | if db.TableExists(tableName) { 86 | return dberr.ErrTableExists 87 | } 88 | 89 | if err := db.store.CreateTable(tableName); err != nil { 90 | return nil 91 | } 92 | 93 | db.locks[tableName] = &sync.RWMutex{} 94 | 95 | lastID, err := db.store.GetLastID(tableName) 96 | if err != nil { 97 | return err 98 | } 99 | db.lastIDs[tableName] = lastID 100 | 101 | return nil 102 | } 103 | 104 | // Delete takes a table name and record id and removes that 105 | // record from the database. 106 | func (db *Database) Delete(tableName string, id int) error { 107 | if !db.TableExists(tableName) { 108 | return dberr.ErrNoTable 109 | } 110 | 111 | db.locks[tableName].Lock() 112 | defer db.locks[tableName].Unlock() 113 | 114 | if err := db.store.DeleteRec(tableName, id); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // DropTable takes a table name and deletes the table. 122 | func (db *Database) DropTable(tableName string) error { 123 | if !db.TableExists(tableName) { 124 | return dberr.ErrNoTable 125 | } 126 | 127 | db.locks[tableName].Lock() 128 | 129 | if err := db.store.RemoveTable(tableName); err != nil { 130 | db.locks[tableName].Unlock() 131 | return err 132 | } 133 | 134 | delete(db.lastIDs, tableName) 135 | 136 | db.locks[tableName].Unlock() 137 | 138 | delete(db.locks, tableName) 139 | 140 | return nil 141 | } 142 | 143 | // Find takes a table name, a record id, and a pointer to a struct that 144 | // implements the Record interface, finds the associated record from the 145 | // table, and populates the struct. 146 | func (db *Database) Find(tableName string, id int, rec Record) error { 147 | if !db.TableExists(tableName) { 148 | return dberr.ErrNoTable 149 | } 150 | 151 | db.locks[tableName].RLock() 152 | defer db.locks[tableName].RUnlock() 153 | 154 | rawRec, err := db.store.ReadRec(tableName, id) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | err = json.Unmarshal(rawRec, rec) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | err = rec.AfterFind(db) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | return nil 170 | } 171 | 172 | // IDs takes a table name and returns a list of all record ids for 173 | // that table. 174 | func (db *Database) IDs(tableName string) ([]int, error) { 175 | if !db.TableExists(tableName) { 176 | return nil, dberr.ErrNoTable 177 | } 178 | 179 | db.locks[tableName].Lock() 180 | defer db.locks[tableName].Unlock() 181 | 182 | ids, err := db.store.IDs(tableName) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | return ids, err 188 | } 189 | 190 | // Insert takes a table name and a struct that implements the Record 191 | // interface and adds a new record to the table. It returns the 192 | // new record's id. 193 | func (db *Database) Insert(tableName string, rec Record) (int, error) { 194 | if !db.TableExists(tableName) { 195 | return 0, dberr.ErrNoTable 196 | } 197 | 198 | db.locks[tableName].Lock() 199 | defer db.locks[tableName].Unlock() 200 | 201 | id := db.incrementLastID(tableName) 202 | rec.SetID(id) 203 | 204 | rawRec, err := json.Marshal(rec) 205 | if err != nil { 206 | return 0, err 207 | } 208 | 209 | if err := db.store.InsertRec(tableName, id, rawRec); err != nil { 210 | return 0, err 211 | } 212 | 213 | return id, nil 214 | } 215 | 216 | // TableExists takes a table name and returns true if the table exists, 217 | // false if it does not. 218 | func (db *Database) TableExists(tableName string) bool { 219 | return db.tableExists(tableName) && db.store.TableExists(tableName) 220 | } 221 | 222 | // Update takes a table name and a struct that implements the Record 223 | // interface and updates the record in the table that has that record's 224 | // id. 225 | func (db *Database) Update(tableName string, rec Record) error { 226 | if !db.TableExists(tableName) { 227 | return dberr.ErrNoTable 228 | } 229 | 230 | db.locks[tableName].Lock() 231 | defer db.locks[tableName].Unlock() 232 | 233 | id := rec.GetID() 234 | 235 | rawRec, err := json.Marshal(rec) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | if err := db.store.UpdateRec(tableName, id, rawRec); err != nil { 241 | return err 242 | } 243 | 244 | return nil 245 | } 246 | 247 | // unexported methods 248 | 249 | func (db *Database) incrementLastID(tableName string) int { 250 | lastID := db.lastIDs[tableName] 251 | 252 | lastID++ 253 | 254 | db.lastIDs[tableName] = lastID 255 | 256 | return lastID 257 | } 258 | 259 | func (db *Database) tableExists(tableName string) bool { 260 | _, ok := db.locks[tableName] 261 | if !ok { 262 | return false 263 | } 264 | _, ok = db.lastIDs[tableName] 265 | if !ok { 266 | return false 267 | } 268 | 269 | return ok 270 | } 271 | -------------------------------------------------------------------------------- /database_test.go: -------------------------------------------------------------------------------- 1 | package hare 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/jameycribbs/hare/datastores/disk" 10 | "github.com/jameycribbs/hare/datastores/ram" 11 | "github.com/jameycribbs/hare/dberr" 12 | ) 13 | 14 | func TestCloseDatabaseTests(t *testing.T) { 15 | var tests = []func(t *testing.T){ 16 | func(t *testing.T) { 17 | //Close Disk Database... 18 | 19 | ds, err := disk.New("./testdata", ".json") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | db, err := New(ds) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | db.Close() 29 | 30 | checkErr(t, dberr.ErrNoTable, db.Find("contacts", 3, &Contact{})) 31 | 32 | gotStore := db.store 33 | if nil != gotStore { 34 | t.Errorf("want %v; got %v", nil, gotStore) 35 | } 36 | 37 | gotLocks := db.locks 38 | if nil != gotLocks { 39 | t.Errorf("want %v; got %v", nil, gotLocks) 40 | } 41 | 42 | gotLastIDs := db.lastIDs 43 | if nil != gotLastIDs { 44 | t.Errorf("want %v; got %v", nil, gotLastIDs) 45 | } 46 | }, 47 | func(t *testing.T) { 48 | //Close Ram Database... 49 | 50 | r, err := ram.New(seedData()) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | db, err := New(r) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | db.Close() 60 | 61 | checkErr(t, dberr.ErrNoTable, db.Find("contacts", 3, &Contact{})) 62 | 63 | gotStore := db.store 64 | if nil != gotStore { 65 | t.Errorf("want %v; got %v", nil, gotStore) 66 | } 67 | 68 | gotLocks := db.locks 69 | if nil != gotLocks { 70 | t.Errorf("want %v; got %v", nil, gotLocks) 71 | } 72 | 73 | gotLastIDs := db.lastIDs 74 | if nil != gotLastIDs { 75 | t.Errorf("want %v; got %v", nil, gotLastIDs) 76 | } 77 | }, 78 | } 79 | 80 | for i, fn := range tests { 81 | t.Run(strconv.Itoa(i), fn) 82 | } 83 | } 84 | 85 | func TestNonMutatingDatabaseTests(t *testing.T) { 86 | var tests = []func(*Database) func(*testing.T){ 87 | func(db *Database) func(*testing.T) { 88 | //New... 89 | 90 | return func(t *testing.T) { 91 | want := 4 92 | got := db.lastIDs["contacts"] 93 | if want != got { 94 | t.Errorf("want %v; got %v", want, got) 95 | } 96 | } 97 | }, 98 | func(db *Database) func(*testing.T) { 99 | //TableExists... 100 | 101 | return func(t *testing.T) { 102 | want := true 103 | got := db.TableExists("contacts") 104 | if want != got { 105 | t.Errorf("want %v; got %v", want, got) 106 | } 107 | 108 | want = false 109 | got = db.TableExists("nonexistent") 110 | if want != got { 111 | t.Errorf("want %v; got %v", want, got) 112 | } 113 | } 114 | }, 115 | func(db *Database) func(*testing.T) { 116 | //tableExists... 117 | 118 | return func(t *testing.T) { 119 | want := true 120 | got := db.tableExists("contacts") 121 | if want != got { 122 | t.Errorf("want %v; got %v", want, got) 123 | } 124 | 125 | want = false 126 | got = db.tableExists("nonexistent") 127 | if want != got { 128 | t.Errorf("want %v; got %v", want, got) 129 | } 130 | } 131 | }, 132 | } 133 | 134 | runTestFns(t, tests) 135 | } 136 | 137 | func TestMutatingDatabaseTests(t *testing.T) { 138 | var tests = []func(*Database) func(*testing.T){ 139 | func(db *Database) func(*testing.T) { 140 | //CreateTable... 141 | 142 | return func(t *testing.T) { 143 | err := db.CreateTable("newtable") 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | want := true 149 | got := db.TableExists("newtable") 150 | 151 | if want != got { 152 | t.Errorf("want %v; got %v", want, got) 153 | } 154 | } 155 | }, 156 | func(db *Database) func(*testing.T) { 157 | //CreateTable (TableExists error)... 158 | 159 | return func(t *testing.T) { 160 | checkErr(t, dberr.ErrTableExists, db.CreateTable("contacts")) 161 | } 162 | }, 163 | func(db *Database) func(*testing.T) { 164 | //DropTable... 165 | 166 | return func(t *testing.T) { 167 | err := db.DropTable("contacts") 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | 172 | want := false 173 | got := db.TableExists("contacts") 174 | 175 | if want != got { 176 | t.Errorf("want %v; got %v", want, got) 177 | } 178 | } 179 | }, 180 | func(db *Database) func(*testing.T) { 181 | //DropTable (NoTable error)... 182 | 183 | return func(t *testing.T) { 184 | checkErr(t, dberr.ErrNoTable, db.DropTable("nonexistent")) 185 | } 186 | }, 187 | func(db *Database) func(*testing.T) { 188 | //incrementLastID... 189 | 190 | return func(t *testing.T) { 191 | want := 5 192 | got := db.incrementLastID("contacts") 193 | 194 | if want != got { 195 | t.Errorf("want %v; got %v", want, got) 196 | } 197 | } 198 | }, 199 | } 200 | 201 | runTestFns(t, tests) 202 | } 203 | 204 | func TestTableTests(t *testing.T) { 205 | var tests = []func(*Database) func(*testing.T){ 206 | func(db *Database) func(*testing.T) { 207 | //IDs()... 208 | 209 | return func(t *testing.T) { 210 | want := []int{1, 2, 3, 4} 211 | got, err := db.IDs("contacts") 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | 216 | sort.Ints(got) 217 | 218 | if len(want) != len(got) { 219 | t.Errorf("want %v; got %v", want, got) 220 | } else { 221 | 222 | for i := range want { 223 | if want[i] != got[i] { 224 | t.Errorf("want %v; got %v", want, got) 225 | } 226 | } 227 | } 228 | } 229 | }, 230 | func(db *Database) func(*testing.T) { 231 | //IDs() (NoTable error)... 232 | 233 | return func(t *testing.T) { 234 | _, gotErr := db.IDs("nonexistent") 235 | 236 | checkErr(t, dberr.ErrNoTable, gotErr) 237 | } 238 | }, 239 | } 240 | 241 | runTestFns(t, tests) 242 | } 243 | 244 | func TestRecordTests(t *testing.T) { 245 | var tests = []func(*Database) func(*testing.T){ 246 | func(db *Database) func(*testing.T) { 247 | //Delete... 248 | 249 | return func(t *testing.T) { 250 | err := db.Delete("contacts", 3) 251 | if err != nil { 252 | t.Fatal(err) 253 | } 254 | 255 | checkErr(t, dberr.ErrNoRecord, db.Find("contacts", 3, &Contact{})) 256 | } 257 | }, 258 | func(db *Database) func(*testing.T) { 259 | //Delete (ErrNoTable error)... 260 | 261 | return func(t *testing.T) { 262 | checkErr(t, dberr.ErrNoTable, db.Delete("nonexistent", 3)) 263 | } 264 | }, 265 | func(db *Database) func(*testing.T) { 266 | //Delete (ErrNoRecord error)... 267 | 268 | return func(t *testing.T) { 269 | checkErr(t, dberr.ErrNoRecord, db.Delete("contacts", 99)) 270 | } 271 | }, 272 | func(db *Database) func(*testing.T) { 273 | //Find... 274 | 275 | return func(t *testing.T) { 276 | c := Contact{} 277 | 278 | err := db.Find("contacts", 2, &c) 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | 283 | want := "Abe Lincoln is 52" 284 | got := fmt.Sprintf("%s %s is %d", c.FirstName, c.LastName, c.Age) 285 | 286 | if want != got { 287 | t.Errorf("want %v; got %v", want, got) 288 | } 289 | } 290 | }, 291 | func(db *Database) func(*testing.T) { 292 | //Find (ErrNoRecord error)... 293 | 294 | return func(t *testing.T) { 295 | checkErr(t, dberr.ErrNoRecord, db.Find("contacts", 99, &Contact{})) 296 | } 297 | }, 298 | func(db *Database) func(*testing.T) { 299 | //Insert... 300 | 301 | return func(t *testing.T) { 302 | wantInt := 5 303 | gotInt, err := db.Insert("contacts", &Contact{FirstName: "Robin", LastName: "Williams", Age: 88}) 304 | if err != nil { 305 | t.Fatal(err) 306 | } 307 | 308 | if wantInt != gotInt { 309 | t.Errorf("want %v; got %v", wantInt, gotInt) 310 | } 311 | 312 | c := Contact{} 313 | 314 | err = db.Find("contacts", 5, &c) 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | 319 | want := "Robin Williams is 88" 320 | got := fmt.Sprintf("%s %s is %d", c.FirstName, c.LastName, c.Age) 321 | 322 | if want != got { 323 | t.Errorf("want %v; got %v", want, got) 324 | } 325 | } 326 | }, 327 | func(db *Database) func(*testing.T) { 328 | //Insert (ErrNoTable error)... 329 | 330 | return func(t *testing.T) { 331 | _, gotErr := db.Insert("nonexistent", &Contact{FirstName: "Robin", LastName: "Williams", Age: 88}) 332 | 333 | checkErr(t, dberr.ErrNoTable, gotErr) 334 | } 335 | }, 336 | func(db *Database) func(*testing.T) { 337 | //Update... 338 | 339 | return func(t *testing.T) { 340 | err := db.Update("contacts", &Contact{ID: 4, FirstName: "Hazel", LastName: "Koller", Age: 26}) 341 | if err != nil { 342 | t.Fatal(err) 343 | } 344 | 345 | c := Contact{} 346 | 347 | err = db.Find("contacts", 4, &c) 348 | if err != nil { 349 | t.Fatal(err) 350 | } 351 | 352 | want := "Hazel Koller is 26" 353 | got := fmt.Sprintf("%s %s is %d", c.FirstName, c.LastName, c.Age) 354 | 355 | if want != got { 356 | t.Errorf("want %v; got %v", want, got) 357 | } 358 | } 359 | }, 360 | func(db *Database) func(*testing.T) { 361 | //Update (ErrNoTable error)... 362 | 363 | return func(t *testing.T) { 364 | gotErr := db.Update("nonexistent", &Contact{ID: 4, FirstName: "Hazel", LastName: "Koller", Age: 26}) 365 | 366 | checkErr(t, dberr.ErrNoTable, gotErr) 367 | } 368 | }, 369 | } 370 | 371 | runTestFns(t, tests) 372 | } 373 | -------------------------------------------------------------------------------- /datastores/disk/disk.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | 8 | "github.com/jameycribbs/hare/dberr" 9 | ) 10 | 11 | // Disk is a struct that holds a map of all the 12 | // table files in a database directory. 13 | type Disk struct { 14 | path string 15 | ext string 16 | tableFiles map[string]*tableFile 17 | } 18 | 19 | // New takes a datastorage path and an extension 20 | // and returns a pointer to a Disk struct. 21 | func New(path string, ext string) (*Disk, error) { 22 | var dsk Disk 23 | 24 | dsk.path = path 25 | dsk.ext = ext 26 | 27 | if err := dsk.init(); err != nil { 28 | return nil, err 29 | } 30 | 31 | return &dsk, nil 32 | } 33 | 34 | // Close closes the datastore. 35 | func (dsk *Disk) Close() error { 36 | for _, tableFile := range dsk.tableFiles { 37 | if err := tableFile.close(); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | dsk.path = "" 43 | dsk.ext = "" 44 | dsk.tableFiles = nil 45 | 46 | return nil 47 | } 48 | 49 | // CreateTable takes a table name, creates a new disk 50 | // file, and adds it to the map of tables in the 51 | // datastore. 52 | func (dsk *Disk) CreateTable(tableName string) error { 53 | if dsk.TableExists(tableName) { 54 | return dberr.ErrTableExists 55 | } 56 | 57 | filePtr, err := dsk.openFile(tableName, true) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | tableFile, err := newTableFile(tableName, filePtr) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | dsk.tableFiles[tableName] = tableFile 68 | 69 | return nil 70 | } 71 | 72 | // DeleteRec takes a table name and a record id and deletes 73 | // the associated record. 74 | func (dsk *Disk) DeleteRec(tableName string, id int) error { 75 | tableFile, err := dsk.getTableFile(tableName) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | if err = tableFile.deleteRec(id); err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | // GetLastID takes a table name and returns the greatest record 88 | // id found in the table. 89 | func (dsk *Disk) GetLastID(tableName string) (int, error) { 90 | tableFile, err := dsk.getTableFile(tableName) 91 | if err != nil { 92 | return 0, err 93 | } 94 | 95 | return tableFile.getLastID(), nil 96 | } 97 | 98 | // IDs takes a table name and returns an array of all record IDs 99 | // found in the table. 100 | func (dsk *Disk) IDs(tableName string) ([]int, error) { 101 | tableFile, err := dsk.getTableFile(tableName) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return tableFile.ids(), nil 107 | } 108 | 109 | // InsertRec takes a table name, a record id, and a byte array and adds 110 | // the record to the table. 111 | func (dsk *Disk) InsertRec(tableName string, id int, rec []byte) error { 112 | tableFile, err := dsk.getTableFile(tableName) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | ids := tableFile.ids() 118 | for _, i := range ids { 119 | if id == i { 120 | return dberr.ErrIDExists 121 | } 122 | } 123 | 124 | offset, err := tableFile.offsetForWritingRec(len(rec)) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if err := tableFile.writeRec(offset, 0, rec); err != nil { 130 | return err 131 | } 132 | 133 | tableFile.offsets[id] = offset 134 | 135 | return nil 136 | } 137 | 138 | // ReadRec takes a table name and an id, reads the record from the 139 | // table, and returns a populated byte array. 140 | func (dsk *Disk) ReadRec(tableName string, id int) ([]byte, error) { 141 | tableFile, err := dsk.getTableFile(tableName) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | rec, err := tableFile.readRec(id) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return rec, err 152 | } 153 | 154 | // RemoveTable takes a table name and deletes that table file from the 155 | // disk. 156 | func (dsk *Disk) RemoveTable(tableName string) error { 157 | tableFile, err := dsk.getTableFile(tableName) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | tableFile.close() 163 | 164 | if err := os.Remove(dsk.path + "/" + tableName + dsk.ext); err != nil { 165 | return err 166 | } 167 | 168 | delete(dsk.tableFiles, tableName) 169 | 170 | return nil 171 | } 172 | 173 | // TableExists takes a table name and returns a bool indicating 174 | // whether or not the table exists in the datastore. 175 | func (dsk *Disk) TableExists(tableName string) bool { 176 | _, ok := dsk.tableFiles[tableName] 177 | 178 | return ok 179 | } 180 | 181 | // TableNames returns an array of table names. 182 | func (dsk *Disk) TableNames() []string { 183 | var names []string 184 | 185 | for k := range dsk.tableFiles { 186 | names = append(names, k) 187 | } 188 | 189 | return names 190 | } 191 | 192 | // UpdateRec takes a table name, a record id, and a byte array and updates 193 | // the table record with that id. 194 | func (dsk *Disk) UpdateRec(tableName string, id int, rec []byte) error { 195 | tableFile, err := dsk.getTableFile(tableName) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if err = tableFile.updateRec(id, rec); err != nil { 201 | return err 202 | } 203 | 204 | return nil 205 | } 206 | 207 | //****************************************************************************** 208 | // UNEXPORTED METHODS 209 | //****************************************************************************** 210 | 211 | func (dsk *Disk) getTableFile(tableName string) (*tableFile, error) { 212 | tableFile, ok := dsk.tableFiles[tableName] 213 | if !ok { 214 | return nil, dberr.ErrNoTable 215 | } 216 | 217 | return tableFile, nil 218 | } 219 | 220 | func (dsk *Disk) getTableNames() ([]string, error) { 221 | var tableNames []string 222 | 223 | files, err := ioutil.ReadDir(dsk.path) 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | for _, file := range files { 229 | fileName := file.Name() 230 | 231 | // If entry is sub dir, current dir, or parent dir, skip it. 232 | if file.IsDir() || fileName == "." || fileName == ".." { 233 | continue 234 | } 235 | 236 | if !strings.HasSuffix(fileName, dsk.ext) { 237 | continue 238 | } 239 | 240 | tableNames = append(tableNames, strings.TrimSuffix(fileName, dsk.ext)) 241 | } 242 | 243 | return tableNames, nil 244 | } 245 | 246 | func (dsk *Disk) init() error { 247 | dsk.tableFiles = make(map[string]*tableFile) 248 | 249 | tableNames, err := dsk.getTableNames() 250 | if err != nil { 251 | return err 252 | } 253 | 254 | for _, tableName := range tableNames { 255 | filePtr, err := dsk.openFile(tableName, false) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | tableFile, err := newTableFile(tableName, filePtr) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | dsk.tableFiles[tableName] = tableFile 266 | } 267 | 268 | return nil 269 | } 270 | 271 | func (dsk Disk) openFile(tableName string, createIfNeeded bool) (*os.File, error) { 272 | var osFlag int 273 | 274 | if createIfNeeded { 275 | osFlag = os.O_CREATE | os.O_RDWR 276 | } else { 277 | osFlag = os.O_RDWR 278 | } 279 | 280 | filePtr, err := os.OpenFile(dsk.path+"/"+tableName+dsk.ext, osFlag, 0660) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | return filePtr, nil 286 | } 287 | 288 | func (dsk *Disk) closeTable(tableName string) error { 289 | tableFile, ok := dsk.tableFiles[tableName] 290 | if !ok { 291 | return dberr.ErrNoTable 292 | } 293 | 294 | if err := tableFile.close(); err != nil { 295 | return err 296 | } 297 | 298 | return nil 299 | } 300 | -------------------------------------------------------------------------------- /datastores/disk/disk_test.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "reflect" 7 | "sort" 8 | "testing" 9 | 10 | "github.com/jameycribbs/hare/dberr" 11 | ) 12 | 13 | func TestNewCloseDiskTests(t *testing.T) { 14 | var tests = []func(t *testing.T){ 15 | func(t *testing.T) { 16 | //New... 17 | 18 | dsk := newTestDisk(t) 19 | defer dsk.Close() 20 | 21 | want := "./testdata" 22 | got := dsk.path 23 | if want != got { 24 | t.Errorf("want %v; got %v", want, got) 25 | } 26 | 27 | want = ".json" 28 | got = dsk.ext 29 | if want != got { 30 | t.Errorf("want %v; got %v", want, got) 31 | } 32 | 33 | wantOffsets := make(map[int]int64) 34 | wantOffsets[1] = 0 35 | wantOffsets[2] = 101 36 | wantOffsets[3] = 160 37 | wantOffsets[4] = 224 38 | 39 | gotOffsets := dsk.tableFiles["contacts"].offsets 40 | 41 | if !reflect.DeepEqual(wantOffsets, gotOffsets) { 42 | t.Errorf("want %v; got %v", wantOffsets, gotOffsets) 43 | } 44 | }, 45 | func(t *testing.T) { 46 | //Close... 47 | 48 | dsk := newTestDisk(t) 49 | dsk.Close() 50 | 51 | wantErr := dberr.ErrNoTable 52 | _, gotErr := dsk.ReadRec("contacts", 3) 53 | 54 | if !errors.Is(gotErr, wantErr) { 55 | t.Errorf("want %v; got %v", wantErr, gotErr) 56 | } 57 | 58 | got := dsk.tableFiles 59 | 60 | if nil != got { 61 | t.Errorf("want %v; got %v", nil, got) 62 | } 63 | }, 64 | } 65 | 66 | runTestFns(t, tests) 67 | } 68 | 69 | func TestCreateTableDiskTests(t *testing.T) { 70 | var tests = []func(t *testing.T){ 71 | func(t *testing.T) { 72 | //CreateTable... 73 | 74 | if _, err := os.Stat("./testdata/newtable.json"); err == nil { 75 | t.Fatal("file already exists for dsk.CreateTable test") 76 | 77 | } 78 | 79 | dsk := newTestDisk(t) 80 | defer dsk.Close() 81 | 82 | err := dsk.CreateTable("newtable") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | if _, err = os.Stat("./testdata/newtable.json"); err != nil { 88 | t.Errorf("want %v; got %v", nil, err) 89 | } 90 | 91 | want := true 92 | got := dsk.TableExists("newtable") 93 | 94 | if want != got { 95 | t.Errorf("want %v; got %v", want, got) 96 | } 97 | }, 98 | func(t *testing.T) { 99 | //CreateTable (TableExists error)... 100 | 101 | dsk := newTestDisk(t) 102 | defer dsk.Close() 103 | 104 | wantErr := dberr.ErrTableExists 105 | gotErr := dsk.CreateTable("contacts") 106 | 107 | if !errors.Is(gotErr, wantErr) { 108 | t.Errorf("want %v; got %v", wantErr, gotErr) 109 | } 110 | }, 111 | } 112 | 113 | runTestFns(t, tests) 114 | } 115 | 116 | func TestDeleteRecDiskTests(t *testing.T) { 117 | var tests = []func(t *testing.T){ 118 | func(t *testing.T) { 119 | //DeleteRec... 120 | 121 | dsk := newTestDisk(t) 122 | defer dsk.Close() 123 | 124 | err := dsk.DeleteRec("contacts", 3) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | want := dberr.ErrNoRecord 130 | _, got := dsk.ReadRec("contacts", 3) 131 | 132 | if !errors.Is(got, want) { 133 | t.Errorf("want %v; got %v", want, got) 134 | } 135 | }, 136 | func(t *testing.T) { 137 | //DeleteRec (NoTable error)... 138 | 139 | dsk := newTestDisk(t) 140 | defer dsk.Close() 141 | 142 | wantErr := dberr.ErrNoTable 143 | gotErr := dsk.DeleteRec("nonexistent", 3) 144 | 145 | if !errors.Is(gotErr, wantErr) { 146 | t.Errorf("want %v; got %v", wantErr, gotErr) 147 | } 148 | }, 149 | } 150 | 151 | runTestFns(t, tests) 152 | } 153 | 154 | func TestGetLastIDDiskTests(t *testing.T) { 155 | var tests = []func(t *testing.T){ 156 | func(t *testing.T) { 157 | //GetLastID... 158 | 159 | dsk := newTestDisk(t) 160 | defer dsk.Close() 161 | 162 | want := 4 163 | got, err := dsk.GetLastID("contacts") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | if want != got { 169 | t.Errorf("want %v; got %v", want, got) 170 | } 171 | }, 172 | func(t *testing.T) { 173 | //GetLastID (NoTable error)... 174 | 175 | dsk := newTestDisk(t) 176 | defer dsk.Close() 177 | 178 | wantErr := dberr.ErrNoTable 179 | _, gotErr := dsk.GetLastID("nonexistent") 180 | 181 | if !errors.Is(gotErr, wantErr) { 182 | t.Errorf("want %v; got %v", wantErr, gotErr) 183 | } 184 | }, 185 | } 186 | 187 | runTestFns(t, tests) 188 | } 189 | 190 | func TestIDsDiskTests(t *testing.T) { 191 | var tests = []func(t *testing.T){ 192 | func(t *testing.T) { 193 | //IDs... 194 | 195 | dsk := newTestDisk(t) 196 | defer dsk.Close() 197 | 198 | want := []int{1, 2, 3, 4} 199 | got, err := dsk.IDs("contacts") 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | 204 | sort.Ints(got) 205 | 206 | if len(want) != len(got) { 207 | t.Errorf("want %v; got %v", want, got) 208 | } else { 209 | 210 | for i := range want { 211 | if want[i] != got[i] { 212 | t.Errorf("want %v; got %v", want, got) 213 | } 214 | } 215 | } 216 | }, 217 | func(t *testing.T) { 218 | //IDs (NoTable error)... 219 | 220 | dsk := newTestDisk(t) 221 | defer dsk.Close() 222 | 223 | wantErr := dberr.ErrNoTable 224 | _, gotErr := dsk.IDs("nonexistent") 225 | 226 | if !errors.Is(gotErr, wantErr) { 227 | t.Errorf("want %v; got %v", wantErr, gotErr) 228 | } 229 | }, 230 | } 231 | 232 | runTestFns(t, tests) 233 | } 234 | 235 | func TestInsertRecDiskTests(t *testing.T) { 236 | var tests = []func(t *testing.T){ 237 | func(t *testing.T) { 238 | //InsertRec... 239 | 240 | dsk := newTestDisk(t) 241 | defer dsk.Close() 242 | 243 | err := dsk.InsertRec("contacts", 5, []byte(`{"id":5,"first_name":"Rex","last_name":"Stout","age":77}`)) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | 248 | rec, err := dsk.ReadRec("contacts", 5) 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | 253 | want := "{\"id\":5,\"first_name\":\"Rex\",\"last_name\":\"Stout\",\"age\":77}\n" 254 | got := string(rec) 255 | 256 | if want != got { 257 | t.Errorf("want %v; got %v", want, got) 258 | } 259 | }, 260 | func(t *testing.T) { 261 | //InsertRec (NoTable error)... 262 | 263 | dsk := newTestDisk(t) 264 | defer dsk.Close() 265 | 266 | wantErr := dberr.ErrNoTable 267 | gotErr := dsk.InsertRec("nonexistent", 5, []byte(`{"id":5,"first_name":"Rex","last_name":"Stout","age":77}`)) 268 | 269 | if !errors.Is(gotErr, wantErr) { 270 | t.Errorf("want %v; got %v", wantErr, gotErr) 271 | } 272 | }, 273 | func(t *testing.T) { 274 | //InsertRec (IDExists error)... 275 | 276 | dsk := newTestDisk(t) 277 | defer dsk.Close() 278 | 279 | wantErr := dberr.ErrIDExists 280 | gotErr := dsk.InsertRec("contacts", 3, []byte(`{"id":3,"first_name":"Rex","last_name":"Stout","age":77}`)) 281 | if !errors.Is(gotErr, wantErr) { 282 | t.Errorf("want %v; got %v", wantErr, gotErr) 283 | } 284 | 285 | rec, err := dsk.ReadRec("contacts", 3) 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | 290 | want := "{\"id\":3,\"first_name\":\"Bill\",\"last_name\":\"Shakespeare\",\"age\":18}\n" 291 | got := string(rec) 292 | 293 | if want != got { 294 | t.Errorf("want %v; got %v", want, got) 295 | } 296 | }, 297 | } 298 | 299 | runTestFns(t, tests) 300 | } 301 | 302 | func TestReadRecDiskTests(t *testing.T) { 303 | var tests = []func(t *testing.T){ 304 | func(t *testing.T) { 305 | //ReadRec... 306 | 307 | dsk := newTestDisk(t) 308 | defer dsk.Close() 309 | 310 | rec, err := dsk.ReadRec("contacts", 3) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | 315 | want := "{\"id\":3,\"first_name\":\"Bill\",\"last_name\":\"Shakespeare\",\"age\":18}\n" 316 | got := string(rec) 317 | 318 | if want != got { 319 | t.Errorf("want %v; got %v", want, got) 320 | } 321 | }, 322 | func(t *testing.T) { 323 | //ReadRec (NoTable error)... 324 | 325 | dsk := newTestDisk(t) 326 | defer dsk.Close() 327 | 328 | wantErr := dberr.ErrNoTable 329 | _, gotErr := dsk.ReadRec("nonexistent", 3) 330 | 331 | if !errors.Is(gotErr, wantErr) { 332 | t.Errorf("want %v; got %v", wantErr, gotErr) 333 | } 334 | }, 335 | } 336 | 337 | runTestFns(t, tests) 338 | } 339 | 340 | func TestRemoveTableDiskTests(t *testing.T) { 341 | var tests = []func(t *testing.T){ 342 | func(t *testing.T) { 343 | //RemoveTable... 344 | 345 | if _, err := os.Stat("./testdata/contacts.json"); err != nil { 346 | t.Fatal(err) 347 | 348 | } 349 | 350 | dsk := newTestDisk(t) 351 | defer dsk.Close() 352 | 353 | err := dsk.RemoveTable("contacts") 354 | if err != nil { 355 | t.Fatal(err) 356 | } 357 | 358 | if _, err := os.Stat("./testdata/contacts.json"); !os.IsNotExist(err) { 359 | t.Errorf("want %v; got %v", os.ErrNotExist, err) 360 | } 361 | 362 | want := false 363 | got := dsk.TableExists("contacts") 364 | 365 | if want != got { 366 | t.Errorf("want %v; got %v", want, got) 367 | } 368 | }, 369 | func(t *testing.T) { 370 | //RemoveTable (NoTable error)... 371 | 372 | dsk := newTestDisk(t) 373 | defer dsk.Close() 374 | 375 | wantErr := dberr.ErrNoTable 376 | gotErr := dsk.RemoveTable("nonexistent") 377 | 378 | if !errors.Is(gotErr, wantErr) { 379 | t.Errorf("want %v; got %v", wantErr, gotErr) 380 | } 381 | }, 382 | } 383 | 384 | runTestFns(t, tests) 385 | } 386 | 387 | func TestTableExistsDiskTests(t *testing.T) { 388 | var tests = []func(t *testing.T){ 389 | func(t *testing.T) { 390 | //TableExists... 391 | 392 | dsk := newTestDisk(t) 393 | defer dsk.Close() 394 | 395 | want := true 396 | got := dsk.TableExists("contacts") 397 | 398 | if want != got { 399 | t.Errorf("want %v; got %v", want, got) 400 | } 401 | 402 | want = false 403 | got = dsk.TableExists("nonexistant") 404 | 405 | if want != got { 406 | t.Errorf("want %v; got %v", want, got) 407 | } 408 | }, 409 | } 410 | 411 | runTestFns(t, tests) 412 | } 413 | 414 | func TestTableNamesDiskTests(t *testing.T) { 415 | var tests = []func(t *testing.T){ 416 | func(t *testing.T) { 417 | //TableNames... 418 | 419 | dsk := newTestDisk(t) 420 | defer dsk.Close() 421 | 422 | want := []string{"contacts"} 423 | got := dsk.TableNames() 424 | 425 | sort.Strings(got) 426 | 427 | if len(want) != len(got) { 428 | t.Errorf("want %v; got %v", want, got) 429 | } else { 430 | 431 | for i := range want { 432 | if want[i] != got[i] { 433 | t.Errorf("want %v; got %v", want, got) 434 | } 435 | } 436 | } 437 | }, 438 | } 439 | 440 | runTestFns(t, tests) 441 | } 442 | 443 | func TestUpdateRecDiskTests(t *testing.T) { 444 | var tests = []func(t *testing.T){ 445 | func(t *testing.T) { 446 | //UpdateRec... 447 | 448 | dsk := newTestDisk(t) 449 | defer dsk.Close() 450 | 451 | err := dsk.UpdateRec("contacts", 3, []byte(`{"id":3,"first_name":"William","last_name":"Shakespeare","age":77}`)) 452 | if err != nil { 453 | t.Fatal(err) 454 | } 455 | 456 | rec, err := dsk.ReadRec("contacts", 3) 457 | if err != nil { 458 | t.Fatal(err) 459 | } 460 | 461 | want := "{\"id\":3,\"first_name\":\"William\",\"last_name\":\"Shakespeare\",\"age\":77}\n" 462 | got := string(rec) 463 | 464 | if want != got { 465 | t.Errorf("want %v; got %v", want, got) 466 | } 467 | }, 468 | func(t *testing.T) { 469 | //UpdateRec (NoTable error)... 470 | 471 | dsk := newTestDisk(t) 472 | defer dsk.Close() 473 | 474 | wantErr := dberr.ErrNoTable 475 | gotErr := dsk.UpdateRec("nonexistent", 3, []byte(`{"id":3,"first_name":"William","last_name":"Shakespeare","age":77}`)) 476 | 477 | if !errors.Is(gotErr, wantErr) { 478 | t.Errorf("want %v; got %v", wantErr, gotErr) 479 | } 480 | }, 481 | } 482 | 483 | runTestFns(t, tests) 484 | } 485 | 486 | func TestCloseTableDiskTests(t *testing.T) { 487 | var tests = []func(t *testing.T){ 488 | func(t *testing.T) { 489 | //closeTable... 490 | 491 | dsk := newTestDisk(t) 492 | defer dsk.Close() 493 | 494 | err := dsk.closeTable("contacts") 495 | if err != nil { 496 | t.Errorf("want %v; got %v", nil, err) 497 | } 498 | }, 499 | func(t *testing.T) { 500 | //closeTable (NoTable error)... 501 | 502 | dsk := newTestDisk(t) 503 | defer dsk.Close() 504 | 505 | wantErr := dberr.ErrNoTable 506 | gotErr := dsk.closeTable("nonexistent") 507 | 508 | if !errors.Is(gotErr, wantErr) { 509 | t.Errorf("want %v; got %v", wantErr, gotErr) 510 | } 511 | }, 512 | } 513 | 514 | runTestFns(t, tests) 515 | } 516 | -------------------------------------------------------------------------------- /datastores/disk/table_file.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | 9 | "github.com/jameycribbs/hare/dberr" 10 | ) 11 | 12 | const dummyRune = 'X' 13 | 14 | type tableFile struct { 15 | ptr *os.File 16 | offsets map[int]int64 17 | } 18 | 19 | func newTableFile(tableName string, filePtr *os.File) (*tableFile, error) { 20 | var currentOffset int64 21 | var totalOffset int64 22 | var recLen int 23 | var recMap map[string]interface{} 24 | 25 | tableFile := tableFile{ptr: filePtr} 26 | tableFile.offsets = make(map[int]int64) 27 | 28 | r := bufio.NewReader(filePtr) 29 | 30 | _, err := filePtr.Seek(0, 0) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | for { 36 | rec, err := r.ReadBytes('\n') 37 | 38 | currentOffset = totalOffset 39 | recLen = len(rec) 40 | totalOffset += int64(recLen) 41 | 42 | if err == io.EOF { 43 | break 44 | } 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // Skip dummy records. 51 | if (rec[0] == '\n') || (rec[0] == dummyRune) { 52 | continue 53 | } 54 | 55 | //Unmarshal so we can grab the record ID. 56 | if err := json.Unmarshal(rec, &recMap); err != nil { 57 | return nil, err 58 | } 59 | recMapID := int(recMap["id"].(float64)) 60 | 61 | tableFile.offsets[recMapID] = currentOffset 62 | } 63 | 64 | return &tableFile, nil 65 | } 66 | 67 | func (t *tableFile) close() error { 68 | if err := t.ptr.Close(); err != nil { 69 | return err 70 | } 71 | 72 | t.offsets = nil 73 | 74 | return nil 75 | } 76 | 77 | func (t *tableFile) deleteRec(id int) error { 78 | offset, ok := t.offsets[id] 79 | if !ok { 80 | return dberr.ErrNoRecord 81 | } 82 | 83 | rec, err := t.readRec(id) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if err = t.overwriteRec(offset, len(rec)); err != nil { 89 | return err 90 | } 91 | 92 | delete(t.offsets, id) 93 | 94 | return nil 95 | } 96 | 97 | func (t *tableFile) getLastID() int { 98 | var lastID int 99 | 100 | for k := range t.offsets { 101 | if k > lastID { 102 | lastID = k 103 | } 104 | } 105 | 106 | return lastID 107 | } 108 | 109 | func (t *tableFile) ids() []int { 110 | ids := make([]int, len(t.offsets)) 111 | 112 | i := 0 113 | for id := range t.offsets { 114 | ids[i] = id 115 | i++ 116 | } 117 | 118 | return ids 119 | } 120 | 121 | // offsetForWritingRec takes a record length and returns the offset in the file 122 | // where the record is to be written. It will try to fit the record on a dummy 123 | // line, otherwise, it will return the offset at the end of the file. 124 | func (t *tableFile) offsetForWritingRec(recLen int) (int64, error) { 125 | var offset int64 126 | var err error 127 | 128 | // Can the record fit onto a line with a dummy record? 129 | offset, recFitErr := t.offsetToFitRec(recLen) 130 | 131 | switch recFitErr.(type) { 132 | case nil: 133 | case dummiesTooShortError: 134 | // Go to the end of the file. 135 | offset, err = t.ptr.Seek(0, 2) 136 | if err != nil { 137 | return 0, err 138 | } 139 | default: 140 | return 0, recFitErr 141 | } 142 | 143 | return offset, nil 144 | } 145 | 146 | // offsetToFitRec takes a record length and checks all the dummy records to see 147 | // if any are big enough to fit the record. 148 | func (t *tableFile) offsetToFitRec(recLenNeeded int) (int64, error) { 149 | var recLen int 150 | var offset int64 151 | var totalOffset int64 152 | 153 | r := bufio.NewReader(t.ptr) 154 | 155 | if _, err := t.ptr.Seek(0, 0); err != nil { 156 | return 0, err 157 | } 158 | 159 | for { 160 | rec, err := r.ReadBytes('\n') 161 | 162 | offset = totalOffset 163 | recLen = len(rec) 164 | totalOffset += int64(recLen) 165 | 166 | // Need to account for newline character. 167 | recLen-- 168 | 169 | if err == io.EOF { 170 | break 171 | } 172 | 173 | if err != nil { 174 | return 0, err 175 | } 176 | 177 | // If this is a dummy record, is it big enough to 178 | // hold the needed record length? 179 | if (rec[0] == '\n') || (rec[0] == dummyRune) { 180 | if recLen >= recLenNeeded { 181 | return offset, nil 182 | } 183 | } 184 | } 185 | 186 | return 0, dummiesTooShortError{} 187 | } 188 | 189 | func (t *tableFile) overwriteRec(offset int64, recLen int) error { 190 | // Overwrite record with XXXXXXXX... 191 | dummyData := make([]byte, recLen-1) 192 | 193 | for i := range dummyData { 194 | dummyData[i] = 'X' 195 | } 196 | 197 | if err := t.writeRec(offset, 0, dummyData); err != nil { 198 | return err 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func (t *tableFile) readRec(id int) ([]byte, error) { 205 | offset, ok := t.offsets[id] 206 | if !ok { 207 | return nil, dberr.ErrNoRecord 208 | } 209 | 210 | r := bufio.NewReader(t.ptr) 211 | 212 | if _, err := t.ptr.Seek(offset, 0); err != nil { 213 | return nil, err 214 | } 215 | 216 | rec, err := r.ReadBytes('\n') 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | return rec, err 222 | } 223 | 224 | func (t *tableFile) updateRec(id int, rec []byte) error { 225 | recLen := len(rec) 226 | 227 | oldRecOffset, ok := t.offsets[id] 228 | if !ok { 229 | return dberr.ErrNoRecord 230 | } 231 | 232 | oldRec, err := t.readRec(id) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | oldRecLen := len(oldRec) 238 | 239 | diff := oldRecLen - (recLen + 1) 240 | 241 | if diff > 0 { 242 | // Changed record is smaller than record in table, so dummy out 243 | // extra space and write over old record. 244 | 245 | rec = append(rec, padRec(diff)...) 246 | 247 | if err = t.writeRec(oldRecOffset, 0, rec); err != nil { 248 | return err 249 | } 250 | 251 | } else if diff < 0 { 252 | // Changed record is larger than the record in table. 253 | 254 | recOffset, err := t.offsetForWritingRec(recLen) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | if err = t.writeRec(recOffset, 0, rec); err != nil { 260 | return err 261 | } 262 | 263 | // Turn the old record into a dummy. 264 | if err = t.overwriteRec(oldRecOffset, oldRecLen); err != nil { 265 | return err 266 | } 267 | 268 | // Update the index with the new offset since the record is in a 269 | // new position in the file. 270 | t.offsets[id] = recOffset 271 | } else { 272 | // Changed record is the same length as the record in the table. 273 | err = t.writeRec(oldRecOffset, 0, rec) 274 | if err != nil { 275 | return err 276 | } 277 | } 278 | 279 | return nil 280 | } 281 | 282 | func (t *tableFile) writeRec(offset int64, whence int, rec []byte) error { 283 | var err error 284 | 285 | w := bufio.NewWriter(t.ptr) 286 | 287 | if _, err = t.ptr.Seek(offset, whence); err != nil { 288 | return err 289 | } 290 | 291 | if _, err = w.Write(append(rec, '\n')); err != nil { 292 | return err 293 | } 294 | 295 | w.Flush() 296 | 297 | return nil 298 | } 299 | 300 | func padRec(padLength int) []byte { 301 | extraData := make([]byte, padLength) 302 | 303 | extraData[0] = '\n' 304 | 305 | for i := 1; i < padLength; i++ { 306 | extraData[i] = dummyRune 307 | } 308 | 309 | return extraData 310 | } 311 | 312 | // dummiesTooShortError is a place to hold a custom error used 313 | // as part of a switch. 314 | type dummiesTooShortError struct { 315 | } 316 | 317 | func (e dummiesTooShortError) Error() string { 318 | return "all dummy records are too short" 319 | } 320 | -------------------------------------------------------------------------------- /datastores/disk/tablefile_test.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "reflect" 7 | "sort" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/jameycribbs/hare/dberr" 12 | ) 13 | 14 | func TestNewCloseTableFileTests(t *testing.T) { 15 | var tests = []func(t *testing.T){ 16 | func(t *testing.T) { 17 | //New... 18 | 19 | tf := newTestTableFile(t) 20 | defer tf.close() 21 | 22 | want := make(map[int]int64) 23 | want[1] = 0 24 | want[2] = 101 25 | want[3] = 160 26 | want[4] = 224 27 | 28 | got := tf.offsets 29 | 30 | if !reflect.DeepEqual(want, got) { 31 | t.Errorf("want %v; got %v", want, got) 32 | } 33 | }, 34 | func(t *testing.T) { 35 | //close... 36 | 37 | tf := newTestTableFile(t) 38 | tf.close() 39 | 40 | wantErr := dberr.ErrNoRecord 41 | _, gotErr := tf.readRec(3) 42 | 43 | if !errors.Is(gotErr, wantErr) { 44 | t.Errorf("want %v; got %v", wantErr, gotErr) 45 | } 46 | 47 | got := tf.offsets 48 | 49 | if nil != got { 50 | t.Errorf("want %v; got %v", nil, got) 51 | } 52 | }, 53 | } 54 | 55 | runTestFns(t, tests) 56 | } 57 | 58 | func TestDeleteRecTableFileTests(t *testing.T) { 59 | var tests = []func(t *testing.T){ 60 | func(t *testing.T) { 61 | //deleteRec... 62 | 63 | tf := newTestTableFile(t) 64 | defer tf.close() 65 | 66 | offset := tf.offsets[3] 67 | 68 | err := tf.deleteRec(3) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | want := "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" 74 | 75 | r := bufio.NewReader(tf.ptr) 76 | 77 | if _, err := tf.ptr.Seek(offset, 0); err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | rec, err := r.ReadBytes('\n') 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | got := string(rec) 86 | 87 | if want != got { 88 | t.Errorf("want %v; got %v", want, got) 89 | } 90 | }, 91 | } 92 | 93 | runTestFns(t, tests) 94 | } 95 | 96 | func TestGetLastIDTableFileTests(t *testing.T) { 97 | var tests = []func(t *testing.T){ 98 | func(t *testing.T) { 99 | //getLastID... 100 | 101 | tf := newTestTableFile(t) 102 | defer tf.close() 103 | 104 | want := 4 105 | got := tf.getLastID() 106 | 107 | if want != got { 108 | t.Errorf("want %v; got %v", want, got) 109 | } 110 | }, 111 | } 112 | 113 | runTestFns(t, tests) 114 | } 115 | 116 | func TestIDsTableFileTests(t *testing.T) { 117 | var tests = []func(t *testing.T){ 118 | func(t *testing.T) { 119 | //ids... 120 | 121 | tf := newTestTableFile(t) 122 | defer tf.close() 123 | 124 | want := []int{1, 2, 3, 4} 125 | got := tf.ids() 126 | sort.Ints(got) 127 | 128 | if len(want) != len(got) { 129 | t.Errorf("want %v; got %v", want, got) 130 | } else { 131 | 132 | for i := range want { 133 | if want[i] != got[i] { 134 | t.Errorf("want %v; got %v", want, got) 135 | } 136 | } 137 | } 138 | }, 139 | } 140 | 141 | runTestFns(t, tests) 142 | } 143 | 144 | func TestOffsetsTableFileTests(t *testing.T) { 145 | var tests = []func(t *testing.T){ 146 | func(t *testing.T) { 147 | //offsetForWritingRec... 148 | 149 | tf := newTestTableFile(t) 150 | defer tf.close() 151 | 152 | tests := []struct { 153 | recLen int 154 | want int 155 | }{ 156 | {45, 284}, 157 | {44, 56}, 158 | } 159 | 160 | for _, tt := range tests { 161 | want := int64(tt.want) 162 | got, err := tf.offsetForWritingRec(tt.recLen) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | if want != got { 167 | t.Errorf("want %v; got %v", want, got) 168 | } 169 | } 170 | }, 171 | func(t *testing.T) { 172 | //offsetToFitRec... 173 | 174 | tf := newTestTableFile(t) 175 | defer tf.close() 176 | 177 | tests := []struct { 178 | recLen int 179 | want int 180 | wanterr error 181 | }{ 182 | {284, 0, dummiesTooShortError{}}, 183 | {44, 56, nil}, 184 | } 185 | 186 | for _, tt := range tests { 187 | want := int64(tt.want) 188 | got, goterr := tf.offsetToFitRec(tt.recLen) 189 | if !((want == got) && (errors.Is(goterr, tt.wanterr))) { 190 | t.Errorf("want %v; wanterr %v; got %v; goterr %v", want, tt.wanterr, got, goterr) 191 | } 192 | } 193 | }, 194 | } 195 | 196 | runTestFns(t, tests) 197 | } 198 | 199 | func TestOverwriteRecTableFileTests(t *testing.T) { 200 | var tests = []func(t *testing.T){ 201 | func(t *testing.T) { 202 | //overwriteRec... 203 | 204 | tf := newTestTableFile(t) 205 | defer tf.close() 206 | 207 | offset := tf.offsets[3] 208 | 209 | err := tf.overwriteRec(160, 64) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | 214 | want := "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n" 215 | 216 | r := bufio.NewReader(tf.ptr) 217 | 218 | if _, err := tf.ptr.Seek(offset, 0); err != nil { 219 | t.Fatal(err) 220 | } 221 | 222 | rec, err := r.ReadBytes('\n') 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | got := string(rec) 227 | 228 | if want != got { 229 | t.Errorf("want %v; got %v", want, got) 230 | } 231 | }, 232 | } 233 | 234 | runTestFns(t, tests) 235 | } 236 | 237 | func TestReadRecTableFileTests(t *testing.T) { 238 | var tests = []func(t *testing.T){ 239 | func(t *testing.T) { 240 | //readRec... 241 | 242 | tf := newTestTableFile(t) 243 | defer tf.close() 244 | 245 | rec, err := tf.readRec(3) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | want := "{\"id\":3,\"first_name\":\"Bill\",\"last_name\":\"Shakespeare\",\"age\":18}\n" 251 | got := string(rec) 252 | 253 | if want != got { 254 | t.Errorf("want %v; got %v", want, got) 255 | } 256 | }, 257 | } 258 | 259 | runTestFns(t, tests) 260 | } 261 | 262 | func TestUpdateRecTableFileTests(t *testing.T) { 263 | var tests = []func(t *testing.T){ 264 | func(t *testing.T) { 265 | //updateRec (fits on same line)... 266 | 267 | tf := newTestTableFile(t) 268 | defer tf.close() 269 | 270 | err := tf.updateRec(3, []byte("{\"id\":3,\"first_name\":\"Bill\",\"last_name\":\"Shakespeare\",\"age\":92}")) 271 | if err != nil { 272 | t.Fatal(err) 273 | } 274 | 275 | wantOffset := int64(160) 276 | gotOffset := tf.offsets[3] 277 | 278 | if wantOffset != gotOffset { 279 | t.Errorf("want %v; got %v", wantOffset, gotOffset) 280 | } 281 | 282 | rec, err := tf.readRec(3) 283 | if err != nil { 284 | t.Fatal(err) 285 | } 286 | 287 | want := "{\"id\":3,\"first_name\":\"Bill\",\"last_name\":\"Shakespeare\",\"age\":92}\n" 288 | got := string(rec) 289 | 290 | if want != got { 291 | t.Errorf("want %v; got %v", want, got) 292 | } 293 | }, 294 | func(t *testing.T) { 295 | //updateRec (does not fit on same line)... 296 | 297 | tf := newTestTableFile(t) 298 | defer tf.close() 299 | 300 | err := tf.updateRec(3, []byte("{\"id\":3,\"first_name\":\"William\",\"last_name\":\"Shakespeare\",\"age\":18}")) 301 | if err != nil { 302 | t.Fatal(err) 303 | } 304 | 305 | wantOffset := int64(284) 306 | gotOffset := tf.offsets[3] 307 | 308 | if wantOffset != gotOffset { 309 | t.Errorf("want %v; got %v", wantOffset, gotOffset) 310 | } 311 | 312 | rec, err := tf.readRec(3) 313 | if err != nil { 314 | t.Fatal(err) 315 | } 316 | 317 | want := "{\"id\":3,\"first_name\":\"William\",\"last_name\":\"Shakespeare\",\"age\":18}\n" 318 | got := string(rec) 319 | 320 | if want != got { 321 | t.Errorf("want %v; got %v", want, got) 322 | } 323 | }, 324 | } 325 | 326 | runTestFns(t, tests) 327 | } 328 | 329 | func TestPadRecTableFileTests(t *testing.T) { 330 | var tests = []func(t *testing.T){ 331 | func(t *testing.T) { 332 | //padRec... 333 | 334 | want := "\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 335 | got := string(padRec(50)) 336 | 337 | if want != got { 338 | t.Errorf("want %v; got %v", want, got) 339 | } 340 | }, 341 | } 342 | 343 | for i, fn := range tests { 344 | testSetup(t) 345 | t.Run(strconv.Itoa(i), fn) 346 | testTeardown(t) 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /datastores/disk/testdata/contacts.bak: -------------------------------------------------------------------------------- 1 | {"id":1,"first_name":"John","last_name":"Doe","age":37} 2 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3 | {"id":2,"first_name":"Abe","last_name":"Lincoln","age":52} 4 | {"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18} 5 | {"id":4,"first_name":"Helen","last_name":"Keller","age":25} 6 | -------------------------------------------------------------------------------- /datastores/disk/testutils_test.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | func runTestFns(t *testing.T, tests []func(t *testing.T)) { 11 | for i, fn := range tests { 12 | testSetup(t) 13 | t.Run(strconv.Itoa(i), fn) 14 | testTeardown(t) 15 | } 16 | } 17 | 18 | func newTestDisk(t *testing.T) *Disk { 19 | dsk, err := New("./testdata", ".json") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | return dsk 25 | } 26 | 27 | func newTestTableFile(t *testing.T) *tableFile { 28 | filePtr, err := os.OpenFile("./testdata/contacts.json", os.O_RDWR, 0660) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | tf, err := newTableFile("contacts", filePtr) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | return tf 39 | } 40 | 41 | func testSetup(t *testing.T) { 42 | testRemoveFiles(t) 43 | 44 | cmd := exec.Command("cp", "./testdata/contacts.bak", "./testdata/contacts.json") 45 | if err := cmd.Run(); err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | func testTeardown(t *testing.T) { 51 | testRemoveFiles(t) 52 | } 53 | 54 | func testRemoveFiles(t *testing.T) { 55 | filesToRemove := []string{"contacts.json", "newtable.json"} 56 | 57 | for _, f := range filesToRemove { 58 | err := os.Remove("./testdata/" + f) 59 | if err != nil && !os.IsNotExist(err) { 60 | t.Fatal(err) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /datastores/ram/ram.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import "github.com/jameycribbs/hare/dberr" 4 | 5 | // Ram is a struct that holds a map of all the 6 | // tables in the datastore. 7 | type Ram struct { 8 | tables map[string]*table 9 | } 10 | 11 | // New takes a map of maps with seed data 12 | // and returns a pointer to a Ram struct. 13 | func New(seedData map[string]map[int]string) (*Ram, error) { 14 | var ram Ram 15 | 16 | if err := ram.init(seedData); err != nil { 17 | return nil, err 18 | } 19 | 20 | return &ram, nil 21 | } 22 | 23 | // Close closes the datastore. 24 | func (ram *Ram) Close() error { 25 | ram.tables = nil 26 | 27 | return nil 28 | } 29 | 30 | // CreateTable takes a table name, creates a new table 31 | // and adds it to the map of tables in the datastore. 32 | func (ram *Ram) CreateTable(tableName string) error { 33 | if ram.TableExists(tableName) { 34 | return dberr.ErrTableExists 35 | } 36 | 37 | ram.tables[tableName] = newTable() 38 | 39 | return nil 40 | } 41 | 42 | // DeleteRec takes a table name and a record id and deletes 43 | // the associated record. 44 | func (ram *Ram) DeleteRec(tableName string, id int) error { 45 | table, err := ram.getTable(tableName) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if err = table.deleteRec(id); err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // GetLastID takes a table name and returns the greatest record 58 | // id found in the table. 59 | func (ram *Ram) GetLastID(tableName string) (int, error) { 60 | table, err := ram.getTable(tableName) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | return table.getLastID(), nil 66 | } 67 | 68 | // IDs takes a table name and returns an array of all record IDs 69 | // found in the table. 70 | func (ram *Ram) IDs(tableName string) ([]int, error) { 71 | table, err := ram.getTable(tableName) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | return table.ids(), nil 77 | } 78 | 79 | // InsertRec takes a table name, a record id, and a byte array and adds 80 | // the record to the table. 81 | func (ram *Ram) InsertRec(tableName string, id int, rec []byte) error { 82 | table, err := ram.getTable(tableName) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if table.recExists(id) { 88 | return dberr.ErrIDExists 89 | } 90 | 91 | table.writeRec(id, rec) 92 | 93 | return nil 94 | } 95 | 96 | // ReadRec takes a table name and an id, reads the record from the 97 | // table, and returns a populated byte array. 98 | func (ram *Ram) ReadRec(tableName string, id int) ([]byte, error) { 99 | table, err := ram.getTable(tableName) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | rec, err := table.readRec(id) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return rec, err 110 | } 111 | 112 | // RemoveTable takes a table name and deletes that table from the 113 | // datastore. 114 | func (ram *Ram) RemoveTable(tableName string) error { 115 | if !ram.TableExists(tableName) { 116 | return dberr.ErrNoTable 117 | } 118 | 119 | delete(ram.tables, tableName) 120 | 121 | return nil 122 | } 123 | 124 | // TableExists takes a table name and returns a bool indicating 125 | // whether or not the table exists in the datastore. 126 | func (ram *Ram) TableExists(tableName string) bool { 127 | _, ok := ram.tables[tableName] 128 | 129 | return ok 130 | } 131 | 132 | // TableNames returns an array of table names. 133 | func (ram *Ram) TableNames() []string { 134 | var names []string 135 | 136 | for k := range ram.tables { 137 | names = append(names, k) 138 | } 139 | 140 | return names 141 | } 142 | 143 | // UpdateRec takes a table name, a record id, and a byte array and updates 144 | // the table record with that id. 145 | func (ram *Ram) UpdateRec(tableName string, id int, rec []byte) error { 146 | table, err := ram.getTable(tableName) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if !table.recExists(id) { 152 | return dberr.ErrNoRecord 153 | } 154 | 155 | table.writeRec(id, rec) 156 | 157 | return nil 158 | } 159 | 160 | //****************************************************************************** 161 | // UNEXPORTED METHODS 162 | //****************************************************************************** 163 | 164 | func (ram *Ram) getTable(tableName string) (*table, error) { 165 | table, ok := ram.tables[tableName] 166 | if !ok { 167 | return nil, dberr.ErrNoTable 168 | } 169 | 170 | return table, nil 171 | } 172 | 173 | func (ram *Ram) getTables() ([]string, error) { 174 | var tableNames []string 175 | 176 | for name := range ram.tables { 177 | tableNames = append(tableNames, name) 178 | } 179 | 180 | return tableNames, nil 181 | } 182 | 183 | func (ram *Ram) init(seedData map[string]map[int]string) error { 184 | ram.tables = make(map[string]*table) 185 | 186 | for tableName, tableData := range seedData { 187 | ram.tables[tableName] = newTable() 188 | 189 | for id, rec := range tableData { 190 | if err := ram.InsertRec(tableName, id, []byte(rec)); err != nil { 191 | return err 192 | } 193 | } 194 | } 195 | 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /datastores/ram/ram_test.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/jameycribbs/hare/dberr" 10 | ) 11 | 12 | func TestNewCloseRamTests(t *testing.T) { 13 | var tests = []func(t *testing.T){ 14 | func(t *testing.T) { 15 | //New... 16 | 17 | ram := newTestRam(t) 18 | defer ram.Close() 19 | 20 | want := make(map[int][]byte) 21 | want[1] = []byte(`{"id":1,"first_name":"John","last_name":"Doe","age":37}`) 22 | want[2] = []byte(`{"id":2,"first_name":"Abe","last_name":"Lincoln","age":52}`) 23 | want[3] = []byte(`{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}`) 24 | want[4] = []byte(`{"id":4,"first_name":"Helen","last_name":"Keller","age":25}`) 25 | 26 | got := ram.tables["contacts"].records 27 | 28 | if !reflect.DeepEqual(want, got) { 29 | t.Errorf("want %v; got %v", want, got) 30 | } 31 | }, 32 | func(t *testing.T) { 33 | //Close... 34 | 35 | ram := newTestRam(t) 36 | ram.Close() 37 | 38 | wantErr := dberr.ErrNoTable 39 | _, gotErr := ram.ReadRec("contacts", 3) 40 | 41 | if !errors.Is(gotErr, wantErr) { 42 | t.Errorf("want %v; got %v", wantErr, gotErr) 43 | } 44 | 45 | got := ram.tables 46 | 47 | if nil != got { 48 | t.Errorf("want %v; got %v", nil, got) 49 | } 50 | }, 51 | } 52 | 53 | runTestFns(t, tests) 54 | } 55 | 56 | func TestCreateTableRamTests(t *testing.T) { 57 | var tests = []func(t *testing.T){ 58 | func(t *testing.T) { 59 | //CreateTable... 60 | 61 | ram := newTestRam(t) 62 | defer ram.Close() 63 | 64 | err := ram.CreateTable("newtable") 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | want := true 70 | got := ram.TableExists("newtable") 71 | 72 | if want != got { 73 | t.Errorf("want %v; got %v", want, got) 74 | } 75 | }, 76 | func(t *testing.T) { 77 | //CreateTable (TableExists error)... 78 | 79 | ram := newTestRam(t) 80 | defer ram.Close() 81 | 82 | wantErr := dberr.ErrTableExists 83 | gotErr := ram.CreateTable("contacts") 84 | 85 | if !errors.Is(gotErr, wantErr) { 86 | t.Errorf("want %v; got %v", wantErr, gotErr) 87 | } 88 | }, 89 | } 90 | 91 | runTestFns(t, tests) 92 | } 93 | 94 | func TestDeleteRecRamTests(t *testing.T) { 95 | var tests = []func(t *testing.T){ 96 | func(t *testing.T) { 97 | //DeleteRec... 98 | 99 | ram := newTestRam(t) 100 | defer ram.Close() 101 | 102 | err := ram.DeleteRec("contacts", 3) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | want := dberr.ErrNoRecord 108 | _, got := ram.ReadRec("contacts", 3) 109 | 110 | if !errors.Is(got, want) { 111 | t.Errorf("want %v; got %v", want, got) 112 | } 113 | }, 114 | func(t *testing.T) { 115 | //DeleteRec (NoTable error)... 116 | 117 | ram := newTestRam(t) 118 | defer ram.Close() 119 | 120 | wantErr := dberr.ErrNoTable 121 | gotErr := ram.DeleteRec("nonexistent", 3) 122 | 123 | if !errors.Is(gotErr, wantErr) { 124 | t.Errorf("want %v; got %v", wantErr, gotErr) 125 | } 126 | }, 127 | } 128 | 129 | runTestFns(t, tests) 130 | } 131 | 132 | func TestGetLastIDRamTests(t *testing.T) { 133 | var tests = []func(t *testing.T){ 134 | func(t *testing.T) { 135 | //GetLastID... 136 | 137 | ram := newTestRam(t) 138 | defer ram.Close() 139 | 140 | want := 4 141 | got, err := ram.GetLastID("contacts") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | if want != got { 147 | t.Errorf("want %v; got %v", want, got) 148 | } 149 | }, 150 | func(t *testing.T) { 151 | //GetLastID (NoTable error)... 152 | 153 | ram := newTestRam(t) 154 | defer ram.Close() 155 | 156 | wantErr := dberr.ErrNoTable 157 | _, gotErr := ram.GetLastID("nonexistent") 158 | 159 | if !errors.Is(gotErr, wantErr) { 160 | t.Errorf("want %v; got %v", wantErr, gotErr) 161 | } 162 | }, 163 | } 164 | 165 | runTestFns(t, tests) 166 | } 167 | 168 | func TestIDsRamTests(t *testing.T) { 169 | var tests = []func(t *testing.T){ 170 | func(t *testing.T) { 171 | //IDs... 172 | 173 | ram := newTestRam(t) 174 | defer ram.Close() 175 | 176 | want := []int{1, 2, 3, 4} 177 | got, err := ram.IDs("contacts") 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | 182 | sort.Ints(got) 183 | 184 | if len(want) != len(got) { 185 | t.Errorf("want %v; got %v", want, got) 186 | } else { 187 | 188 | for i := range want { 189 | if want[i] != got[i] { 190 | t.Errorf("want %v; got %v", want, got) 191 | } 192 | } 193 | } 194 | }, 195 | func(t *testing.T) { 196 | //IDs (NoTable error)... 197 | 198 | ram := newTestRam(t) 199 | defer ram.Close() 200 | 201 | wantErr := dberr.ErrNoTable 202 | _, gotErr := ram.IDs("nonexistent") 203 | 204 | if !errors.Is(gotErr, wantErr) { 205 | t.Errorf("want %v; got %v", wantErr, gotErr) 206 | } 207 | }, 208 | } 209 | 210 | runTestFns(t, tests) 211 | } 212 | 213 | func TestInsertRecRamTests(t *testing.T) { 214 | var tests = []func(t *testing.T){ 215 | func(t *testing.T) { 216 | //InsertRec... 217 | 218 | ram := newTestRam(t) 219 | defer ram.Close() 220 | 221 | err := ram.InsertRec("contacts", 5, []byte(`{"id":5,"first_name":"Rex","last_name":"Stout","age":77}`)) 222 | if err != nil { 223 | t.Fatal(err) 224 | } 225 | 226 | rec, err := ram.ReadRec("contacts", 5) 227 | if err != nil { 228 | t.Fatal(err) 229 | } 230 | 231 | want := `{"id":5,"first_name":"Rex","last_name":"Stout","age":77}` 232 | got := string(rec) 233 | 234 | if want != got { 235 | t.Errorf("want %v; got %v", want, got) 236 | } 237 | }, 238 | func(t *testing.T) { 239 | //InsertRec (NoTable error)... 240 | 241 | ram := newTestRam(t) 242 | defer ram.Close() 243 | 244 | wantErr := dberr.ErrNoTable 245 | gotErr := ram.InsertRec("nonexistent", 5, []byte(`{"id":5,"first_name":"Rex","last_name":"Stout","age":77}`)) 246 | 247 | if !errors.Is(gotErr, wantErr) { 248 | t.Errorf("want %v; got %v", wantErr, gotErr) 249 | } 250 | }, 251 | func(t *testing.T) { 252 | //InsertRec (IDExists error)... 253 | 254 | ram := newTestRam(t) 255 | defer ram.Close() 256 | 257 | wantErr := dberr.ErrIDExists 258 | gotErr := ram.InsertRec("contacts", 3, []byte(`{"id":3,"first_name":"Rex","last_name":"Stout","age":77}`)) 259 | if !errors.Is(gotErr, wantErr) { 260 | t.Errorf("want %v; got %v", wantErr, gotErr) 261 | } 262 | 263 | rec, err := ram.ReadRec("contacts", 3) 264 | if err != nil { 265 | t.Fatal(err) 266 | } 267 | 268 | want := `{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}` 269 | got := string(rec) 270 | 271 | if want != got { 272 | t.Errorf("want %v; got %v", want, got) 273 | } 274 | }, 275 | } 276 | 277 | runTestFns(t, tests) 278 | } 279 | 280 | func TestRecRecRamTests(t *testing.T) { 281 | var tests = []func(t *testing.T){ 282 | func(t *testing.T) { 283 | //ReadRec... 284 | 285 | ram := newTestRam(t) 286 | defer ram.Close() 287 | 288 | rec, err := ram.ReadRec("contacts", 3) 289 | if err != nil { 290 | t.Fatal(err) 291 | } 292 | 293 | want := `{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}` 294 | got := string(rec) 295 | 296 | if want != got { 297 | t.Errorf("want %v; got %v", want, got) 298 | } 299 | }, 300 | func(t *testing.T) { 301 | //ReadRec (NoTable error)... 302 | 303 | ram := newTestRam(t) 304 | defer ram.Close() 305 | 306 | wantErr := dberr.ErrNoTable 307 | _, gotErr := ram.ReadRec("nonexistent", 3) 308 | 309 | if !errors.Is(gotErr, wantErr) { 310 | t.Errorf("want %v; got %v", wantErr, gotErr) 311 | } 312 | }, 313 | } 314 | 315 | runTestFns(t, tests) 316 | } 317 | 318 | func TestRemoveTableRamTests(t *testing.T) { 319 | var tests = []func(t *testing.T){ 320 | func(t *testing.T) { 321 | //RemoveTable... 322 | 323 | ram := newTestRam(t) 324 | defer ram.Close() 325 | 326 | err := ram.RemoveTable("contacts") 327 | if err != nil { 328 | t.Fatal(err) 329 | } 330 | 331 | want := false 332 | got := ram.TableExists("contacts") 333 | 334 | if want != got { 335 | t.Errorf("want %v; got %v", want, got) 336 | } 337 | }, 338 | func(t *testing.T) { 339 | //RemoveTable (NoTable error)... 340 | 341 | ram := newTestRam(t) 342 | defer ram.Close() 343 | 344 | wantErr := dberr.ErrNoTable 345 | gotErr := ram.RemoveTable("nonexistent") 346 | 347 | if !errors.Is(gotErr, wantErr) { 348 | t.Errorf("want %v; got %v", wantErr, gotErr) 349 | } 350 | }, 351 | } 352 | 353 | runTestFns(t, tests) 354 | } 355 | 356 | func TestTableExistsRamTests(t *testing.T) { 357 | var tests = []func(t *testing.T){ 358 | func(t *testing.T) { 359 | //TableExists... 360 | 361 | ram := newTestRam(t) 362 | defer ram.Close() 363 | 364 | want := true 365 | got := ram.TableExists("contacts") 366 | 367 | if want != got { 368 | t.Errorf("want %v; got %v", want, got) 369 | } 370 | 371 | want = false 372 | got = ram.TableExists("nonexistant") 373 | 374 | if want != got { 375 | t.Errorf("want %v; got %v", want, got) 376 | } 377 | }, 378 | } 379 | 380 | runTestFns(t, tests) 381 | } 382 | 383 | func TestTableNamesRamTests(t *testing.T) { 384 | var tests = []func(t *testing.T){ 385 | func(t *testing.T) { 386 | //TableNames... 387 | 388 | ram := newTestRam(t) 389 | defer ram.Close() 390 | 391 | want := []string{"contacts"} 392 | got := ram.TableNames() 393 | 394 | sort.Strings(got) 395 | 396 | if len(want) != len(got) { 397 | t.Errorf("want %v; got %v", want, got) 398 | } else { 399 | 400 | for i := range want { 401 | if want[i] != got[i] { 402 | t.Errorf("want %v; got %v", want, got) 403 | } 404 | } 405 | } 406 | }, 407 | } 408 | 409 | runTestFns(t, tests) 410 | } 411 | 412 | func TestUpdateRecRamTests(t *testing.T) { 413 | var tests = []func(t *testing.T){ 414 | func(t *testing.T) { 415 | //UpdateRec... 416 | 417 | ram := newTestRam(t) 418 | defer ram.Close() 419 | 420 | err := ram.UpdateRec("contacts", 3, []byte(`{"id":3,"first_name":"William","last_name":"Shakespeare","age":77}`)) 421 | if err != nil { 422 | t.Fatal(err) 423 | } 424 | 425 | rec, err := ram.ReadRec("contacts", 3) 426 | if err != nil { 427 | t.Fatal(err) 428 | } 429 | 430 | want := `{"id":3,"first_name":"William","last_name":"Shakespeare","age":77}` 431 | got := string(rec) 432 | 433 | if want != got { 434 | t.Errorf("want %v; got %v", want, got) 435 | } 436 | }, 437 | func(t *testing.T) { 438 | //UpdateRec (NoTable error)... 439 | 440 | ram := newTestRam(t) 441 | defer ram.Close() 442 | 443 | wantErr := dberr.ErrNoTable 444 | gotErr := ram.UpdateRec("nonexistent", 3, []byte(`{"id":3,"first_name":"William","last_name":"Shakespeare","age":77}`)) 445 | 446 | if !errors.Is(gotErr, wantErr) { 447 | t.Errorf("want %v; got %v", wantErr, gotErr) 448 | } 449 | }, 450 | } 451 | 452 | runTestFns(t, tests) 453 | } 454 | -------------------------------------------------------------------------------- /datastores/ram/table.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import "github.com/jameycribbs/hare/dberr" 4 | 5 | type table struct { 6 | records map[int][]byte 7 | } 8 | 9 | func newTable() *table { 10 | var t table 11 | 12 | t.records = make(map[int][]byte) 13 | 14 | return &t 15 | } 16 | 17 | func (t *table) deleteRec(id int) error { 18 | if !t.recExists(id) { 19 | return dberr.ErrNoRecord 20 | } 21 | 22 | delete(t.records, id) 23 | 24 | return nil 25 | } 26 | 27 | func (t *table) getLastID() int { 28 | var lastID int 29 | 30 | for id := range t.records { 31 | if id > lastID { 32 | lastID = id 33 | } 34 | } 35 | 36 | return lastID 37 | } 38 | 39 | func (t *table) ids() []int { 40 | ids := make([]int, len(t.records)) 41 | 42 | i := 0 43 | for id := range t.records { 44 | ids[i] = id 45 | i++ 46 | } 47 | 48 | return ids 49 | } 50 | 51 | func (t *table) readRec(id int) ([]byte, error) { 52 | rec, ok := t.records[id] 53 | if !ok { 54 | return nil, dberr.ErrNoRecord 55 | } 56 | 57 | return rec, nil 58 | } 59 | 60 | func (t *table) recExists(id int) bool { 61 | _, ok := t.records[id] 62 | 63 | return ok 64 | } 65 | 66 | func (t *table) writeRec(id int, rec []byte) { 67 | t.records[id] = rec 68 | } 69 | -------------------------------------------------------------------------------- /datastores/ram/table_test.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/jameycribbs/hare/dberr" 10 | ) 11 | 12 | func TestNewTableTests(t *testing.T) { 13 | var tests = []func(t *testing.T){ 14 | func(t *testing.T) { 15 | //New... 16 | 17 | tbl := newTestTable(t) 18 | 19 | want := make(map[int][]byte) 20 | want[1] = []byte(`{"id":1,"first_name":"John","last_name":"Doe","age":37}`) 21 | want[2] = []byte(`{"id":2,"first_name":"Abe","last_name":"Lincoln","age":52}`) 22 | want[3] = []byte(`{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}`) 23 | want[4] = []byte(`{"id":4,"first_name":"Helen","last_name":"Keller","age":25}`) 24 | 25 | got := tbl.records 26 | 27 | if !reflect.DeepEqual(want, got) { 28 | t.Errorf("want %v; got %v", want, got) 29 | } 30 | }, 31 | } 32 | 33 | runTestFns(t, tests) 34 | } 35 | 36 | func TestDeleteRecTableTests(t *testing.T) { 37 | var tests = []func(t *testing.T){ 38 | func(t *testing.T) { 39 | //deleteRec... 40 | 41 | tbl := newTestTable(t) 42 | 43 | err := tbl.deleteRec(3) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | wantErr := dberr.ErrNoRecord 49 | _, gotErr := tbl.readRec(3) 50 | if !errors.Is(gotErr, wantErr) { 51 | t.Errorf("want %v; got %v", wantErr, gotErr) 52 | } 53 | }, 54 | } 55 | 56 | runTestFns(t, tests) 57 | } 58 | 59 | func TestGetLastIDTableTests(t *testing.T) { 60 | var tests = []func(t *testing.T){ 61 | func(t *testing.T) { 62 | //getLastID... 63 | 64 | tbl := newTestTable(t) 65 | 66 | want := 4 67 | got := tbl.getLastID() 68 | 69 | if want != got { 70 | t.Errorf("want %v; got %v", want, got) 71 | } 72 | }, 73 | } 74 | 75 | runTestFns(t, tests) 76 | } 77 | 78 | func TestIDsTableTests(t *testing.T) { 79 | var tests = []func(t *testing.T){ 80 | func(t *testing.T) { 81 | //ids... 82 | 83 | tbl := newTestTable(t) 84 | 85 | want := []int{1, 2, 3, 4} 86 | got := tbl.ids() 87 | sort.Ints(got) 88 | 89 | if len(want) != len(got) { 90 | t.Errorf("want %v; got %v", want, got) 91 | } else { 92 | 93 | for i := range want { 94 | if want[i] != got[i] { 95 | t.Errorf("want %v; got %v", want, got) 96 | } 97 | } 98 | } 99 | }, 100 | } 101 | 102 | runTestFns(t, tests) 103 | } 104 | 105 | func TestReadRecTableTests(t *testing.T) { 106 | var tests = []func(t *testing.T){ 107 | func(t *testing.T) { 108 | //readRec... 109 | 110 | tbl := newTestTable(t) 111 | 112 | rec, err := tbl.readRec(3) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | want := `{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}` 118 | got := string(rec) 119 | 120 | if want != got { 121 | t.Errorf("want %v; got %v", want, got) 122 | } 123 | }, 124 | } 125 | 126 | runTestFns(t, tests) 127 | } 128 | 129 | func TestWriteRecTableTests(t *testing.T) { 130 | var tests = []func(t *testing.T){ 131 | func(t *testing.T) { 132 | //writeRec 133 | 134 | tbl := newTestTable(t) 135 | 136 | want := []byte(`{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":92}`) 137 | tbl.writeRec(3, want) 138 | 139 | got, err := tbl.readRec(3) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | if !reflect.DeepEqual(want, got) { 145 | t.Errorf("want %v; got %v", want, got) 146 | } 147 | }, 148 | } 149 | 150 | runTestFns(t, tests) 151 | } 152 | -------------------------------------------------------------------------------- /datastores/ram/testutils_test.go: -------------------------------------------------------------------------------- 1 | package ram 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | ) 7 | 8 | func runTestFns(t *testing.T, tests []func(t *testing.T)) { 9 | for i, fn := range tests { 10 | t.Run(strconv.Itoa(i), fn) 11 | } 12 | } 13 | 14 | func newTestRam(t *testing.T) *Ram { 15 | s := make(map[string]map[int]string) 16 | s["contacts"] = seedData() 17 | 18 | ram, err := New(s) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | return ram 24 | } 25 | 26 | func newTestTable(t *testing.T) *table { 27 | tbl := newTable() 28 | 29 | for k, v := range seedData() { 30 | tbl.records[k] = []byte(v) 31 | } 32 | 33 | return tbl 34 | } 35 | 36 | func seedData() map[int]string { 37 | s := make(map[int]string) 38 | s[1] = `{"id":1,"first_name":"John","last_name":"Doe","age":37}` 39 | s[2] = `{"id":2,"first_name":"Abe","last_name":"Lincoln","age":52}` 40 | s[3] = `{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}` 41 | s[4] = `{"id":4,"first_name":"Helen","last_name":"Keller","age":25}` 42 | 43 | return s 44 | } 45 | -------------------------------------------------------------------------------- /dberr/dberr.go: -------------------------------------------------------------------------------- 1 | package dberr 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrIDExists error means a record with the specified id already exists in the table. 7 | ErrIDExists = errors.New("hare: record with that id already exists") 8 | 9 | // ErrNoRecord error means no record with the specified id was not found. 10 | ErrNoRecord = errors.New("hare: no record with that id found") 11 | 12 | // ErrNoTable error means a table that the specified name does not exist. 13 | ErrNoTable = errors.New("hare: table with that name does not exist") 14 | 15 | // ErrTableExists error means a table with the specified name already exists in the database. 16 | ErrTableExists = errors.New("hare: table with that name already exists") 17 | ) 18 | -------------------------------------------------------------------------------- /examples/crud/crud.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "time" 7 | 8 | "github.com/jameycribbs/hare" 9 | "github.com/jameycribbs/hare/datastores/disk" 10 | "github.com/jameycribbs/hare/examples/crud/models" 11 | ) 12 | 13 | func main() { 14 | ds, err := disk.New("./data", ".json") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | db, err := hare.New(ds) 20 | if err != nil { 21 | panic(err) 22 | } 23 | defer db.Close() 24 | 25 | //----- CREATE ----- 26 | 27 | recID, err := db.Insert("episodes", &models.Episode{ 28 | Season: 6, 29 | Episode: 19, 30 | Film: "Red Zone Cuba", 31 | Shorts: []string{"Speech: Platform, Posture, and Appearance"}, 32 | YearFilmReleased: 1966, 33 | DateEpisodeAired: time.Date(1994, 12, 17, 0, 0, 0, 0, time.UTC), 34 | HostID: 2, // See associated Host model 35 | }) 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | fmt.Println("New record id is:", recID) 42 | 43 | //----- READ ----- 44 | 45 | rec := models.Episode{} 46 | 47 | err = db.Find("episodes", 4, &rec) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | // Notice that this is using the benefits of the associated 53 | // Host model to print the host's name. 54 | fmt.Printf("Found record is %v and it was hosted by %v\n", rec.Film, rec.Host.Name) 55 | 56 | //----- UPDATE ----- 57 | 58 | rec.Film = "The Skydivers - The Final Cut" 59 | if err = db.Update("episodes", &rec); err != nil { 60 | panic(err) 61 | } 62 | 63 | //----- DELETE ----- 64 | 65 | err = db.Delete("episodes", 2) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | //----- QUERYING ----- 71 | 72 | results, err := models.QueryEpisodes(db, func(r models.Episode) bool { 73 | // Notice that we are taking advantage of the 74 | // code we put in the Episode AfterFind method 75 | // to be able to do the query by the associated 76 | // host's name. 77 | return r.Host.Name == "Joel" 78 | }, 0) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | for _, r := range results { 84 | // Again, we are able to automatically use the host's name, because the 85 | // embedd Host struct was populated in the AfterFind method. 86 | fmt.Printf("%v hosted the season %v episode %v film, '%v'\n", r.Host.Name, r.Season, r.Episode, r.Film) 87 | 88 | // Here we are once again taking advantage of the code we put in the Episode 89 | // AfterFind method that automatically populates the episode's Comments struct 90 | // with associated records from the comments table. 91 | for _, c := range r.Comments { 92 | fmt.Printf("\t-- Comment for episode %v: %v\n", r.Episode, c.Text) 93 | } 94 | } 95 | } 96 | 97 | func init() { 98 | cmd := exec.Command("cp", "./data/episodes_default.txt", "./data/episodes.json") 99 | if err := cmd.Run(); err != nil { 100 | panic(err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /examples/crud/data/comments.json: -------------------------------------------------------------------------------- 1 | {"id":1,"episode_id":1,"text":"This is one of my favorite episodes."} 2 | {"id":2,"episode_id":1,"text":"I saw the original movie when I was a kid."} 3 | {"id":3,"episode_id":5,"text":"One of the most ambitiously bad movies ever made!"} 4 | {"id":4,"episode_id":6,"text":"Another Coleman Francis extravaganza."} 5 | {"id":5,"episode_id":3,"text":"This episode has some great Peter Graves riffs."} 6 | -------------------------------------------------------------------------------- /examples/crud/data/episodes_default.txt: -------------------------------------------------------------------------------- 1 | {"id":1,"season":1,"episode":9,"film":"Project Moonbase","shorts":["Radar Men from the Moon, Part 7","Radar Men from the Moon, Part 8"], "year_film_released":1953,"date_episode_aired":"1990-01-06T00:00:00Z","host_id":1} 2 | {"id":2,"season":2,"episode":3,"film":"Jungle Goddess","shorts":["The Phantom Creeps, Chapter 1: 'The Menacing Power'"], "year_film_released":1948,"date_episode_aired":"1990-10-06T00:00:00Z","host_id":1} 3 | {"id":3,"season":3,"episode":11,"film":"It Conquered the World","shorts":["Snow Thrills"], "year_film_released":1956,"date_episode_aired":"1991-08-24T00:00:00Z","host_id":1} 4 | {"id":4,"season":6,"episode":9,"film":"The Skydivers","shorts":["Why Study Industrial Arts?"], "year_film_released":1963,"date_episode_aired":"1994-08-27T00:00:00Z","host_id":2} 5 | {"id":5,"season":7,"episode":3,"film":"Deathstalker and the Warriors from Hell","shorts":[], "year_film_released":1988,"date_episode_aired":"1996-02-17T00:00:00Z","host_id":2} 6 | -------------------------------------------------------------------------------- /examples/crud/data/hosts.json: -------------------------------------------------------------------------------- 1 | {"id":1,"name":"Joel"} 2 | {"id":2,"name":"Mike"} 3 | {"id":3,"name":"Jonah"} 4 | -------------------------------------------------------------------------------- /examples/crud/models/comments.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jameycribbs/hare" 5 | ) 6 | 7 | // Comment is a record for a MST3K episode comment. 8 | type Comment struct { 9 | // Required field!!! 10 | ID int `json:"id"` 11 | EpisodeID int `json:"episode_id"` 12 | Text string `json:"text"` 13 | } 14 | 15 | // GetID returns the record id. 16 | // This method is used internally by Hare. 17 | // You need to add this method to each one of 18 | // your models. 19 | func (c *Comment) GetID() int { 20 | return c.ID 21 | } 22 | 23 | // SetID takes an id. This method is used 24 | // internally by Hare. 25 | // You need to add this method to each one of 26 | // your models. 27 | func (c *Comment) SetID(id int) { 28 | c.ID = id 29 | } 30 | 31 | // AfterFind is a callback that is run by Hare after 32 | // a record is found. 33 | // You need to add this method to each one of your 34 | // models. 35 | func (c *Comment) AfterFind(db *hare.Database) error { 36 | // IMPORTANT!!! These two lines of code are necessary in your AfterFind 37 | // in order for the Find method to work correctly! 38 | *c = Comment(*c) 39 | 40 | return nil 41 | } 42 | 43 | // QueryComments takes a Hare db handle and a query function, and returns 44 | // an array of comments. If you add this boilerplate method to your model 45 | // you can then write queries using a closure as the query language. 46 | func QueryComments(db *hare.Database, queryFn func(c Comment) bool, limit int) ([]Comment, error) { 47 | var results []Comment 48 | var err error 49 | 50 | ids, err := db.IDs("comments") 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | for _, id := range ids { 56 | c := Comment{} 57 | 58 | if err = db.Find("comments", id, &c); err != nil { 59 | return nil, err 60 | } 61 | 62 | if queryFn(c) { 63 | results = append(results, c) 64 | } 65 | 66 | if limit != 0 && limit == len(results) { 67 | break 68 | } 69 | } 70 | 71 | return results, err 72 | } 73 | -------------------------------------------------------------------------------- /examples/crud/models/episodes.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jameycribbs/hare" 7 | ) 8 | 9 | // Episode is a record for a MST3K episode. 10 | type Episode struct { 11 | // Required field!!! 12 | ID int `json:"id"` 13 | Season int `json:"season"` 14 | Episode int `json:"episode"` 15 | Film string `json:"film"` 16 | Shorts []string `json:"shorts"` 17 | YearFilmReleased int `json:"year_film_released"` 18 | DateEpisodeAired time.Time `json:"date_episode_aired"` 19 | HostID int `json:"host_id"` 20 | Host // embedded struct of Host model 21 | Comments []Comment // array of Comment models 22 | } 23 | 24 | // GetID returns the record id. 25 | // This method is used internally by Hare. 26 | // You need to add this method to each one of 27 | // your models. 28 | func (e *Episode) GetID() int { 29 | return e.ID 30 | } 31 | 32 | // SetID takes an id. This method is used 33 | // internally by Hare. 34 | // You need to add this method to each one of 35 | // your models. 36 | func (e *Episode) SetID(id int) { 37 | e.ID = id 38 | } 39 | 40 | // AfterFind is a callback that is run by Hare after 41 | // a record is found. 42 | // You need to add this method to each one of your 43 | // models, but the only required lines are the first one 44 | // and the last one. 45 | func (e *Episode) AfterFind(db *hare.Database) error { 46 | // IMPORTANT!!! This line of code is necessary in your AfterFind 47 | // in order for the Find method to work correctly! 48 | *e = Episode(*e) 49 | 50 | // Except for the last line, none of the lines below are 51 | // required, but they are a good example of extra 52 | // functionality you can implement in your callbacks. 53 | 54 | // This is an example of how you can do a Rails-like 55 | // "belongs_to" association. When an episode is found, this 56 | // code will run and lookup the associated host record then 57 | // populate the embedded Host struct. 58 | h := Host{} 59 | err := db.Find("hosts", e.HostID, &h) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | e.Host = h 65 | 66 | // This is an example of how you can do a Rails-like "has_many" 67 | // association. This will run a query on the comments table and 68 | // populate the episode's Comments embedded struct with child 69 | // comment records. 70 | e.Comments, err = QueryComments(db, func(c Comment) bool { 71 | return c.EpisodeID == e.ID 72 | }, 0) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | // IMPORTANT!!! This line of code is necessary in your AfterFind 78 | // in order for the Find method to work correctly! 79 | return nil 80 | } 81 | 82 | // QueryEpisodes takes a Hare db handle and a query function, and returns 83 | // an array of episodes. If you add this boilerplate function to your model 84 | // you can then write queries using a closure as the query language. 85 | func QueryEpisodes(db *hare.Database, queryFn func(e Episode) bool, limit int) ([]Episode, error) { 86 | var results []Episode 87 | var err error 88 | 89 | ids, err := db.IDs("episodes") 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | for _, id := range ids { 95 | e := Episode{} 96 | 97 | if err = db.Find("episodes", id, &e); err != nil { 98 | return nil, err 99 | } 100 | 101 | if queryFn(e) { 102 | results = append(results, e) 103 | } 104 | 105 | if limit != 0 && limit == len(results) { 106 | break 107 | } 108 | } 109 | 110 | return results, err 111 | } 112 | -------------------------------------------------------------------------------- /examples/crud/models/hosts.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jameycribbs/hare" 5 | ) 6 | 7 | // Host is a record for a MST3K episode comment. 8 | type Host struct { 9 | // Required field!!! 10 | ID int `json:"id"` 11 | Name string `json:"name"` 12 | } 13 | 14 | // GetID returns the record id. 15 | // This method is used internally by Hare. 16 | // You need to add this method to each one of 17 | // your models. 18 | func (h *Host) GetID() int { 19 | return h.ID 20 | } 21 | 22 | // SetID takes an id. This method is used 23 | // internally by Hare. 24 | // You need to add this method to each one of 25 | // your models. 26 | func (h *Host) SetID(id int) { 27 | h.ID = id 28 | } 29 | 30 | // AfterFind is a callback that is run by Hare after 31 | // a record is found. 32 | // You need to add this method to each one of your 33 | // models. 34 | func (h *Host) AfterFind(db *hare.Database) error { 35 | // IMPORTANT!!! These two lines of code are necessary in your AfterFind 36 | // in order for the Find method to work correctly! 37 | *h = Host(*h) 38 | 39 | return nil 40 | } 41 | 42 | // QueryHosts takes a Hare db handle and a query function, and returns 43 | // an array of comments. If you add this boilerplate method to your model 44 | // you can then write queries using a closure as the query language. 45 | func QueryHosts(db *hare.Database, queryFn func(h Host) bool, limit int) ([]Host, error) { 46 | var results []Host 47 | var err error 48 | 49 | ids, err := db.IDs("hosts") 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | for _, id := range ids { 55 | h := Host{} 56 | 57 | if err = db.Find("hosts", id, &h); err != nil { 58 | return nil, err 59 | } 60 | 61 | if queryFn(h) { 62 | results = append(results, h) 63 | } 64 | 65 | if limit != 0 && limit == len(results) { 66 | break 67 | } 68 | } 69 | 70 | return results, err 71 | } 72 | -------------------------------------------------------------------------------- /examples/dbadmin/compact/compact.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // This is an example of a script you could 11 | // write to periodically compact your database. 12 | 13 | // IMPORTANT!!! Before running this script or 14 | // one like it that you write, make sure no 15 | // processes are using the database!!! 16 | 17 | const dirPath = "./data/" 18 | const tblExt = ".json" 19 | 20 | func main() { 21 | files, err := ioutil.ReadDir(dirPath) 22 | if err != nil { 23 | panic(err) 24 | } 25 | 26 | for _, file := range files { 27 | filename := file.Name() 28 | 29 | // If entry is sub dir, current dir, or parent dir, skip it. 30 | if file.IsDir() || filename == "." || filename == ".." { 31 | continue 32 | } 33 | 34 | if !strings.HasSuffix(filename, tblExt) { 35 | continue 36 | } 37 | 38 | compactFile(filename) 39 | if err != nil { 40 | panic(err) 41 | } 42 | } 43 | } 44 | 45 | func compactFile(filename string) error { 46 | filepath := dirPath + filename 47 | backupFilepath := dirPath + strings.TrimSuffix(filename, tblExt) + ".old" 48 | 49 | // Move the table to a backup file. 50 | if err := os.Rename(filepath, backupFilepath); err != nil { 51 | return err 52 | } 53 | 54 | oldfile, err := os.Open(backupFilepath) 55 | if err != nil { 56 | return err 57 | } 58 | defer oldfile.Close() 59 | 60 | newfile, err := os.Create(filepath) 61 | if err != nil { 62 | return err 63 | } 64 | defer newfile.Close() 65 | 66 | oldfileScanner := bufio.NewScanner(oldfile) 67 | for oldfileScanner.Scan() { 68 | str := strings.TrimRight(oldfileScanner.Text(), "X") 69 | 70 | if len(str) > 0 { 71 | _, err := newfile.WriteString(str + "\n") 72 | if err != nil { 73 | return err 74 | } 75 | } 76 | } 77 | 78 | if err := oldfileScanner.Err(); err != nil { 79 | return err 80 | } 81 | 82 | newfile.Sync() 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /examples/dbadmin/compact/data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jameycribbs/hare/1e37662981b57a0fcdc8dee1390941a21ea3a482/examples/dbadmin/compact/data/.keep -------------------------------------------------------------------------------- /examples/dbadmin/dbadmin/data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jameycribbs/hare/1e37662981b57a0fcdc8dee1390941a21ea3a482/examples/dbadmin/dbadmin/data/.keep -------------------------------------------------------------------------------- /examples/dbadmin/dbadmin/dbadmin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jameycribbs/hare" 7 | "github.com/jameycribbs/hare/datastores/disk" 8 | ) 9 | 10 | func main() { 11 | ds, err := disk.New("./data", ".json") 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | db, err := hare.New(ds) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer db.Close() 21 | 22 | // Here is how to create a new table in the database. 23 | err = db.CreateTable("contacts") 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | // Here is how to check if a table exists in your database. 29 | if !db.TableExists("contacts") { 30 | fmt.Println("Table 'contacts' does not exist!") 31 | } 32 | 33 | // Here is how to drop a table. 34 | err = db.DropTable("contacts") 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jameycribbs/hare 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /hare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jameycribbs/hare/1e37662981b57a0fcdc8dee1390941a21ea3a482/hare.jpg -------------------------------------------------------------------------------- /testdata/contacts.bak: -------------------------------------------------------------------------------- 1 | {"id":1,"first_name":"John","last_name":"Doe","age":37} 2 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3 | {"id":2,"first_name":"Abe","last_name":"Lincoln","age":52} 4 | {"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18} 5 | {"id":4,"first_name":"Helen","last_name":"Keller","age":25} 6 | -------------------------------------------------------------------------------- /testutils_test.go: -------------------------------------------------------------------------------- 1 | package hare 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/jameycribbs/hare/datastores/disk" 12 | "github.com/jameycribbs/hare/datastores/ram" 13 | ) 14 | 15 | type Contact struct { 16 | ID int `json:"id"` 17 | FirstName string `json:"first_name"` 18 | LastName string `json:"last_name"` 19 | Age int `json:"age"` 20 | } 21 | 22 | func (c *Contact) GetID() int { 23 | return c.ID 24 | } 25 | 26 | func (c *Contact) SetID(id int) { 27 | c.ID = id 28 | } 29 | 30 | func (c *Contact) AfterFind(db *Database) error { 31 | *c = Contact(*c) 32 | 33 | return nil 34 | } 35 | 36 | func runTestFns(t *testing.T, testFns []func(*Database) func(*testing.T)) { 37 | for i, fn := range testFns { 38 | tstNum := strconv.Itoa(i) 39 | 40 | testSetup(t) 41 | 42 | diskDS, err := disk.New("./testdata", ".json") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | diskDB, err := New(diskDS) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer diskDB.Close() 52 | 53 | t.Run(fmt.Sprintf("disk/%s", tstNum), fn(diskDB)) 54 | 55 | ramDS, err := ram.New(seedData()) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | ramDB, err := New(ramDS) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | defer ramDB.Close() 65 | 66 | t.Run(fmt.Sprintf("ram/%s", tstNum), fn(ramDB)) 67 | 68 | testTeardown(t) 69 | } 70 | } 71 | 72 | func checkErr(t *testing.T, wantErr error, gotErr error) { 73 | if !errors.Is(gotErr, wantErr) { 74 | t.Errorf("want %v; got %v", wantErr, gotErr) 75 | } 76 | } 77 | 78 | func testSetup(t *testing.T) { 79 | testRemoveFiles(t) 80 | 81 | cmd := exec.Command("cp", "./testdata/contacts.bak", "./testdata/contacts.json") 82 | if err := cmd.Run(); err != nil { 83 | t.Fatal(err) 84 | } 85 | } 86 | 87 | func testTeardown(t *testing.T) { 88 | testRemoveFiles(t) 89 | } 90 | 91 | func testRemoveFiles(t *testing.T) { 92 | filesToRemove := []string{"contacts.json", "newtable.json"} 93 | 94 | for _, f := range filesToRemove { 95 | err := os.Remove("./testdata/" + f) 96 | if err != nil && !os.IsNotExist(err) { 97 | t.Fatal(err) 98 | } 99 | } 100 | } 101 | 102 | func seedData() map[string]map[int]string { 103 | tblMap := make(map[string]map[int]string) 104 | contactsMap := make(map[int]string) 105 | 106 | contactsMap[1] = `{"id":1,"first_name":"John","last_name":"Doe","age":37}` 107 | contactsMap[2] = `{"id":2,"first_name":"Abe","last_name":"Lincoln","age":52}` 108 | contactsMap[3] = `{"id":3,"first_name":"Bill","last_name":"Shakespeare","age":18}` 109 | contactsMap[4] = `{"id":4,"first_name":"Helen","last_name":"Keller","age":25}` 110 | 111 | tblMap["contacts"] = contactsMap 112 | 113 | return tblMap 114 | } 115 | --------------------------------------------------------------------------------