Skip to content

Conversation

@josephschorr
Copy link
Member

The PostgresFDW allows SpiceDB to be used from within Postgres via the Postgres Foreign Data Wrapper protocol, as-if Postgres was speaking to another Postgres.

It supports querying (Check, LookupResources, LookupSubjects), as well as reads (RealRelationships), writes (WriteRelationships) and schema updates (ReadSchema/WriteSchema).

These operations are performed against "virtual" tables (permissions, relationships and schema) that are placed into the Postgres and translated by this proxy.

For more information, see the README.md

This is currently experimental and subject to changes

@josephschorr josephschorr requested a review from a team as a code owner January 7, 2026 23:40
@github-actions github-actions bot added area/cli Affects the command line area/dependencies Affects dependencies area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Jan 7, 2026
@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 36.27628% with 1061 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.53%. Comparing base (3e906b6) to head (1d80dac).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
internal/fdw/tables/permissions.go 0.00% 308 Missing ⚠️
internal/fdw/tables/relationships.go 25.68% 161 Missing and 4 partials ⚠️
internal/fdw/pgserver.go 0.00% 100 Missing ⚠️
internal/fdw/tables/matcher.go 61.04% 61 Missing and 29 partials ⚠️
internal/fdw/tables/select.go 56.29% 61 Missing and 12 partials ⚠️
internal/fdw/cursor.go 0.00% 53 Missing ⚠️
pkg/cmd/postgresfdw.go 49.50% 45 Missing and 5 partials ⚠️
internal/fdw/session.go 0.00% 37 Missing ⚠️
internal/fdw/tables/schema.go 0.00% 37 Missing ⚠️
internal/fdw/transaction.go 0.00% 28 Missing ⚠️
... and 9 more

❌ Your project status has failed because the head coverage (74.53%) 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    #2806      +/-   ##
==========================================
- Coverage   75.78%   74.53%   -1.24%     
==========================================
  Files         460      483      +23     
  Lines       55026    56691    +1665     
==========================================
+ Hits        41694    42251     +557     
- Misses      10456    11490    +1034     
- Partials     2876     2950      +74     

☔ 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.

