-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(allsrv): add sqlite db implementation and test suite
The test suite underwent a few changes here. There are race conditions in the original test suite, when it comes to executing the concurrency focused tests. Made a small update here to address that. TL/DR the race is with the actual `*testing.T` type, so we make use of the closure to capture the error and log. This enforces the `testing.T` access is done **after** any test behavior. We are also able to update the error handling a bit here. I don't care much for what the error message says, but I care deeply about the behavior of the errors I receive. I want to validate the correct behavior is obtained. This is very useful when integrating amongst a larger, more complex system. For this trivial `Foo` server, we don't have much complexity. The sqlite db implementation here is fairly trivial once again. We're able to reuse the test suite in full. All that was required is a new funciton to initize the unit under test (UUT), and the rest is including 4 more lines of code to call the test around it. Not to shabby. Since we've effectively decoupled our domain `Foo` from the db entity `foo`, we've provided maximum flexibility to our database implementation without having to pollute the domain space. This is intensely useful as a system grows. Think through, what would it look like to add a `PostgreSQL` db implementation? Not as much now that you have a test suite to verify the desired behavior. The last thing that is missing here is what we do to decouple our server from HTTP. There is a glaring hole in our design, and that's the lack of service layer. The layer where all our business logic resides. Take a moment to think through what this might look like. Question: How would you break up the server so that it's no longer coupled to HTTP/REST? Question: What can we do to allow ourselves to support any myriad of `RPC` technologies without duplicating allthe business logic?
- Loading branch information
Showing
12 changed files
with
362 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package allsrv | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"errors" | ||
"sync" | ||
"time" | ||
|
||
sq "github.com/Masterminds/squirrel" | ||
"github.com/jmoiron/sqlx" | ||
"github.com/mattn/go-sqlite3" | ||
) | ||
|
||
// NewSQLiteDB creates a new sqlite db. | ||
func NewSQLiteDB(db *sqlx.DB) DB { | ||
return &sqlDB{ | ||
db: db, | ||
sq: sq.StatementBuilder.PlaceholderFormat(sq.Question), | ||
} | ||
} | ||
|
||
type sqlDB struct { | ||
db *sqlx.DB | ||
sq sq.StatementBuilderType | ||
|
||
mu sync.RWMutex | ||
} | ||
|
||
func (s *sqlDB) CreateFoo(ctx context.Context, f Foo) error { | ||
sb := s.sq. | ||
Insert("foos"). | ||
Columns("id", "name", "note", "created_at", "updated_at"). | ||
Values(f.ID, f.Name, f.Note, f.CreatedAt, f.UpdatedAt) | ||
|
||
_, err := s.exec(ctx, sb) | ||
return err | ||
} | ||
|
||
func (s *sqlDB) ReadFoo(ctx context.Context, id string) (Foo, error) { | ||
s.mu.RLock() | ||
defer s.mu.RUnlock() | ||
|
||
const query = `SELECT * FROM foos WHERE id=?` | ||
|
||
var ent entFoo | ||
err := s.db.GetContext(ctx, &ent, s.db.Rebind(query), id) | ||
if err != nil { | ||
if errors.Is(err, sql.ErrNoRows) { | ||
return Foo{}, NotFoundErr("foo not found for id: " + id) | ||
} | ||
return Foo{}, err | ||
} | ||
|
||
out := Foo{ | ||
ID: ent.ID, | ||
Name: ent.Name, | ||
Note: ent.Note, | ||
CreatedAt: ent.CreatedAt, | ||
UpdatedAt: ent.UpdatedAt, | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func (s *sqlDB) UpdateFoo(ctx context.Context, f Foo) error { | ||
sb := s.sq. | ||
Update("foos"). | ||
Set("name", f.Name). | ||
Set("note", f.Note). | ||
Set("updated_at", f.UpdatedAt). | ||
Where(sq.Eq{"id": f.ID}) | ||
|
||
return s.update(ctx, sb) | ||
} | ||
|
||
func (s *sqlDB) DelFoo(ctx context.Context, id string) error { | ||
err := s.update(ctx, s.sq.Delete("foos").Where(sq.Eq{"id": id})) | ||
return err | ||
} | ||
|
||
func (s *sqlDB) exec(ctx context.Context, sqlizer sq.Sqlizer) (sql.Result, error) { | ||
query, args, err := sqlizer.ToSql() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
s.mu.Lock() | ||
defer s.mu.Unlock() | ||
|
||
res, err := s.db.ExecContext(ctx, query, args...) | ||
if sqErr := new(sqlite3.Error); errors.As(err, sqErr) { | ||
switch sqErr.Code { | ||
case sqlite3.ErrConstraint: | ||
return nil, ExistsErr("foo exists") | ||
} | ||
} | ||
return res, err | ||
} | ||
|
||
func (s *sqlDB) update(ctx context.Context, sqlizer sq.Sqlizer) error { | ||
res, err := s.exec(ctx, sqlizer) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
n, err := res.RowsAffected() | ||
if err == nil && n == 0 { | ||
return NotFoundErr("foo not found") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type entFoo struct { | ||
ID string `db:"id"` | ||
Name string `db:"name"` | ||
Note string `db:"note"` | ||
CreatedAt time.Time `db:"created_at"` | ||
UpdatedAt time.Time `db:"updated_at"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package allsrv_test | ||
|
||
import ( | ||
"database/sql" | ||
"testing" | ||
|
||
"github.com/golang-migrate/migrate/v4" | ||
migsqlite "github.com/golang-migrate/migrate/v4/database/sqlite3" | ||
"github.com/golang-migrate/migrate/v4/source/iofs" | ||
"github.com/jmoiron/sqlx" | ||
_ "github.com/mattn/go-sqlite3" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/jsteenb2/mess/allsrv" | ||
"github.com/jsteenb2/mess/allsrv/migrations" | ||
) | ||
|
||
func TestSQLite(t *testing.T) { | ||
testDB(t, func(t *testing.T) allsrv.DB { | ||
db := newSQLiteInmem(t) | ||
t.Cleanup(func() { | ||
assert.NoError(t, db.Close()) | ||
}) | ||
|
||
return allsrv.NewSQLiteDB(db) | ||
}) | ||
} | ||
|
||
func newSQLiteInmem(t *testing.T) *sqlx.DB { | ||
const driver = "sqlite3" | ||
|
||
db, err := sql.Open(driver, ":memory:") | ||
require.NoError(t, err) | ||
|
||
const dbName = "testdb" | ||
drvr, err := migsqlite.WithInstance(db, &migsqlite.Config{DatabaseName: dbName}) | ||
require.NoError(t, err) | ||
|
||
iodrvr, err := iofs.New(migrations.SQLite, "sqlite") | ||
require.NoError(t, err) | ||
|
||
m, err := migrate.NewWithInstance("iofs", iodrvr, dbName, drvr) | ||
require.NoError(t, err) | ||
require.NoError(t, m.Up()) | ||
|
||
dbx := sqlx.NewDb(db, driver) | ||
dbx.SetMaxIdleConns(1) | ||
return dbx | ||
} |
Oops, something went wrong.