Skip to content

Commit 6e74892

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 6e74892

24 files changed

Lines changed: 773 additions & 30 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ Try to keep listed changes to a concise bulleted list of simple explanations of
4545

4646
### Changed
4747
- Datastore throttling separated from dispatch throttling in BatchCheck, ListUsers metadata. Also, `throttling_type` label added to `throttledRequestCounter` metric to differentiate between dispatch/datastore throttling. [#2839](https://github.com/openfga/openfga/pull/2839)
48+
- Update Aurora DSQL connector to use the new official monorepo location (`github.com/awslabs/aurora-dsql-connectors/go/pgx`). [#15](https://github.com/amaksimo/openfga-dsql-alemaksi/pull/15)
4849

4950
### Removed
5051
- Removed custom grpc_prometheus fork, replace with go-grpc-middleware's provider. Removes the custom `grpc_code` label on this metric. [#2855](https://github.com/openfga/openfga/pull/2855)
5152

5253
### Fixed
54+
- DSQL connections now respect `OPENFGA_DATASTORE_USERNAME` for IAM token generation. [#12](https://github.com/amaksimo/openfga-dsql-alemaksi/pull/12)
5355
- ListUsers will now properly get datastore throttled if enabled. [#2846](https://github.com/openfga/openfga/pull/2846)
5456
- Cache controller now uses the logger provided to the server instead of always using a no-op logger. [#2847](https://github.com/openfga/openfga/pull/2847)
5557
- Typesystem invalidate model with empty intersection and union. [#2865](https://github.com/openfga/openfga/pull/2865)

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)