Type-safe DynamoDB query code generator for Go.
Write constrained PartiQL-like queries in .partiql files and dynac generates strongly typed methods that use the AWS SDK v2. The validator is intentionally strict to prevent accidental scans and unsafe queries.
Early-stage / design in progress. Expect breaking changes. The roadmap is in docs/plan/plan.md (Japanese).
- Go 1.25+
- AWS SDK v2 (required by generated code; pulled via Go modules)
Go (from this repository root):
go install ./cmd/dynacGo (from module path):
go install github.com/kotahorii/dynac/cmd/dynac@latestHomebrew (tap):
brew install kotahorii/dynac/dynacOr:
brew tap kotahorii/dynac
brew install dynacEnsure your GOBIN (or $(go env GOPATH)/bin) is on your PATH if you use go install.
- Define your models with
dynamodbavtags:
// internal/ddb/models.go
package ddb
type User struct {
PK string `dynamodbav:"pk"`
SK string `dynamodbav:"sk"`
UserName string `dynamodbav:"user_name"`
}- Write queries in
.partiqlfiles underqueries/(or pass--queries):
-- queries/user.partiql
-- name: GetUser :one
SELECT * FROM "App"
WHERE pk = ? AND sk = ?
-- name: ListUsersByOrg :many
-- @limit 100
SELECT * FROM "App"
WHERE pk = ? AND begins_with(sk, ?)
-- name: UpdateUserName :exec
UPDATE "App"
SET user_name = ?
WHERE pk = ? AND sk = ?- Generate code (defaults to
internal/ddb/queries_gen.go):
dynac generate --model ./internal/ddb
# or, without installing:
go run github.com/kotahorii/dynac/cmd/dynac@latest generate --model ./internal/ddb- Use the generated API:
package main
import (
"context"
"fmt"
"log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
// Replace with your own module path.
// Note: Go's internal packages can only be imported from within your module.
"example.com/yourapp/internal/ddb"
)
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatal(err)
}
client := dynamodb.NewFromConfig(cfg)
q := &ddb.Queries{Client: client, Table: "App"}
user, err := q.GetUser(ctx, "USER#1", "PROFILE")
if err != nil {
log.Fatal(err)
}
fmt.Println(user.UserName)
}Note: This example assumes AWS credentials are configured and a DynamoDB table named "App" exists with pk/sk keys.
A runnable demo that exercises Get/List/Put/Update/Delete lives under examples/sample-app.
See examples/sample-app/README.md for setup and run instructions.
- Validate queries without generating code:
dynac vet
# or, without installing:
go run github.com/kotahorii/dynac/cmd/dynac@latest vetdynac generate [flags]
dynac vet [flags]
Flags:
--table DynamoDB table name
--pkg output package path (default internal/ddb)
--model model file or directory (repeatable, generate only)
--pk partition key name (default pk)
--sk sort key name (default sk; set empty for single-key)
--queries query root (default queries)
Notes:
generaterequires--model.- Queries are discovered by scanning
--queries(defaultqueries/) for.partiqlfiles. - If you have no
queries/directory, create it or pass--queriesto point elsewhere. generatewritesqueries_gen.goandruntime_gen.gointo--pkg. If aruntime.goalready exists in that package,runtime_gen.gois not written.
Each query is defined by a name annotation followed by a single statement:
-- name: QueryName :one|:many|:exec
<SQL>
Supported statements (current):
SELECT * FROM "Table" WHERE ... [LIMIT n]INSERT INTO "Table" VALUE ?UPDATE "Table" SET a = ? [, b = ? ...] WHERE ... [RETURNING ...]DELETE FROM "Table" WHERE ... [RETURNING ...]
Placeholders must be ? (positional). Only AND is allowed in WHERE clauses.
The validator enforces DynamoDB-safe query shapes:
- Only
SELECT *is supported. :manyrequiresLIMIT,@limit, or@nolimit.:onerequires full key equality.- Only key attributes may appear in
WHERE. - Partition key conditions must be
=. - Sort key conditions must be left-contiguous and can be:
=begins_with(sk, ?)sk BETWEEN ? AND ?
OR,NOT, andINare rejected.
These rules are enforced by both dynac vet and dynac generate.
@limit N- maximum number of items returned by:many.@nolimit- allow unbounded:many(explicit opt-in).@index Name pk(a,b,...) sk(c,d,...)- use a GSI/LSI.@projection all- required when@indexis used.@model TypeName- override model type inference.
@index notes:
- Supports up to 4 PK attributes and 4 SK attributes.
- SK conditions must be left-contiguous (no gaps).
- Optional type hints are accepted (
pk(user_id:S)) and used to bind index key values as S/N/B.
:oneSELECT- Uses
GetItemif there is no@index. - Otherwise uses
QuerywithLimit=2and returnsErrInvalidif more than one item is returned.
- Uses
:manySELECT- Uses
Querywith a paginator, returning all pages. LIMIT/@limitcaps the total number of results.
- Uses
INSERT->PutItem- Queries prefixed with
Createadd a conditional expression (attribute_not_exists(pk)) and returnErrConflicton conflict.
- Queries prefixed with
UPDATE->UpdateItem- Adds
attribute_exists(pk)to prevent blind writes. :onerequiresRETURNING ALL OLD|NEWand returns the item.
- Adds
DELETE->DeleteItem- Adds
attribute_exists(pk)to prevent blind deletes. :onerequiresRETURNING ALL OLDand returns the item.
- Adds
Generated code depends on runtime helpers and errors defined in internal/ddb/runtime.go:
ErrNotFound, ErrConflict, ErrInvalid, ErrThrottled, ErrNotFoundTable, plus helpers such as
Queries, marshal, unmarshal, marshalValue, ptrBool, normalizeError, invalidErr,
and isConditionalFailed. If you change --pkg, copy internal/ddb/runtime.go into that package
or provide equivalent definitions.
- dynac reads Go structs from
--modelpaths and matches attributes viadynamodbavtags. - Every attribute referenced in a query must exist on the selected model type.
- If you omit
@model, dynac infers the model name from the query name (e.g.GetUser->User,ListUsersByOrg->User).
go test ./...go vet ./...gofmt -w ./cmd ./internalgo run ./cmd/dynac
- No projections other than
SELECT *. - No filters on non-key attributes.
- No
OR,NOT,IN, or comparison operators other than=andBETWEEN(plusbegins_withon the sort key). - No batch APIs yet (BatchGet/BatchWrite/ExecuteStatement).
If you'd like, I can also add a worked example under docs/ or expand the README with a generated-code snippet.