Skip to content

Commit 1d50cdc

Browse files
committed
feat: add Aurora DSQL storage backend
Add support for Amazon Aurora DSQL as a storage engine for OpenFGA. - Add DSQL-specific migrations adapted for DSQL constraints (ASYNC index creation, NO TRANSACTION DDL, epoch-based goose table IDs, full indexes instead of partial) - Add OCC retry logic with exponential backoff for write operations - Skip FOR UPDATE clause (DSQL uses OCC at commit time) - Use aurora-dsql-connectors/go/pgx for IAM authentication - Optimize query plans to avoid table scans on DSQL - Respect OPENFGA_DATASTORE_USERNAME for DSQL connections - Add DSQL migration tooling and test fixtures
1 parent 6b6b2c5 commit 1d50cdc

24 files changed

Lines changed: 781 additions & 30 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
Try to keep listed changes to a concise bulleted list of simple explanations of changes. Aim for the amount of information needed so that readers can understand where they would look in the codebase to investigate the changes' implementation, or where they would look in the documentation to understand how to make use of the change in practice - better yet, link directly to the docs and provide detailed information there. Only elaborate if doing so is required to avoid breaking changes or experimental features from ruining someone's day.
88

99
## [Unreleased]
10+
### Added
11+
- Add Amazon Aurora DSQL as a supported storage backend.
12+
1013
### Changed
1114
- HTTP gateway's internal gRPC client now uses dynamic TLS credentials that automatically update on certificate rotation via certwatcher, preventing connection failures when certificates are rotated (e.g., by cert-manager). [#2951](https://github.com/openfga/openfga/pull/2951)
1215

assets/assets.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const (
77
MySQLMigrationDir = "migrations/mysql"
88
PostgresMigrationDir = "migrations/postgres"
99
SqliteMigrationDir = "migrations/sqlite"
10+
DSQLMigrationDir = "migrations/dsql"
1011
)
1112

1213
// EmbedMigrations within the openfga binary.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
-- DSQL schema: uses full indexes (no partial), ASYNC index creation, C collation by default
4+
5+
CREATE TABLE tuple (
6+
store TEXT NOT NULL,
7+
object_type TEXT NOT NULL,
8+
object_id TEXT NOT NULL,
9+
relation TEXT NOT NULL,
10+
_user TEXT NOT NULL,
11+
user_type TEXT NOT NULL,
12+
ulid TEXT NOT NULL,
13+
inserted_at TIMESTAMPTZ NOT NULL,
14+
PRIMARY KEY (store, object_type, object_id, relation, _user)
15+
);
16+
17+
CREATE INDEX ASYNC idx_tuple_user ON tuple (store, object_type, object_id, relation, _user, user_type);
18+
CREATE UNIQUE INDEX ASYNC idx_tuple_ulid ON tuple (ulid);
19+
20+
CREATE TABLE authorization_model (
21+
store TEXT NOT NULL,
22+
authorization_model_id TEXT NOT NULL,
23+
type TEXT NOT NULL,
24+
type_definition BYTEA,
25+
PRIMARY KEY (store, authorization_model_id, type)
26+
);
27+
28+
CREATE TABLE store (
29+
id TEXT PRIMARY KEY,
30+
name TEXT NOT NULL,
31+
created_at TIMESTAMPTZ NOT NULL,
32+
updated_at TIMESTAMPTZ,
33+
deleted_at TIMESTAMPTZ
34+
);
35+
36+
CREATE TABLE assertion (
37+
store TEXT NOT NULL,
38+
authorization_model_id TEXT NOT NULL,
39+
assertions BYTEA,
40+
PRIMARY KEY (store, authorization_model_id)
41+
);
42+
43+
CREATE TABLE changelog (
44+
store TEXT NOT NULL,
45+
object_type TEXT NOT NULL,
46+
object_id TEXT NOT NULL,
47+
relation TEXT NOT NULL,
48+
_user TEXT NOT NULL,
49+
operation INTEGER NOT NULL,
50+
ulid TEXT NOT NULL,
51+
inserted_at TIMESTAMPTZ NOT NULL,
52+
PRIMARY KEY (store, ulid, object_type)
53+
);
54+
55+
-- +goose Down
56+
DROP TABLE IF EXISTS tuple;
57+
DROP TABLE IF EXISTS authorization_model;
58+
DROP TABLE IF EXISTS store;
59+
DROP TABLE IF EXISTS assertion;
60+
DROP TABLE IF EXISTS changelog;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
-- DSQL: ADD COLUMN without DEFAULT, then UPDATE existing rows
4+
ALTER TABLE authorization_model ADD COLUMN schema_version TEXT;
5+
UPDATE authorization_model SET schema_version = '1.0' WHERE schema_version IS NULL;
6+
7+
-- +goose Down
8+
-- +goose NO TRANSACTION
9+
ALTER TABLE authorization_model DROP COLUMN schema_version;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
CREATE INDEX ASYNC idx_reverse_lookup_user ON tuple (store, object_type, relation, _user);
4+
5+
-- +goose Down
6+
-- +goose NO TRANSACTION
7+
DROP INDEX IF EXISTS idx_reverse_lookup_user;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
ALTER TABLE authorization_model ADD COLUMN serialized_protobuf BYTEA;
4+
5+
-- +goose Down
6+
-- +goose NO TRANSACTION
7+
ALTER TABLE authorization_model DROP COLUMN serialized_protobuf;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
-- DSQL: separate ALTER statements (one DDL per transaction)
4+
ALTER TABLE tuple ADD COLUMN condition_name TEXT;
5+
ALTER TABLE tuple ADD COLUMN condition_context BYTEA;
6+
ALTER TABLE changelog ADD COLUMN condition_name TEXT;
7+
ALTER TABLE changelog ADD COLUMN condition_context BYTEA;
8+
9+
-- +goose Down
10+
-- +goose NO TRANSACTION
11+
ALTER TABLE tuple DROP COLUMN condition_name;
12+
ALTER TABLE tuple DROP COLUMN condition_context;
13+
ALTER TABLE changelog DROP COLUMN condition_name;
14+
ALTER TABLE changelog DROP COLUMN condition_context;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- +goose Up
2+
-- +goose NO TRANSACTION
3+
-- DSQL: uses C collation by default, ASYNC instead of CONCURRENTLY
4+
CREATE INDEX ASYNC idx_user_lookup ON tuple (
5+
store,
6+
_user,
7+
relation,
8+
object_type,
9+
object_id
10+
);
11+
12+
DROP INDEX IF EXISTS idx_reverse_lookup_user;
13+
14+
-- +goose Down
15+
-- +goose NO TRANSACTION
16+
DROP INDEX IF EXISTS idx_user_lookup;
17+
CREATE INDEX ASYNC idx_reverse_lookup_user ON tuple (store, object_type, relation, _user);

