Skip to content

Conversation

@ivanauth
Copy link
Contributor

@ivanauth ivanauth commented Dec 16, 2025

When users start SpiceDB without running migrations first, they get a confusing error like:

unable to list namespaces: ERROR: relation "namespace_config" does not exist (SQLSTATE 42P01)

This doesn't tell them what went wrong or how to fix it.

Now the error message is clear and actionable:

datastore error: the database schema has not been initialized; please run "spicedb datastore migrate": ERROR: relation "namespace_config" does not exist (SQLSTATE 42P01)

This applies to all SQL datastores (postgres, mysql, crdb) and covers read operations, write operations, and statistics queries.

Closes #2749

@ivanauth ivanauth requested a review from a team as a code owner December 16, 2025 01:04
@github-actions github-actions bot added area/datastore Affects the storage system area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Dec 16, 2025
@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 83.43195% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.45%. Comparing base (7a18b39) to head (acd0018).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
internal/datastore/spanner/errors.go 0.00% 18 Missing ⚠️
pkg/middleware/readiness/readiness.go 89.37% 8 Missing and 2 partials ⚠️

❌ Your project check has failed because the head coverage (74.45%) is below the target coverage (75.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2775      +/-   ##
==========================================
+ Coverage   74.40%   74.45%   +0.06%     
==========================================
  Files         484      487       +3     
  Lines       57406    57869     +463     
==========================================
+ Hits        42707    43083     +376     
- Misses      11709    11775      +66     
- Partials     2990     3011      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch from 8ec386b to 126c8ff Compare December 19, 2025 22:47
@ivanauth ivanauth changed the title Improve error messages for missing database tables fix: improve error messages for missing database tables Dec 19, 2025
Copy link
Contributor

@miparnisari miparnisari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used Postman and tried all the APIs against CRDB and Spanner. Notes:

  • we're missing this change for Spanner. You can use docker-compose -f docker-compose.spanner.yaml up --build to test. E.g.
    • call to WriteSchema I get "unable to list caveats: spanner: code = \"NotFound\", desc = \"Table not found: caveat\", requestID = \"1.23ff9b5081a896f8.1.1.70.1\"",.
    • call to DeleteRelationships I get "object definition doc not found". This line has to be updated to account for when the error description says "table"
      if spanner.ErrCode(err) == codes.NotFound {
  • in many cases, the errors returned to the user are prefixed with error encoding zedtoken: unable to read unique ID which makes no sense to me, and probably not to the end user 😆 It comes from
    datastoreUniqueID, err := ds.UniqueID(ctx)
    if err != nil {
    return nil, fmt.Errorf(errEncodeError, err)
    }
    . i'm very tempted to remove that error encoding zedtoken and to clarify what the "unique ID" is
  • in spanner, if you didn't run migrations, calling Watch api gives a HTTP 400. But against CRDB, the watch service is completely disabled: unknown service authzed.api.v1.WatchService, which means that you need to re-boot the server after you ran the migrations. I wonder if the server is supposed to be hable to handle migrations running at the same time ?

Anyway. Could you update

features.Watch.Reason = "Range feeds must be enabled in CockroachDB and the user must have permission to create them in order to enable the Watch API: " + err.Error()
to set a different error message in this case?

  • when you call ImportBulk, you get this
{
    "code": 2,
    "message": "statement description failed: ERROR: relation \"relation_tuple\" does not exist (SQLSTATE 42P01)",
    "details": []
}

, seems like we missed an error handling here:

copied, err := tx.CopyFrom(ctx, pgx.Identifier{tupleTableName}, colNames, adapter)

err,
codes.FailedPrecondition,
spiceerrors.ForReason(
v1.ErrorReason_ERROR_REASON_UNSPECIFIED,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please create the right error code

// instructing the user to run migrations.
func NewSchemaNotInitializedError(underlying error) error {
return SchemaNotInitializedError{
fmt.Errorf("datastore error: the database schema has not been initialized; please run \"spicedb datastore migrate\": %w", underlying),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Errorf("datastore error: the database schema has not been initialized; please run \"spicedb datastore migrate\": %w", underlying),
fmt.Errorf("%w. please run \"spicedb datastore migrate\", underlying),

Comment on lines 57 to 58
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
return nil, datastore.NoRevision, wrappedErr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
return nil, datastore.NoRevision, wrappedErr
if pgxcommon.IsMissingTableError(err) {
err = common.NewSchemaNotInitializedError(err)
}

to keep the same pattern as the lines before

pgReadOnlyTransaction = "25006"
pgQueryCanceled = "57014"
pgInvalidArgument = "22023"
pgMissingTable = "42P01"
Copy link
Contributor

@miparnisari miparnisari Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are two constants with the same value defined elsewhere: postgresMissingTableErrorCode

please remove them in favor of this one

cterr := tx.QueryRow(ctx, sql, args...).Scan(&newXID, &newSnapshot, &timestamp)
if cterr != nil {
if wrappedErr := common.WrapMissingTableError(cterr); wrappedErr != nil {
return newXID, newSnapshot, timestamp, wrappedErr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return newXID, newSnapshot, timestamp, wrappedErr
cterr = ....

})
if err != nil {
if wrappedErr := pgxcommon.WrapMissingTableError(err); wrappedErr != nil {
return nil, wrappedErr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, wrappedErr
err = wrappedErr

otherwise you lose the errUnableToListNamespaces below

@miparnisari
Copy link
Contributor

miparnisari commented Dec 23, 2025

I also wonder if instead of littering all the datastore implementations with error handling, we could just prevent the server from coming up online when you have never ran migrate.

E.g. here

if !dsReady.IsReady {
log.Ctx(ctx).Warn().Bool("datastoreReady", false).Msgf("datastore failed readiness checks: %s", dsReady.Message)
return false
}

if dsReady.message includes datastore is not migrated: currently at revision \"\", then we could tell people to run migrate. we'd need to write a generic test ensuring that all datastores return empty revision when they haven't been migrated.

I'm sure there are more changes required than this, perhaps an update to Server.Complete to include this check, but @josephschorr does this sound reasonable?

ivanauth added a commit to ivanauth/spicedb that referenced this pull request Jan 5, 2026
- Add TODO comment for future ERROR_REASON_DATASTORE_NOT_MIGRATED
- Simplify error message format to: "%w. please run migrate"
- Use IsMissingTableError + reassignment pattern (not early return)
- Export PgMissingTable constant, remove duplicates from migration drivers
- Add Spanner IsMissingTableError support in reader.go and caveat.go
- Differentiate CRDB watch service error message for missing tables
- Add ImportBulk error handling in bulk.go
- Update tests for new error message format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
ivanauth added a commit to ivanauth/spicedb that referenced this pull request Jan 5, 2026
- Add TODO comment for future ERROR_REASON_DATASTORE_NOT_MIGRATED
- Simplify error message format to: "%w. please run migrate"
- Use IsMissingTableError + reassignment pattern (not early return)
- Export PgMissingTable constant, remove duplicates from migration drivers
- Add Spanner IsMissingTableError support in reader.go and caveat.go
- Differentiate CRDB watch service error message for missing tables
- Add ImportBulk error handling in bulk.go
- Update tests for new error message format
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch from ee34e7f to 7c53e96 Compare January 5, 2026 17:59
@ivanauth
Copy link
Contributor Author

ivanauth commented Jan 6, 2026

I replaced scattered IsMissingTableError checks with a single gRPC readiness middleware that blocks ALL requests until the datastore is migrated.

Before: 15 files with scattered IsMissingTableError checks (easy to miss spots)
After: One middleware gates ALL requests until migrated

@github-actions github-actions bot added the area/cli Affects the command line label Jan 7, 2026
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch 2 times, most recently from ab8dd24 to 33b2e94 Compare January 7, 2026 15:38
@github-actions github-actions bot added area/schema Affects the Schema Language area/dependencies Affects dependencies labels Jan 7, 2026
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch from 3d37e2d to d873dd4 Compare January 7, 2026 20:39
@github-actions github-actions bot removed area/schema Affects the Schema Language area/dependencies Affects dependencies labels Jan 7, 2026
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch 2 times, most recently from 131f022 to 7176e82 Compare January 23, 2026 21:15
ivanauth added a commit to ivanauth/spicedb that referenced this pull request Jan 23, 2026
- Add TODO comment for future ERROR_REASON_DATASTORE_NOT_MIGRATED
- Simplify error message format to: "%w. please run migrate"
- Use IsMissingTableError + reassignment pattern (not early return)
- Export PgMissingTable constant, remove duplicates from migration drivers
- Add Spanner IsMissingTableError support in reader.go and caveat.go
- Differentiate CRDB watch service error message for missing tables
- Add ImportBulk error handling in bulk.go
- Update tests for new error message format
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch 2 times, most recently from a03d096 to 10f604e Compare January 24, 2026 00:36
When datastore operations fail due to missing database tables (typically
because migrations haven't been run), users now see a clear error message:
'please ensure you have run spicedb datastore migrate'

This improves the user experience by providing actionable guidance instead
of raw database errors like 'relation X does not exist'.

The improvement covers all reader operations across CRDB, PostgreSQL, and
MySQL datastores including namespace, caveat, counter, and statistics
queries.
- Add TODO comment for future ERROR_REASON_DATASTORE_NOT_MIGRATED
- Simplify error message format to: "%w. please run migrate"
- Use IsMissingTableError + reassignment pattern (not early return)
- Export PgMissingTable constant, remove duplicates from migration drivers
- Add Spanner IsMissingTableError support in reader.go and caveat.go
- Differentiate CRDB watch service error message for missing tables
- Add ImportBulk error handling in bulk.go
- Update tests for new error message format
Replaces scattered IsMissingTableError checks (15 files, 157 lines) with a single
gRPC readiness middleware that blocks ALL requests until the datastore is migrated.

Key improvements:
- Single point of control vs copy-pasted checks everywhere
- Impossible to miss code paths - all gRPC requests gated automatically
- Clear error message: "Please run 'spicedb datastore migrate'"
- Cached checks (500ms) with singleflight to prevent thundering herd
- Health probes bypass the gate for Kubernetes compatibility

Net: -81 lines, better coverage, consistent UX.
@ivanauth ivanauth force-pushed the fix/issue-2749-complete-coverage branch from 10f604e to c3ce28a Compare January 24, 2026 00:38
ivanauth and others added 2 commits January 29, 2026 11:30
Sort imports alphabetically to satisfy gci linter - logmw before readiness.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli Affects the command line area/datastore Affects the storage system area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

update error messages about missing tables in the database to say "run spicedb migrate"

2 participants