TableTheory v6 migration
TableTheory v6 is a Go-only major release with two breaking changes:
- the Go module path moves from
github.com/theory-cloud/tabletheory/v5togithub.com/theory-cloud/tabletheory/v6(Go semantic import versioning), and LambdaDB.WithLambdaTimeout(ctx)returnscore.DBinstead of*LambdaDB.
TypeScript and Python are unaffected by both. They ship no Lambda-optimized DB handle with a shadowed interface method, and they do not encode a Go module path, so no TypeScript or Python API, package version, or stored representation changes in this major.
Nothing about the stored representation changes: key encoding, marshalling, and the attributes written are identical to v5, and no data migration is required.
1. Go module path move
The module path carries the major version because Go requires a /vN suffix for
any module at v2 or above. The v6 source tree declares that path in go.mod and
in every internal, package, command, example, and contract-test import; the
example and contract-test modules point their require and replace directives
at github.com/theory-cloud/tabletheory/v6.
For Go consumers, update go.mod and every TableTheory import:
go get github.com/theory-cloud/tabletheory/v6@v6.0.0
go mod tidy
import (
tabletheory "github.com/theory-cloud/tabletheory/v6"
"github.com/theory-cloud/tabletheory/v6/pkg/session"
)
Every import that was github.com/theory-cloud/tabletheory/v5 — including
sub-packages such as /pkg/query, /pkg/transaction, /pkg/mocks, and
/pkg/testing/fakedb — becomes the same path with /v6. No TableTheory symbol
moves with it, so a mechanical rewrite covers the consumer’s own TableTheory
imports — but it is not sufficient on its own. A consumer whose other
dependencies expose TableTheory types in their public API cannot move until
those dependencies have; see Transitive dependencies.
Code generated by tabletheory gen --lang go emits /v6 imports, so regenerate
generated models after upgrading rather than hand-editing them.
Transitive dependencies
The import rewrite above is not enough on its own whenever another dependency
exposes TableTheory types in its public API. core.DB is a named interface, and
the /v5 and /v6 copies are distinct types: neither implements the other,
because the interface signatures refer to package-local types such as
core.Query. A consumer on /v6 therefore cannot pass its handle into a
dependency that is still on /v5.
AppTheory is that dependency today. Its published lines expose a TableTheory
major one behind their own — apptheory/v6 requires
github.com/theory-cloud/tabletheory/v5, and apptheory/v5 requires
tabletheory/v4 — so the AppTheory seam still takes a v5 handle:
apptheoryservices.NewDynamoDBEventBus(db tablecore.DB, config EventBusConfig)limited.NewDynamoRateLimiter(db tablecore.DB, config *Config, strategy RateLimitStrategy)mcpruntime.NewDynamoSessionStore(db tablecore.DB)mcpruntime.NewDynamoStreamStore(db tablecore.DB, opts ...DynamoStreamStoreOption)mcpruntime.NewDynamoTaskStore(db tablecore.DB)
tablecore here is github.com/theory-cloud/tabletheory/v5/pkg/core. Passing a
/v6 handle into any of those parameters fails to compile:
./main.go:10:44: cannot use db (variable of interface type "github.com/theory-cloud/tabletheory/v6/pkg/core".DB) as "github.com/theory-cloud/tabletheory/v5/pkg/core".DB value in argument to apptheoryservices.NewDynamoDBEventBus: "github.com/theory-cloud/tabletheory/v6/pkg/core".DB does not implement "github.com/theory-cloud/tabletheory/v5/pkg/core".DB (wrong type for method Model)
have Model(any) "github.com/theory-cloud/tabletheory/v6/pkg/core".Query
want Model(any) "github.com/theory-cloud/tabletheory/v5/pkg/core".Query
That shape — a /v6 handle rejected by a parameter whose type prints a /v5
path — is the signal to look for, and no consumer-side edit fixes it. It clears
only once AppTheory itself adopts /v6, so the migration has a wave order:
- TableTheory v6 is released.
- AppTheory adopts
/v6and republishes; its public API then accepts a/v6handle. - Consumers move their own module requirement and imports to
/v6.
Consumers still on a v4 TableTheory line — TCA-style apps among them — are on
apptheory/v5, whose seam takes a tabletheory/v4 handle, and chain both
guides: v5.md first, then this one. The same gating applies at each
TableTheory major they cross: each step needs the AppTheory line whose seam is
typed with that major.
2. LambdaDB.WithLambdaTimeout returns core.DB
What broke
*tabletheory.LambdaDB embeds core.ExtendedDB but declares its own
WithLambdaTimeout. Go compares method signatures for interface satisfaction by
identity, so while that declared method returned *LambdaDB it shadowed the
promoted interface method WithLambdaTimeout(ctx context.Context) DB and
*LambdaDB did not satisfy core.ExtendedDB. A consumer that asserted a
LambdaInit handle to core.ExtendedDB — the shape used to reach transaction and
schema methods — failed at Lambda cold start.
In v6 the declared method returns core.DB, so *LambdaDB satisfies core.DB
and core.ExtendedDB. The returned value is the configured *LambdaDB, and the
deadline, buffer, and cache behavior are unchanged.
Before and after
Before (v5 — no longer compiles):
var db *tabletheory.MultiAccountDB
func handler(ctx context.Context) (Response, error) {
partnerDB, err := db.Partner("partner1")
if err != nil {
return Response{}, err
}
// ✗ cannot use a core.DB value as *tabletheory.LambdaDB
partnerDB = partnerDB.WithLambdaTimeout(ctx)
return handleGetPayment(partnerDB)
}
func handleGetPayment(db *tabletheory.LambdaDB) (Response, error) { /* ... */ }
After (v6) — type the derived handle as core.DB, or as core.ExtendedDB when
you need schema or transaction methods:
import "github.com/theory-cloud/tabletheory/v6/pkg/core"
func handler(ctx context.Context) (Response, error) {
partnerDB, err := db.Partner("partner1")
if err != nil {
return Response{}, err
}
// ✓ the derived handle is interface-typed
var invocationDB core.DB = partnerDB.WithLambdaTimeout(ctx)
return handleGetPayment(invocationDB)
}
func handleGetPayment(db core.DB) (Response, error) { /* ... */ }
invocationDB := partnerDB.WithLambdaTimeout(ctx) infers the same core.DB type,
so the explicit annotation is only needed when the interface you want is not the
inferred one.
Migrating code that chains LambdaDB-specific methods
WithLambdaTimeoutConfig, PreRegisterModels, IsModelRegistered,
GetMemoryStats, OptimizeForMemory, and OptimizeForColdStart stay on the
concrete *tabletheory.LambdaDB. There are two supported patterns:
-
Apply the timeout last. Call the LambdaDB-specific methods on the concrete handle, then derive the invocation handle:
base, err := tabletheory.LambdaInit(&User{}, &Session{}) if err != nil { panic(err) } configured := base.WithLambdaTimeoutConfig(tabletheory.LambdaTimeoutConfig{ Buffer: 500 * time.Millisecond, }) invocationDB := configured.WithLambdaTimeout(ctx) // core.DB -
Assert the concrete handle back when the concrete type is genuinely required. The dynamic value is always the configured
*LambdaDB:lambdaDB, ok := base.WithLambdaTimeout(ctx).(*tabletheory.LambdaDB) if !ok { // only reachable when the receiver was not a *tabletheory.LambdaDB }
Unchanged LambdaDB API
LambdaInit, NewLambdaOptimized, and MultiAccountDB.Partner still return
*tabletheory.LambdaDB, and MultiAccountDB.WithContext still returns
*MultiAccountDB. None of them is part of a core interface, and a *LambdaDB
value is assignable wherever core.DB or core.ExtendedDB is expected, so no
consumer has to change for them. WithLambdaTimeoutConfig keeps returning
*LambdaDB for the same reason: it is not part of an interface, and keeping the
concrete result is strictly more permissive.
3. Consumer migration checklist
- Update the module requirement and every import (section 1).
- Audit every
WithLambdaTimeoutcall: none may assign the result back to a*tabletheory.LambdaDBvariable or pass it to a*tabletheory.LambdaDBparameter (section 2). - Read the release notes generated for v6.0.0, and the
BREAKING CHANGE:footers of the v6 pull request, for the behavioral changes this major ships. release-please writes the changelog and release notes at release time, so the changelog’sUnreleasedsection is not the v6 source of truth before the release exists. - Run the consumer’s real DynamoDB Local integration suite. Put-semantics fakes are insufficient for a major upgrade.
Release-lane transition
The source tree adopts the /v6 Go module path before release-please advances the
single manifest from the latest v5 stable baseline. During that bounded state,
scripts/verify-go-semantic-import-version.sh accepts exactly a one-major advance
on topic branches and integration/promotion PRs only when this migration guide and
a non-empty Unreleased Breaking Changes section exist.
main never accepts the pending mismatch. A premain push in this state is fenced
to two non-publishing contexts: the prerelease-PR workflow may generate the v6 RC
PR, while the prerelease publication workflow must skip release creation. The
generated v6 release PR must advance the manifest to v6 before any prerelease
artifact can be published, at which point the module and manifest majors match
strictly again. No manifest, tag, or package version is hand-edited to create this
transition.
Coordination
Autheory, theory-mcp-server, and other Go consumers that type a TableTheory
Lambda handle as *tabletheory.LambdaDB must migrate before adopting v6. The
TableTheory maintainer coordinates those downstream migrations; this repository
does not modify consumer repositories.