Skip to content

Commit

Permalink
feat: inject DB verb resources
Browse files Browse the repository at this point in the history
  • Loading branch information
worstell committed Oct 3, 2024
1 parent dcd3dc7 commit 74768cc
Show file tree
Hide file tree
Showing 30 changed files with 481 additions and 197 deletions.
8 changes: 4 additions & 4 deletions backend/controller/sql/testdata/go/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK.
)

var db = ftl.PostgresDatabase("testdb")
type testdb = ftl.PostgresDatabaseHandle

type InsertRequest struct {
Data string
Expand All @@ -15,16 +15,16 @@ type InsertRequest struct {
type InsertResponse struct{}

//ftl:verb
func Insert(ctx context.Context, req InsertRequest) (InsertResponse, error) {
err := persistRequest(ctx, req)
func Insert(ctx context.Context, req InsertRequest, db testdb) (InsertResponse, error) {
err := persistRequest(ctx, req, db)
if err != nil {
return InsertResponse{}, err
}

return InsertResponse{}, nil
}

func persistRequest(ctx context.Context, req InsertRequest) error {
func persistRequest(ctx context.Context, req InsertRequest, db testdb) error {
_, err := db.Get(ctx).Exec(`CREATE TABLE IF NOT EXISTS requests
(
data TEXT,
Expand Down
33 changes: 27 additions & 6 deletions backend/controller/sql/testdata/go/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,26 @@ import (

func TestDatabase(t *testing.T) {
ctx := ftltest.Context(
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithProjectFile("ftl-project.toml"),
ftltest.WithDatabase(db),
ftltest.WithDatabases(),
)

Insert(ctx, InsertRequest{Data: "unit test 1"})
_, err := ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 1"})
assert.NoError(t, err)
list, err := getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
assert.Equal(t, "unit test 1", list[0])

ctx = ftltest.Context(
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithProjectFile("ftl-project.toml"),
ftltest.WithDatabase(db),
ftltest.WithDatabases(),
)

Insert(ctx, InsertRequest{Data: "unit test 2"})
_, err = ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 2"})
assert.NoError(t, err)
list, err = getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
Expand All @@ -35,18 +39,35 @@ func TestDatabase(t *testing.T) {

func TestOptionOrdering(t *testing.T) {
ctx := ftltest.Context(
ftltest.WithDatabase(db), // <--- consumes DSNs
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithDatabases(), // <--- consumes DSNs
ftltest.WithProjectFile("ftl-project.toml"), // <--- provides DSNs
)

Insert(ctx, InsertRequest{Data: "unit test 1"})
_, err := ftltest.Call[InsertClient, InsertRequest, InsertResponse](ctx, InsertRequest{Data: "unit test 1"})
assert.NoError(t, err)
list, err := getAll(ctx)
assert.NoError(t, err)
assert.Equal(t, 1, len(list))
assert.Equal(t, "unit test 1", list[0])
}

func TestWrongDbNameFetch(t *testing.T) {
ctx := ftltest.Context(
ftltest.WithCallsAllowedWithinModule(),
ftltest.WithProjectFile("ftl-project.toml"),
ftltest.WithDatabases(),
)

_, err := ftltest.GetDatabaseHandle(ctx, "Testdb")
assert.Error(t, err, `could not find database "Testdb"; did you mean "testdb"?`)
}

func getAll(ctx context.Context) ([]string, error) {
db, err := ftltest.GetDatabaseHandle(ctx, "testdb")
if err != nil {
return nil, err
}
rows, err := db.Get(ctx).Query("SELECT data FROM requests ORDER BY created_at;")
if err != nil {
return nil, err
Expand Down
20 changes: 20 additions & 0 deletions backend/controller/sql/testdata/go/database/types.ftl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/provisioner/testdata/go/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/TBD54566975/ftl/go-runtime/ftl"
)

var db = ftl.PostgresDatabase("echodb")
var EchoDb = ftl.PostgresDatabaseHandle

// Echo returns a greeting with the current time.
//
Expand Down
13 changes: 8 additions & 5 deletions docs/content/docs/reference/unittests.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,19 @@ ctx := ftltest.Context(
### Databases
By default, calling `Get(ctx)` on a database panics.

To enable database access in a test, you must first [provide a DSN via a project file](#project-files-configs-and-secrets). You can then set up a test database:
To enable database access in a test, you must first [provide a DSN via a project file](#project-files-configs-and-secrets).

You can then opt for `WithDatabases()` in your context, and all databases declared in your module will be
automatically provided for tests.
```go
ctx := ftltest.Context(
ftltest.WithDefaultProjectFile(),
ftltest.WithDatabase(db),
ftltest.WithDatabases(),
)
```
This will:
- Take the provided DSN and appends `_test` to the database name. Eg: `accounts` becomes `accounts_test`
- Wipe all tables in the database so each test run happens on a clean database
Note:
- Database names from the provided DSNs will be appended with `_test`. Eg: `accounts` becomes `accounts_test`
- All tables in the database are wiped between tests, so each test run happens on a clean database


### Maps
Expand Down
11 changes: 11 additions & 0 deletions go-runtime/compile/build-template/.ftl.tmpl/go/main/main.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- $verbs := .Verbs -}}
{{- $dbs := .Databases -}}
{{- $name := .Name -}}
{{- with .MainCtx -}}

Expand All @@ -24,6 +25,11 @@ func init() {
{{- range .ExternalTypes}}
reflection.ExternalType(*new({{.TypeName}})),
{{- end}}
{{- range $dbs}}
{{- if eq .Type "postgres" }}
reflection.Database("{{.Module}}", "{{.Name}}", server.InitPostgres),
{{- end }}
{{- end}}
{{- range $verbs}}
reflection.ProvideResourcesForVerb(
{{.TypeName}},
Expand All @@ -38,6 +44,11 @@ func init() {
{{- else }}
server.VerbClient[{{.TypeName}}, {{.Request.TypeName}}, {{.Response.TypeName}}](),
{{- end -}}
{{- end }}
{{- with getDatabaseHandle . }}
{{- if eq .Type "postgres" }}
server.PostgresDatabase("{{.Module}}", "{{.Name}}"),
{{- end }}
{{- end }}
{{- end}}
),
Expand Down
11 changes: 11 additions & 0 deletions go-runtime/compile/build-template/types.ftl.go.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- $verbs := .Verbs -}}
{{- $dbs := .Databases -}}
{{- $name := .Name -}}
{{- with .TypesCtx -}}
{{- $moduleName := .MainModulePkg -}}
Expand Down Expand Up @@ -42,6 +43,11 @@ func init() {
{{- range .ExternalTypes}}
reflection.ExternalType(*new({{.TypeName}})),
{{- end}}
{{- range $dbs}}
{{- if eq .Type "postgres" }}
reflection.Database("{{.Module}}", "{{.Name}}", server.InitPostgres),
{{- end }}
{{- end}}
{{- range $verbs}}
reflection.ProvideResourcesForVerb(
{{ trimModuleQualifier $moduleName .TypeName }},
Expand All @@ -58,6 +64,11 @@ func init() {
{{- else }}
server.VerbClient[{{$verb}}, {{.Request.LocalTypeName}}, {{.Response.LocalTypeName}}](),
{{- end }}
{{- end }}
{{- with getDatabaseHandle . }}
{{- if eq .Type "postgres" }}
server.PostgresDatabase("{{.Module}}", "{{.Name}}"),
{{- end }}
{{- end }}
{{- end}}
),
Expand Down
70 changes: 70 additions & 0 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"unicode"

"github.com/TBD54566975/ftl/go-runtime/schema/common"
"github.com/TBD54566975/scaffolder"
"github.com/alecthomas/types/optional"
sets "github.com/deckarep/golang-set/v2"
Expand Down Expand Up @@ -56,6 +57,7 @@ type mainModuleContext struct {
Name string
SharedModulesPaths []string
Verbs []goVerb
Databases []goDBHandle
Replacements []*modfile.Replace
MainCtx mainFileContext
TypesCtx typesFileContext
Expand Down Expand Up @@ -99,6 +101,9 @@ func (c *mainModuleContext) generateTypesImports(mainModuleImport string) []stri
if len(c.Verbs) > 0 {
imports.Add(`"context"`)
}
if len(c.Databases) > 0 {
imports.Add(`"github.com/TBD54566975/ftl/go-runtime/server"`)
}
for _, st := range c.TypesCtx.SumTypes {
imports.Add(st.importStatement())
for _, v := range st.Variants {
Expand Down Expand Up @@ -249,6 +254,20 @@ type verbClient struct {

func (v verbClient) resource() {}

type goDBHandle struct {
Type string
Name string
Module string

nativeType
}

func (d goDBHandle) resource() {}

func (d goDBHandle) getNativeType() nativeType {
return d.nativeType
}

type ModifyFilesTransaction interface {
Begin() error
ModifiedFiles(paths ...string) error
Expand Down Expand Up @@ -571,6 +590,7 @@ func (b *mainModuleContextBuilder) build(goModVersion, ftlVersion, projectName s
SharedModulesPaths: sharedModulesPaths,
Replacements: replacements,
Verbs: make([]goVerb, 0, len(b.mainModule.Decls)),
Databases: make([]goDBHandle, 0, len(b.mainModule.Decls)),
MainCtx: mainFileContext{
ProjectName: projectName,
SumTypes: []goSumType{},
Expand Down Expand Up @@ -653,6 +673,8 @@ func (b *mainModuleContextBuilder) visit(
case goExternalType:
ctx.TypesCtx.ExternalTypes = append(ctx.TypesCtx.ExternalTypes, n)
ctx.MainCtx.ExternalTypes = append(ctx.MainCtx.ExternalTypes, n)
case goDBHandle:
ctx.Databases = append(ctx.Databases, n)
}
return next()
})
Expand Down Expand Up @@ -690,6 +712,15 @@ func (b *mainModuleContextBuilder) getGoType(module *schema.Module, node schema.
return optional.None[goType](), isLocal, nil
}
return b.processExternalTypeAlias(n), isLocal, nil
case *schema.Database:
if !isLocal {
return optional.None[goType](), false, nil
}
dbHandle, err := b.processDatabase(module.Name, n)
if err != nil {
return optional.None[goType](), isLocal, err
}
return optional.Some[goType](dbHandle), isLocal, nil

default:
}
Expand Down Expand Up @@ -788,6 +819,26 @@ func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error
calleeverb,
})
}
case *schema.MetadataDatabases:
for _, call := range md.Calls {
resolved, ok := b.sch.Resolve(call).Get()
if !ok {
return goVerb{}, fmt.Errorf("failed to resolve %s database, used by %s.%s", call,
b.mainModule.Name, verb.Name)
}
db, ok := resolved.(*schema.Database)
if !ok {
return goVerb{}, fmt.Errorf("%s.%s uses %s database handle, but %s is not a database",
b.mainModule.Name, verb.Name, call, call)
}

dbHandle, err := b.processDatabase(call.Module, db)
if err != nil {
return goVerb{}, err
}
resources = append(resources, dbHandle)
}

default:
// TODO: implement other resources
}
Expand All @@ -800,6 +851,19 @@ func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error
return b.getGoVerb(nativeName, verb, resources...)
}

func (b *mainModuleContextBuilder) processDatabase(moduleName string, db *schema.Database) (goDBHandle, error) {
nt, err := b.getNativeType(common.FtlPostgresDBTypePath)
if err != nil {
return goDBHandle{}, err
}
return goDBHandle{
Name: db.Name,
Module: moduleName,
Type: db.Type,
nativeType: nt,
}, nil
}

func (b *mainModuleContextBuilder) getGoVerb(nativeName string, verb *schema.Verb, resources ...verbResource) (goVerb, error) {
nt, err := b.getNativeType(nativeName)
if err != nil {
Expand Down Expand Up @@ -985,6 +1049,12 @@ var scaffoldFuncs = scaffolder.FuncMap{
}
return nil
},
"getDatabaseHandle": func(resource verbResource) *goDBHandle {
if c, ok := resource.(goDBHandle); ok {
return &c
}
return nil
},
}

// returns the import path and the directory name for a type alias if there is an associated go library
Expand Down
3 changes: 2 additions & 1 deletion go-runtime/compile/testdata/go/one/one.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ type ExportedData struct {

var configValue = ftl.Config[Config]("configValue")
var secretValue = ftl.Secret[string]("secretValue")
var testDb = ftl.PostgresDatabase("testDb")

type testDb = ftl.PostgresDatabaseHandle

//ftl:verb
func Verb(ctx context.Context, req Req) (Resp, error) {
Expand Down
Loading

0 comments on commit 74768cc

Please sign in to comment.