assets/migrations/dsql/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# DSQL Migrations for OpenFGA
2+
3+
These migrations are adapted from the PostgreSQL migrations for Aurora DSQL compatibility.
4+
5+
## Key Differences from PostgreSQL Migrations
6+
7+
1. **ASYNC Indexes**: All `CREATE INDEX` statements use `CREATE INDEX ASYNC` (DSQL requirement)
8+
2. **NO TRANSACTION**: All migrations use `-- +goose NO TRANSACTION` since DSQL DDL is always non-transactional
9+
3. **No SERIAL/IDENTITY**: The goose version table uses epoch microseconds instead of auto-increment
10+
4. **No Partial Indexes**: DSQL doesn't support partial indexes, so full indexes are used
11+
12+
## Migration Files
13+
14+
| File | Description |
15+
|------|-------------|
16+
| 001_initialize_schema.sql | Creates core tables (tuple, authorization_model, store, assertion, changelog) |
17+
| 002_add_authorization_model_version.sql | Adds schema_version column |
18+
| 003_add_reverse_lookup_index.sql | Adds reverse lookup index |
19+
| 004_add_authorization_model_serialized_protobuf.sql | Adds serialized_protobuf column |
20+
| 005_add_conditions_to_tuples.sql | Adds condition columns to tuple and changelog |
21+
| 006_add_collate_index.sql | Adds user lookup index with C collation |
22+
23+
## Future Consideration: Splitting Migrations
24+
25+
Per DSQL best practices, each migration should ideally contain only one DDL statement. Currently, migrations 002 and 005 contain multiple statements:
26+
27+
- **002**: ALTER TABLE + UPDATE (mixed DDL and DML)
28+
- **005**: Four ALTER TABLE statements
29+
30+
While these work correctly with `-- +goose NO TRANSACTION` (goose executes each statement separately), consider splitting them into individual migration files if you encounter OCC errors during migration. For example:
31+
32+
```
33+
005a_add_condition_name_to_tuple.sql
34+
005b_add_condition_context_to_tuple.sql
35+
005c_add_condition_name_to_changelog.sql
36+
005d_add_condition_context_to_changelog.sql
37+
```
38+
39+
See [DSQL Development Guide](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) for more information on DDL best practices.

cmd/run/run.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,13 @@ func (s *ServerContext) datastoreConfig(config *serverconfig.Config) (storage.Op
503503
if err != nil {
504504
return nil, nil, fmt.Errorf("initialize sqlite datastore: %w", err)
505505
}
506+
case "dsql":
507+
// Aurora DSQL uses PostgreSQL wire protocol with IAM authentication
508+
// The postgres.New function handles dsql:// URIs automatically
509+
datastore, err = postgres.New(config.Datastore.URI, dsCfg)
510+
if err != nil {
511+
return nil, nil, fmt.Errorf("initialize dsql datastore: %w", err)
512+
}
506513
default:
507514
return nil, nil, fmt.Errorf("storage engine '%s' is unsupported", config.Datastore.Engine)
508515
}

0 commit comments

Comments
 (0)