docs/spicedb.md Outdated
- [spicedb datastore](#reference-spicedb-datastore) - datastore operations
- [spicedb lsp](#reference-spicedb-lsp) - serve language server protocol
- [spicedb man](#reference-spicedb-man) - Generate man page
- [spicedb postgres-fdw](#reference-spicedb-postgres-fdw) - serve a Postgres Foreign Data Wrapper for SpiceDB
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
- [spicedb postgres-fdw](#reference-spicedb-postgres-fdw) - serve a Postgres Foreign Data Wrapper for SpiceDB
- [spicedb postgres-fdw](#reference-spicedb-postgres-fdw) - serve a Postgres Foreign Data Wrapper for SpiceDB

whitespace change

Serves a Postgres-compatible interface for querying SpiceDB data using foreign data wrappers

```
spicedb postgres-fdw [flags]
Copy link
Contributor

Choose a reason for hiding this comment

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

General question: does this mean the postgres-fdw is now a part of the SpiceDB binary? When do we start to get concerned about the size of the binary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes and the plan is any SpiceDB-adjacent systems will be as well.

I think, for now, we won't be worrying about it

Comment on lines 45 to 46
// Verify error is properly wrapped with psql codes
require.Error(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see these assertions

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 83 to 84
// Verify error is properly wrapped with psql codes
require.Error(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 121 to 122
// Verify error is properly wrapped with psql codes
require.Error(t, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 42 to 48

// slog.SetLogLoggerLevel(slog.LevelDebug)
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
// slog.SetLogLoggerLevel(slog.LevelDebug)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added comment

Comment on lines +121 to +137
SELECT resource_type, resource_id, relation, subject_type, subject_id
FROM relationships
WHERE resource_type = 'document'
AND resource_id = 'readme';
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is a "now" issue, but is there a way that we can help ensure that we don't have index misses on ReadRels?

Copy link
Member Author

Choose a reason for hiding this comment

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

No; We do not have full coverage on all shapes of read rels that are possible (and likely never will). The best we could do is warn, but that's unrelated to these changes

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. My concern is that this implementation detail is kinda hidden, and that it might be relatively easy to write a postgres query that accidentally misses the indices and then it leaves the user wondering why SpiceDB is slow. I'm okay with saying that's a future problem, though.

Comment on lines +129 to +146
```sql
-- Add a new relationship
INSERT INTO relationships (resource_type, resource_id, relation, subject_type, subject_id)
VALUES ('document', 'readme', 'viewer', 'user', 'alice');
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I wanna make sure we clarify in external documentation if I've got it right: the transaction support here doesn't sidestep the dual-write problem, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, because postgres_fdw sadly does not support two-phase commit


// Run starts the Postgres wire protocol server on the specified endpoint.
// It blocks until the context is cancelled or an error occurs.
func (p *PgBackend) Run(ctx context.Context, endpoint string) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't have to be in this PR, but I'd wanna see observability and metrics for this as a fast follow

Copy link
Member Author

Choose a reason for hiding this comment

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

okay

serverReady := make(chan bool)
go (func() {
serverReady <- true
_ = runnableServer.Run(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

can use assert.NoError here if desired

Copy link
Member Author

@josephschorr josephschorr Jan 13, 2026

Choose a reason for hiding this comment

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

Nah; I don't like error checking after cleanup in tests. It likely is returning an error (context canceled) when its forcibly shutdown at the end of the time

tstirrat15
tstirrat15 previously approved these changes Jan 12, 2026
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comments; otherwise this is looking good to me for the first implementation.


// Verify server is ready
retryCount := 0
for retryCount < 10 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +38 to +43
prevEnv := os.Environ()
restore := func() {
restoreEnv(prevEnv)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary with t.Setenv?


var trailerMD metadata.MD

checkResult, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the caveat context also a todo here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it has TODOs saying so

ObjectType: valuesByColumnName["subject_type"],
ObjectId: valuesByColumnName["subject_id"],
},
OptionalRelation: valuesByColumnName["optional_subject_relation"],
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean that the optional subject relation has to be defined, or is the assumption that it'll typically come through as an empty string and be handled as an unset value?

Copy link
Member Author

Choose a reason for hiding this comment

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

Empty string

return nil
}

type valueOrRef struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Docstring on this one? Also maybe pull it towards the top or into a separate file.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why? Its used internally

Copy link
Contributor

Choose a reason for hiding this comment

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

Just cuz it's used widely enough in this package. I ended up inferring it from usage as I was reading and then finding it in one of the last files I looked at and thinking that it would have been nicer to have it up front. I guess that's a review-time concern, though, so it's probably fine.

t.Parallel()

require.Equal(t, tc.expectedName, tc.stmt.TableName())
require.Equal(t, tc.stmt.isUnsupported, tc.stmt.IsUnsupported())
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this asserting? Is it functionally asserting that the interface is satisfied?

Copy link
Member Author

Choose a reason for hiding this comment

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

Its just making sure the accessors return what was defined

tstirrat15
tstirrat15 previously approved these changes Jan 13, 2026
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comments, otherwise LGTM

Comment on lines +694 to +697
// Give PGServer time to start
time.Sleep(50 * time.Millisecond)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a condition that we can explicitly poll here? I don't want this to become a flake source.

tstirrat15
tstirrat15 previously approved these changes Jan 13, 2026
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

LGTM

@josephschorr josephschorr force-pushed the postgres-fdw branch 3 times, most recently from c53bb4b to c3fef24 Compare January 13, 2026 21:32
@josephschorr josephschorr marked this pull request as draft January 13, 2026 22:02
@josephschorr josephschorr marked this pull request as ready for review January 14, 2026 18:12
@josephschorr josephschorr force-pushed the postgres-fdw branch 2 times, most recently from 6ddcc43 to 17d5534 Compare January 14, 2026 19:05
Copy link
Contributor

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

LGTM. I'm curious to see what the ramifications of CGO are.

The PostgresFDW allows SpiceDB to be used from within Postgres via the Postgres Foreign Data Wrapper protocol, as-if Postgres was speaking to another Postgres.

It supports querying (Check, LookupResources, LookupSubjects), as well as reads (RealRelationships), writes (WriteRelationships) and schema updates (ReadSchema/WriteSchema).

These operations are performed against "virtual" tables (`permissions`, `relationships` and `schema`) that are placed into the Postgres and translated by this proxy.

For more information, see the README.md

This is currently experimental and subject to changes
@josephschorr josephschorr added this pull request to the merge queue Jan 16, 2026
Merged via the queue into authzed:main with commit fd4b204 Jan 16, 2026
43 of 45 checks passed
@josephschorr josephschorr deleted the postgres-fdw branch January 16, 2026 23:38
@github-actions github-actions bot locked and limited conversation to collaborators Jan 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area/cli Affects the command line area/dependencies Affects dependencies 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.

2 participants