diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 83341dd5..b0c971b0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -191,7 +191,7 @@ "serverless", "postgresql" ], - "version": "1.9.1" + "version": "1.10.0" }, { "category": "deployment", diff --git a/plugins/databases-on-aws/.claude-plugin/plugin.json b/plugins/databases-on-aws/.claude-plugin/plugin.json index b5a42d2e..7d947740 100644 --- a/plugins/databases-on-aws/.claude-plugin/plugin.json +++ b/plugins/databases-on-aws/.claude-plugin/plugin.json @@ -22,5 +22,5 @@ "license": "Apache-2.0", "name": "databases-on-aws", "repository": "https://github.com/awslabs/agent-plugins", - "version": "1.9.1" + "version": "1.10.0" } diff --git a/plugins/databases-on-aws/.codex-plugin/plugin.json b/plugins/databases-on-aws/.codex-plugin/plugin.json index 9c1b8b19..b5e89305 100644 --- a/plugins/databases-on-aws/.codex-plugin/plugin.json +++ b/plugins/databases-on-aws/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "databases-on-aws", - "version": "1.9.1", + "version": "1.10.0", "description": "Expert database guidance for the AWS database portfolio. Design schemas, execute queries, handle migrations, and choose the right database for your workload.", "author": { "name": "Amazon Web Services", diff --git a/plugins/databases-on-aws/skills/dsql/SKILL.md b/plugins/databases-on-aws/skills/dsql/SKILL.md index e9a4693d..6feea7fa 100644 --- a/plugins/databases-on-aws/skills/dsql/SKILL.md +++ b/plugins/databases-on-aws/skills/dsql/SKILL.md @@ -1,6 +1,6 @@ --- name: dsql -description: "Build with Aurora DSQL — manage schemas, execute queries, handle migrations, diagnose query plans, diagnose cluster performance, load data, and develop applications with a serverless, distributed SQL database. Covers IAM auth, multi-tenant patterns, MySQL-to-DSQL and PostgreSQL-to-DSQL schema conversion, foreign key constraints, OCC retry patterns, ORM migration (Django/EF Core/Hibernate/Rails/SQLAlchemy), DDL operations, query plan explainability, system diagnostics via CloudWatch AAS, SQL compatibility validation, and bulk data loading. Triggers on phrases like: DSQL, Aurora DSQL, distributed SQL database, serverless PostgreSQL-compatible database, migrate to DSQL, DSQL query plan, DSQL EXPLAIN ANALYZE, DSQL ENUM, DSQL foreign key, DSQL OCC retry, DSQL multi-region, DSQL JSONB, DSQL GIN index, load into DSQL, load CSV into DSQL, bulk load DSQL, aurora-dsql-loader, DSQL slow, DSQL performance, DSQL wait events, DSQL AAS." +description: "Build with Aurora DSQL — manage schemas, execute queries, handle migrations, diagnose query plans, diagnose cluster performance, load data, and develop applications with a serverless, distributed SQL database. Covers IAM auth, multi-tenant patterns, MySQL-to-DSQL and PostgreSQL-to-DSQL schema conversion, foreign key constraints, OCC retry patterns, ORM migration (Django/EF Core/Hibernate/Rails/SQLAlchemy), DDL operations, query plan explainability, system diagnostics via CloudWatch AAS, SQL compatibility validation, and bulk data loading. Load before answering DSQL limit or error-code questions; it has limits docs omit. Triggers on phrases like: DSQL, Aurora DSQL, distributed SQL database, serverless PostgreSQL-compatible database, migrate to DSQL, DSQL query plan, DSQL EXPLAIN ANALYZE, DSQL ENUM, DSQL foreign key, DSQL OCC retry, DSQL multi-region, DSQL JSONB, DSQL GIN index, load into DSQL, load CSV into DSQL, bulk load DSQL, aurora-dsql-loader, DSQL slow, DSQL performance, DSQL wait events, DSQL AAS." license: Apache-2.0 metadata: tags: aws, aurora, dsql, distributed-sql, distributed, distributed-database, database, serverless, serverless-database, postgresql, postgres, sql, schema, migration, multi-tenant, iam-auth, aurora-dsql, mcp, orm, enum, foreign-key, occ-retry, django, ef-core, dotnet, csharp, hibernate, rails, multi-region, schema-conversion, type-mapping, data-loading, system-diagnostics, wait-events, aas, performance, cloudwatch @@ -18,16 +18,17 @@ Load these files as needed for detailed guidance: ### Core: -| Reference | When to Load | Contains | -| --------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| [development-guide.md](references/development-guide.md) | ALWAYS before schema changes or DB operations | Best practices, DDL rules, transaction limits, foreign key constraints | -| [foreign-keys.md](references/foreign-keys.md) | MUST load for foreign key operations or migrations | FK syntax, actions, validation, tenant keys | -| [language.md](references/language.md) | MUST load for language-specific choices | Driver selection, DSQL Connectors, connection code | -| [access-control.md](references/access-control.md) | MUST load for roles, grants, or sensitive data | Scoped role setup, IAM-to-database role mapping | -| [troubleshooting.md](references/troubleshooting.md) | SHOULD load for errors or unexpected behavior | OCC and `23503` errors, FK validation, connection failures, cluster state, DDL rejection | -| [dsql-examples.md](references/dsql-examples.md) | Load for implementation examples | Multi-tenant access, batch operations, identity and sequences, connection pooling | -| [onboarding.md](references/onboarding.md) | User requests "Get started with DSQL" | Interactive step-by-step guide | -| [occ-retry-patterns.md](references/occ-retry-patterns.md) | MUST load for OCC retry code or conflict mitigation | Connectors, `40001` retry, non-retryable `23503`, FK read conflicts, idempotent design | +| Reference | When to Load | Contains | +| ----------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| [development-guide.md](references/development-guide.md) | ALWAYS before schema changes or DB operations | Best practices, DDL rules, transaction limits, foreign key constraints | +| [foreign-keys.md](references/foreign-keys.md) | MUST load for foreign key operations or migrations | FK syntax, actions, validation, tenant keys | +| [language.md](references/language.md) | MUST load for language-specific choices | Driver selection, DSQL Connectors, connection code | +| [access-control.md](references/access-control.md) | MUST load for roles, grants, or sensitive data | Scoped role setup, IAM-to-database role mapping | +| [troubleshooting.md](references/troubleshooting.md) | SHOULD load for errors or unexpected behavior | OCC and `23503`, FK, connections, cluster state | +| [limits-and-error-codes.md](references/limits-and-error-codes.md) | SHOULD load before sizing batches, keys or indexes, or on `54000`/`0A000`/`54011` | Every limit, rejection and quota with SQLSTATE and exact message | +| [dsql-examples.md](references/dsql-examples.md) | Load for implementation examples | Multi-tenant access, batch operations, identity and sequences, connection pooling | +| [onboarding.md](references/onboarding.md) | User requests "Get started with DSQL" | Interactive step-by-step guide | +| [occ-retry-patterns.md](references/occ-retry-patterns.md) | MUST load for OCC retry code or conflict mitigation | Connectors, `40001` retry, non-retryable `23503`, FK read conflicts, idempotent design | ### MCP: @@ -56,12 +57,12 @@ Load these files as needed for detailed guidance: ### PostgreSQL Migrations: -| Reference | When to Load | Contains | -| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| [pg-migrations/type-mapping.md](references/pg-migrations/type-mapping.md) | MUST load for DSQL NUMERIC or PG type questions | C collation rules, NUMERIC(p,s), JSON/JSONB | -| [pg-migrations/index-conversion.md](references/pg-migrations/index-conversion.md) | MUST load for any index conversion or index support question | GIN/GiST/BRIN → btree, operator class removal, partial + expression indexes | -| [pg-migrations/schema-objects.md](references/pg-migrations/schema-objects.md) | MUST load for ENUM, materialized views, extensions, multi-schema, CREATE FUNCTION | ENUM → CHECK, views, role/IAM mapping, LANGUAGE sql functions | -| [pg-migrations/multi-region.md](references/pg-migrations/multi-region.md) | Multi-region, active-active, or HA questions | Architecture, geographic partitioning | +| Reference | When to Load | Contains | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| [pg-migrations/type-mapping.md](references/pg-migrations/type-mapping.md) | MUST load for DSQL NUMERIC or PG type questions | C collation rules, NUMERIC(p,s), JSON/JSONB | +| [pg-migrations/index-conversion.md](references/pg-migrations/index-conversion.md) | MUST load for any index conversion or index support question | GIN/GiST/BRIN → btree, operator class removal, partial + expression indexes, sort order | +| [pg-migrations/schema-objects.md](references/pg-migrations/schema-objects.md) | MUST load for ENUM, materialized views, extensions, multi-schema, CREATE FUNCTION | ENUM → CHECK, views, role/IAM mapping, LANGUAGE sql functions | +| [pg-migrations/multi-region.md](references/pg-migrations/multi-region.md) | Multi-region, active-active, or HA questions | Architecture, geographic partitioning | ### ORM Guides: @@ -153,12 +154,12 @@ defaults that may change — when a user's decision depends on an exact limit, v | Max connections per cluster | 10,000 | `aurora dsql connection limits` | | Auth token expiry | 15 minutes | `aurora dsql authentication token` | | Max connection duration | 60 minutes | `aurora dsql connection limits` | -| Max indexes per table | 24 | `aurora dsql index limits` | +| Max indexes per table | 24 incl. PK | `aurora dsql index limits` | | Max columns per index | 8 | `aurora dsql index limits` | | IDENTITY/SEQUENCE CACHE values | 1 or >= 65536 | `aurora dsql sequence cache` | | Supported column data types | See docs | `aurora dsql supported data types` | -**When to verify:** Before recommending batch sizes, connection pool settings, or schema designs where hitting a limit would cause failures; any time the exact number can affect user decision. +**When to verify:** Before recommending batch sizes, connection pool settings, or schema designs where hitting a limit would cause failures; any time the exact number can affect user decision. For the full limit set with each SQLSTATE and its exact error message, see [limits-and-error-codes.md](references/limits-and-error-codes.md); where it marks a value observed and the docs differ, use the observed value. **Fallback:** If `awsknowledge` is unavailable, use the defaults above and flag that limits should be verified against [DSQL documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/). @@ -287,10 +288,9 @@ MUST load [system-diagnostics/workflow.md](references/system-diagnostics/workflo - **`awsknowledge` returns no results:** Use the default limits in the table above and note that limits should be verified against [DSQL documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/). - **`dsql_lint` unavailable or timing out:** See the Error Handling section of [dsql-lint.md](references/dsql-lint.md). Do not silently skip validation — inform the user and require explicit confirmation before proceeding with manual rules from [development-guide.md](references/development-guide.md). -- **OCC serialization error:** Retry the transaction. If persistent, check for hot-key contention — see [troubleshooting.md](references/troubleshooting.md). -- **Foreign key violation (`23503`):** Correct the relationship or referential action; **MUST NOT** - send it through the `40001` retry loop — see [troubleshooting.md](references/troubleshooting.md). -- **Transaction exceeds limits:** Split write batches to stay under 3,000 row modifications and 10 MiB; locked-row primary keys count toward the size limit — see [batched-migration.md](references/ddl-migrations/batched-migration.md). +- **OCC serialization error (`40001`):** Retry with backoff, and keep `(OC001)` in the loop too — see [occ-retry-patterns.md](references/occ-retry-patterns.md). +- **Foreign key violation (`23503`):** Correct the relationship or referential action; **MUST NOT** send it through the `40001` retry loop — see [troubleshooting.md](references/troubleshooting.md). +- **Transaction exceeds limits:** Split batches under 3,000 rows and 10 MiB; size is checked at `COMMIT` — see [limits-and-error-codes.md](references/limits-and-error-codes.md#exceeded-limits) to size them and [batched-migration.md](references/ddl-migrations/batched-migration.md) for the pattern. - **Token expiration mid-operation:** Generate a fresh IAM token — see [authentication-guide.md](references/auth/authentication-guide.md). See [troubleshooting.md](references/troubleshooting.md) for other issues. ## Additional Resources diff --git a/plugins/databases-on-aws/skills/dsql/references/auth/authentication-guide.md b/plugins/databases-on-aws/skills/dsql/references/auth/authentication-guide.md index 62aa9e43..7cbb7864 100644 --- a/plugins/databases-on-aws/skills/dsql/references/auth/authentication-guide.md +++ b/plugins/databases-on-aws/skills/dsql/references/auth/authentication-guide.md @@ -45,7 +45,7 @@ Part of [DSQL Development Guide](../development-guide.md). **Best practices:** - Keep authentication tokens in memory only; discard after use -- Regenerate token on connection errors +- Regenerate the token only on a `Signature expired` hint; otherwise a fresh token will not help — see [troubleshooting.md](../troubleshooting.md#connection-and-authorization) - Monitor token generation failures - Set connection timeouts appropriately diff --git a/plugins/databases-on-aws/skills/dsql/references/development-guide.md b/plugins/databases-on-aws/skills/dsql/references/development-guide.md index 48885d67..87bc7309 100644 --- a/plugins/databases-on-aws/skills/dsql/references/development-guide.md +++ b/plugins/databases-on-aws/skills/dsql/references/development-guide.md @@ -72,7 +72,7 @@ effortless scaling, multi-region viability, among other advantages. - REQUIRED: **at most one DDL statement** per operation - ALWAYS separate schema (DDL) and data (DML) changes - MUST use **`CREATE INDEX ASYNC`:** No synchronous creation (verify limits via `awsknowledge`: `aurora dsql index limits`) - - MAXIMUM: **24 indexes per table** + - MAXIMUM: **24 indexes per table**, including the primary key ([details](limits-and-error-codes.md#exceeded-limits)) - MAXIMUM: **8 columns per index** - **MUST** verify index is ready before relying on it: `SELECT indisvalid FROM pg_index WHERE indexrelid = 'index_name'::regclass` — queries work but skip the index until `indisvalid = true` - MUST use **`ALTER TABLE ASYNC ... VALIDATE CONSTRAINT`** for constraint validation: No synchronous validation diff --git a/plugins/databases-on-aws/skills/dsql/references/dsql-lint.md b/plugins/databases-on-aws/skills/dsql/references/dsql-lint.md index 66ce9321..afdc423e 100644 --- a/plugins/databases-on-aws/skills/dsql/references/dsql-lint.md +++ b/plugins/databases-on-aws/skills/dsql/references/dsql-lint.md @@ -43,13 +43,22 @@ Concrete example (from `dsql_lint(sql="CREATE INDEX idx ON t (c);", fix=true)`): **Schema notes:** -- `rule` is a snake_case string identifying the rule (e.g., `index_async`, `truncate`, `json_type`, `set_transaction`); `line` is 1-indexed. +- `rule` is a snake_case string identifying the rule (e.g., `index_async`, `index_sort_direction`, `truncate`, `json_type`, `set_transaction`); `line` is 1-indexed. - `fix_result.status` is one of three values: `fixed`, `fixed_with_warning`, or `unfixable`. Always check this field — `fix_result` is present for every diagnostic when `fix=true`. - `fix_result.detail` is present for `fixed` and `fixed_with_warning`; absent for `unfixable`. - `fixed_sql` contains rewritten SQL when the linter produces a rewrite. Do not assume it is present when no rewrite is needed. Presence of `fixed_sql` does NOT mean the SQL is safe to execute — check every diagnostic first. - `summary.errors` counts `unfixable` diagnostics; `summary.warnings` counts `fixed_with_warning`; `summary.fixed` counts `fixed`. +- **Do not gate a pipeline on any one counter.** Severity is derived from `fix_result.status`, so the + same rule can land in different counters depending on the input: `index_sort_direction` reports + `fixed_with_warning` for `DESC` but a silent `fixed` for `ASC`, even though DSQL rejects both. A + check on `errors == 0` or `warnings == 0` passes SQL that was rewritten underneath it. Check + `len(diagnostics)` instead — but zero means no lint rule fired, not that DSQL accepts the SQL: + operator classes pass with no diagnostic and are rejected + ([index-conversion.md](pg-migrations/index-conversion.md)), while valid `BEGIN ATOMIC` bodies + raise a `parse_error` (see [Error Handling](#error-handling)). Do not compare `fixed_sql` to + the input; it can match while an `unfixable` diagnostic is present. - `statement_preview` is the linter's pointer to the offending statement — useful when presenting diagnostics to the user. --- diff --git a/plugins/databases-on-aws/skills/dsql/references/examples/schema.md b/plugins/databases-on-aws/skills/dsql/references/examples/schema.md index 777fdd20..15643158 100644 --- a/plugins/databases-on-aws/skills/dsql/references/examples/schema.md +++ b/plugins/databases-on-aws/skills/dsql/references/examples/schema.md @@ -32,7 +32,7 @@ Both `JSONB` and `JSON` are valid; pick by access pattern (see Schema Design Rul ## Schema Design: Index Creation -MUST use `CREATE INDEX ASYNC` (max 24 indexes/table, 8 columns/index — verify via `awsknowledge`: `aurora dsql index limits`). Source: [aurora-dsql-samples/java/liquibase](https://github.com/aws-samples/aurora-dsql-samples/tree/main/java/liquibase) +MUST use `CREATE INDEX ASYNC` (max 24 indexes/table including the primary key, 8 columns/index — verify via `awsknowledge`: `aurora dsql index limits`). Source: [aurora-dsql-samples/java/liquibase](https://github.com/aws-samples/aurora-dsql-samples/tree/main/java/liquibase) ```sql CREATE INDEX ASYNC idx_owner_city ON owner(city); diff --git a/plugins/databases-on-aws/skills/dsql/references/limits-and-error-codes.md b/plugins/databases-on-aws/skills/dsql/references/limits-and-error-codes.md new file mode 100644 index 00000000..85b80311 --- /dev/null +++ b/plugins/databases-on-aws/skills/dsql/references/limits-and-error-codes.md @@ -0,0 +1,180 @@ +# Limits and Error Codes + +Every DSQL limit, rejection and cluster quota with its SQLSTATE and exact message. Load it before +sizing batches, keys or index sets, and to identify a `54000`, `0A000` or `54011` error. Other +errors are in [troubleshooting.md](troubleshooting.md). + +**Match on the message rather than the code.** `54000` and `0A000` each cover +more than a dozen of the rows below, so the code alone does not identify which row you are on. This +is a lookup rule for these tables, not a retry rule — classification elsewhere in the skill is +SQLSTATE-based, and `40001` remains the only retryable code (see +[occ-retry-patterns.md](occ-retry-patterns.md)). + +**MUST** match a stable prefix, not the whole string, and match it case-insensitively — case varies +even within one family (`Datatype limit …` for `char`/`varchar`, `datatype limit …` for `text`). +``, `` and `...` in the Message column are values DSQL interpolates at run time, so an +equality test against the cell never matches. If no message matches, **SHOULD** fall back to the +SQLSTATE class and verify the current limit via `awsknowledge` — message text is not a stable +contract. + +Values marked **observed** were measured on a single-Region cluster (PostgreSQL 16 +compatibility) rather than taken from the documentation. Where an observed value contradicts the +[quotas documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/CHAP_quotas.html), +the row says so. **SHOULD** size against the observed value, which is what DSQL enforced when +measured, and tell the user the documented value differs. To re-check one before a design decision +depends on it, test it on a cluster; the quotas page is what it already contradicts. + +Run `dsql_lint` to catch unsupported _features_ before execution; use this section for the +_limits_, which are only observable at run time. + +## Exceeded Limits + +| Limit | Value | SQLSTATE | Message | +| ----------------------------------- | ----------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | +| Row modifications per transaction | 3,000 | `54000` | `transaction row limit exceeded` | +| Transaction size | 10 MiB, incl. per-row and per-index overhead (see below) | `54000` | `transaction size limit 10mb exceeded`, with `DETAIL: Current transaction size mb > 10mb` | +| Transaction age | 5 minutes | `54000` | `transaction age limit of 300s exceeded`, with `DETAIL: Current transaction age s exceeds 300s` | +| Query temp space | 128 MiB; docs give `53200` and other text | `54000` | `query requires more than allowed temp space of 131072 KB to execute` | +| Extended statistics per table | 5 | `54000` | `more than 5 extended statistics per table are not allowed` | +| Schemas per database | 10, excluding `public` and `sys` | `54000` | `more than 10 schemas not allowed` | +| Indexes per table | 24, counting every index — see below | `54000` | `more than 24 indexes per table are not allowed` | +| Primary key or index key size | ~1,981 bytes observed; docs say 1 KiB | `54000` | `key size too large` | +| Declared key size | 2,000 bytes, rejected at DDL time | `54000` | `key size greater than 2000 bytes not supported` | +| Row size | 2 MiB | `54000` | `maximum row size exceeded` | +| Non-index column size | 1 MiB; docs give the message below, a per-datatype one was seen | `54000` | docs: `maximum column size exceeded`; observed: `datatype limit greater than 1048576 bytes not supported for ` | +| View definition size | 131–524 KiB of SQL text observed, varies by shape; docs say 2 MiB | `54000` | `view definition too large` | +| `char` declared size | 4,096 bytes, rejected at DDL time | `54000` | `Datatype limit greater than 4096 bytes not supported for char` | +| `varchar` declared size | 65,535 bytes, rejected at DDL time | `54000` | `Datatype limit greater than 65535 bytes not supported for varchar` | +| `text` value | 1 MiB stored, rejected at write time | `54000` | `datatype limit greater than 1048576 bytes not supported for text` | +| `bytea` value | 1 MiB stored, rejected at write time | `54000` | `datatype limit greater than 1048576 bytes not supported for bytea` | +| `json` / `jsonb` value | 1 MiB stored, rejected at write time | `54000` | `datatype limit greater than 1048576 bytes not supported for jsonb` | +| Columns per index or primary key | 8 | `54011` | `more than 8 column keys in an index are not supported` | +| Columns per table, no primary key | 256 accepted, 257 rejected; docs and message both say 255 | `54011` | `tables can have at most 255 columns` | +| Columns per table, with primary key | 256 accepted, 257 rejected; message names an _index_ | `54011` | `cannot use more than 256 columns in an index` | +| Extended-statistics target | 100 | `22023` | `statistics target exceeds maximum allowed value of 100` | +| `numeric` precision | 1,000 | `22023` | `NUMERIC precision must be between 1 and 1000` | + +Several of these behave in ways the limit alone does not convey: + +- **The per-value 1 MiB limit is a hard bound on high-entropy data.** For random `text`, `base64`, + `bytea` and `jsonb`, 1,048,576 bytes is the largest value accepted and 1,048,577 the first + rejected — nothing fails early. Compressible values go much further. Size against raw byte count; + compression only ever buys headroom. +- **The 10 MiB transaction limit also counts index writes.** The check runs at `COMMIT` against an + accounted size that adds a cost per row and per secondary index entry written. The per-entry + cost grows with the index key plus the primary key: small for `int`, `uuid` or `timestamptz` + keys, several times the key width for wide `text` keys. A 500-row batch committed at most + 9.93 MiB of raw data with no secondary index, and 9.11 MiB with four on 100-byte `text` keys. + The row limit is documented as independent of secondary indexes; the size limit is not, and its + accounting is undocumented. Large compressible values, such as JSON over a few KB, can count + below their raw size. To size a batch, read the error: `DETAIL: Current transaction size mb > + 10mb` is the exact accounted size, so one over-limit trial on the real schema gives the ratio to + scale by. +- **The key-size budget is shared, in bytes, by every key column.** One column holds ~1,981 bytes + and each additional key column costs ~7 more, so an 8-column key gets ~241 bytes each. Bytes, + not characters — a 4-byte UTF-8 character key holds ~495 characters. A secondary index key also + carries the table's primary key columns, so a 500-byte primary key leaves ~1,474 bytes for the + index columns. Separately, DDL rejects a key whose _declared_ sizes sum past 2,000 bytes, leaving + the primary key out of that sum for a secondary index; a `varchar(2000)` key therefore passes + `CREATE` and still fails on write above ~1,981 bytes. The quotas page lists the primary key and + secondary index as two separate 1 KiB budgets, but DSQL enforces the single shared budget above: + give the user these figures, and note that the documentation says 1 KiB. +- **The view-definition limit is a 512 KiB internal budget, not a text length.** How much SQL + fits depends on the query's structure, so treat **~120 KiB** of definition text as a working + ceiling and size by trial when a view approaches it. +- **Every index counts toward the 24**: the primary key, the index behind each `UNIQUE` + constraint, and every `CREATE [UNIQUE] INDEX ASYNC`, including one still building + (`indisvalid = f`). A table with a primary key and one `UNIQUE` constraint has 22 slots left; + a table with no primary key has no hidden index and holds 24. Count with + `SELECT count(*) FROM pg_index WHERE indrelid = 'tbl'::regclass`. `DROP INDEX` frees a slot. +- **`char` and `varchar` limits are enforced on the _declared_ size at DDL time**, so an + oversized declaration fails at `CREATE TABLE`, not on first write. +- **Dropped columns keep counting.** The documented ceiling is 1,600 _cumulative_ columns including + dropped ones, so a table churned by repeated `ADD COLUMN`/`DROP COLUMN` during a migration can + hit `54011` while holding far fewer live columns. Recreate the table rather than continuing to + churn it, with user approval, following [ddl-migrations/overview.md](ddl-migrations/overview.md). + +## Rejections and Constraint Violations + +Unsupported-feature list: [PostgreSQL compatibility — unsupported features](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html). Index syntax: [CREATE INDEX syntax support](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/create-index-syntax-support.html). + +These are not limits — the feature or the value is refused outright. + +| Cause | SQLSTATE | Message | +| --------------------------------------------------- | -------- | --------------------------------------------------------------------------------- | +| Non-immutable function in an index expression | `42P17` | `functions in index expression must be marked IMMUTABLE` | +| Non-immutable generated-column expression | `42P17` | `generation expression is not immutable` | +| `serial` column type | `42704` | `type "serial" does not exist` | +| Array column type | `0A000` | `datatype text[] not supported` — the declared type is echoed back | +| Sort direction on an index key | `0A000` | `specifying sort order not supported for index keys` | +| Operator class on an index key | `0A000` | `opclass not supported for index keys` | +| Index on an unsupported column type | `0A000` | `datatype is not supported in a key` | +| `CREATE INDEX` without `ASYNC` | `0A000` | `unsupported mode. please use CREATE INDEX ASYNC.` | +| `CREATE INDEX CONCURRENTLY` | `0A000` | `CONCURRENTLY not supported for CREATE INDEX` | +| `ALTER TABLE ADD COLUMN` with any inline constraint | `0A000` | `ALTER TABLE ADD COLUMN with constraint not supported` | +| `ALTER COLUMN ... SET DATA TYPE` | `0A000` | `unsupported ALTER TABLE ALTER COLUMN ... SET DATA TYPE statement` | +| `ALTER COLUMN ... SET STATISTICS`, at any value | `0A000` | `unsupported ALTER TABLE ALTER COLUMN ... SET STATISTICS statement` | +| `CREATE PROCEDURE` | `0A000` | `PROCEDURE is not supported` | +| `CREATE DATABASE` | `0A000` | `unsupported statement: Createdb` | +| Post-creation foreign key without `NOT VALID` | `0A000` | `unsupported ALTER TABLE ADD CONSTRAINT statement` | +| Synchronous `VALIDATE CONSTRAINT` | `0A000` | `unsupported ALTER TABLE VALIDATE CONSTRAINT statement` | +| Foreign key violation — **not retryable** | `23503` | `insert or update on table ... violates foreign key constraint ...` | +| Duplicate against a unique index still building | `23505` | `duplicate key value violates unique constraint ...` | +| Any statement issued after a failure inside `BEGIN` | `25P02` | `current transaction is aborted, commands ignored until end of transaction block` | +| Protocol message over 10 MiB | **none** | No error reaches the client — see below | + +These mislead in practice: + +- **`25P02` masks the real error.** It is the code an agent sees most often, because anything + that keeps issuing statements after a failure inside a transaction block gets it in place of + the original cause. Read the _first_ error in the transaction, not the last. +- **An oversized protocol message closes the connection with no error message.** The client sees + only `SSL SYSCALL error: EOF detected`, which is indistinguishable from a transient network + fault, so a naive retry resends the same oversized statement forever. Treat an EOF on the first + statement after a large one as a size problem, not a network one. Note the AWS documentation + lists `08P01` / `FATAL: invalid message length` for this case; that error is not delivered. If + you _do_ see `08P01: invalid message length`, that is this case and the behaviour has changed — trust the error. + +**Any EOF on a statement that could have committed — `COMMIT`, or any write in autocommit mode — +leaves the commit outcome unknown**, whatever the cause, because the +connection closed before the server replied. This applies to the oversized-message case above and +equally to an EOF the agent classifies as a network fault — the size discriminator tells you why the +connection dropped, not whether the write landed. Before resending, an agent **MUST** reconnect and +confirm whether the write committed. Designing the write to be idempotent removes the need for that +check, but only if it was already idempotent — it is not something that can be applied after the +EOF arrives, and a bare retry of a non-idempotent write such as +`UPDATE accounts SET balance = balance - 100` double-applies it. See +[occ-retry-patterns.md](occ-retry-patterns.md#idempotent-transaction-design). + +**If the outcome cannot be confirmed, an agent MUST NOT retry.** A blind +`balance = balance - 100` has no observable trace that distinguishes "applied once" from "not +applied", so there is nothing to query. **MUST** report the unresolved write to the user with the +transaction's identifying values and stop; a duplicated debit is worse than a reported gap. Avoid the situation rather than resolve it: write a +correlation id the transaction itself records, so the question becomes answerable next time. + +The threshold is the _protocol message_ size, which a client does not see directly: a batched or +parameterised multi-row insert can exceed it well below 10 MiB of visible data. If you cannot +measure it, reduce the batch size until the EOF stops — after confirming, as above, that the +previous attempt did not commit. + +## Cluster Quotas + +From the [Aurora DSQL quotas documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/CHAP_quotas.html). +Unlike the tables above, these are **not verified here** — they need provisioned resources to +exercise, so treat the values as documented rather than observed. + +| Quota | Value | SQLSTATE | Message when exceeded | +| ---------------------------------------------- | ------------------------------ | -------- | --------------------------------------------------------- | +| Tables per database | 1,000 | `54000` | `creating more than 1000 tables not allowed` | +| Views per database | 5,000 | `54000` | `creating more than 5000 views not allowed` | +| Sequences per database | 5,000 | `54000` | `creating more than 5000 sequences is not allowed` | +| Single-Region clusters per account, per Region | 20 (increasable) | API | `You have reached the cluster limit.` | +| Multi-Region clusters per account, per Region | 5 (increasable) | API | `You have reached the cluster limit.` | +| Storage per cluster | 10 TiB, increasable to 256 TiB | `53100` | `Current cluster size exceeds cluster size limit.` | +| Connections per cluster | 10,000 | `53300` | `Unable to accept connection, too many open connections.` | +| New connections per second | 100 sustained, 1,000 burst | `53400` | `Unable to accept connection, rate exceeded.` | +| Concurrent restore jobs | 4 | — | No error code documented | +| CDC streams per cluster | 5 | API | `You have reached the stream limit.` | + +The cluster-count and CDC rows surface as the control-plane API error +`ServiceQuotaExceededException: 402`, not a SQLSTATE, so they never reach a SQL client. diff --git a/plugins/databases-on-aws/skills/dsql/references/occ-retry-patterns.md b/plugins/databases-on-aws/skills/dsql/references/occ-retry-patterns.md index c48d8f5e..37e00f6a 100644 --- a/plugins/databases-on-aws/skills/dsql/references/occ-retry-patterns.md +++ b/plugins/databases-on-aws/skills/dsql/references/occ-retry-patterns.md @@ -4,6 +4,10 @@ DSQL uses Optimistic Concurrency Control (OCC). Write transactions are validated COMMIT time — if another transaction modified the same rows, COMMIT fails with `SQLSTATE 40001` (serialization failure). Every application MUST implement retry logic. +A **catalog change** raises the same `40001`, so the code is not on its own enough to tell which +happened. The trailing marker in the message is the discriminator — see +[Retry Strategy](#retry-strategy). + ## Table of Contents 1. [Retry Strategy](#retry-strategy) @@ -22,10 +26,43 @@ Base delay: 50ms (allows concurrent transaction to commit) Backoff: exponential with jitter Formula: delay = min(base * 2^attempt + random(0, base), max_delay) Max delay: 5000ms (stays under DSQL's 5-minute transaction timeout) -Retryable: SQLSTATE 40001 only +Retryable: SQLSTATE 40001 only (both markers — see below) Non-retryable: all other errors, including foreign key violation 23503 (raise immediately) ``` +`40001` carries two causes, distinguished by the trailing marker in the message. `(OC000)` is a +write conflict and backoff clears it. `(OC001)` means the session's cached schema catalog is older +than a catalog change another session committed. + +**The trigger is any catalog change, not only `CREATE`/`ALTER`/`DROP`, and not only a concurrent +one.** `GRANT` and `REVOKE` raise `OC001` just as `ALTER TABLE` and `CREATE INDEX ASYNC` do — DSQL +groups them with DDL itself, rejecting a pair with `multiple ddl statements not supported in a +transaction` — and it fires whether the change is still in flight or already finished. A migration +that only adjusts privileges will raise it, so do not go looking for a `CREATE` or `ALTER` that is +not there. Reads are hit as well as writes, so read paths need the retry loop too. It arrives on +the statement when the change committed before it, and at `COMMIT` when the change lands +mid-transaction. + +The +[DDL and distributed transactions](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-ddl.html) +guide states the mechanism directly: "An OC001 response can also occur when the schema change has +already completed before the affected transaction starts. Aurora DSQL query processors discover +catalog changes reactively during query execution, so a session that has been idle might still be +operating with a stale catalog version. On retry, the session refreshes its catalog cache and the +transaction typically succeeds." + +Because a retry refreshes the catalog cache, a one-shot catalog change clears on the first retry. It +keeps recurring only while another session keeps issuing changes — serialize those rather than +relying on retries. Both markers **MUST** stay in the retry loop: excluding `OC001` turns a +one-shot collision that the first retry would have cleared into a hard application error. Capture +the marker before each sleep so an exhausted loop records which cause it was retrying. See +[troubleshooting.md](troubleshooting.md#error-schema-has-been-updated-by-another-transaction-oc001). + +An error that arrives as a dropped connection rather than a SQLSTATE — `SSL SYSCALL error: EOF +detected` — leaves the commit outcome **unknown**, because the connection closed without a reply. +Do not feed it to this loop; follow the confirm-or-stop rule in +[limits-and-error-codes.md](limits-and-error-codes.md#rejections-and-constraint-violations). + --- ## DSQL Connectors (Preferred) @@ -51,9 +88,11 @@ When using a DSQL Connector, OCC retry is built in — no manual retry wrapper n Use when a DSQL Connector is not available or when custom retry behavior is required: ```python -import time, random, psycopg2 +import logging, time, random, psycopg2 from psycopg2 import errors +log = logging.getLogger(__name__) + def execute_with_retry(conn_params, operation, max_retries=5): """Execute a database operation with OCC retry.""" for attempt in range(max_retries): @@ -64,12 +103,27 @@ def execute_with_retry(conn_params, operation, max_retries=5): operation(cur) conn.commit() return - except errors.SerializationFailure: + except errors.SerializationFailure as error: conn.rollback() + # Both markers are 40001 and both belong in this loop, but they mean + # different things operationally. Logging the marker is what lets an + # exhausted loop be diagnosed: a one-shot catalog change clears on the + # first retry, so an OC001 that survives the loop means something keeps + # changing the catalog, and tuning row contention will never fix it. + marker = "OC001" if "OC001" in str(error) else "OC000" if attempt < max_retries - 1: delay = min(0.05 * (2 ** attempt) + random.uniform(0, 0.05), 5.0) + log.warning( + "40001 (%s) on attempt %d/%d, retrying in %.3fs", + marker, attempt + 1, max_retries, delay, + ) time.sleep(delay) else: + log.error( + "40001 (%s) exhausted %d attempts%s", + marker, max_retries, + "; serialize the catalog changes" if marker == "OC001" else "", + ) raise except Exception: conn.rollback() @@ -79,7 +133,8 @@ def execute_with_retry(conn_params, operation, max_retries=5): ``` The same pattern applies in any language — catch SQLSTATE 40001, apply exponential backoff -with jitter, retry up to the max. See the [DSQL code samples](https://github.com/aws-samples/aurora-dsql-samples) +with jitter, retry up to the max, and record the `(OC000)`/`(OC001)` marker so an exhausted loop +can be told apart from row contention. See the [DSQL code samples](https://github.com/aws-samples/aurora-dsql-samples) for Java, Go, Node.js, and Rust implementations. --- diff --git a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/index-conversion.md b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/index-conversion.md index d992eb17..d3b658ea 100644 --- a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/index-conversion.md +++ b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/index-conversion.md @@ -1,7 +1,9 @@ # Index Conversion for DSQL Run `dsql_lint(fix=true)` first — it handles most index conversions automatically (ASYNC, -USING gin/gist/brin/hash → btree, CONCURRENTLY removal, INCLUDE preservation, sort order). +USING gin/gist/brin/hash → btree, CONCURRENTLY removal, INCLUDE preservation, and **stripping** +`ASC`/`DESC` from index keys — DSQL rejects sort direction outright, see +[Index Key Sort Order](#index-key-sort-order)). This file covers the cases a mechanical fix cannot resolve: @@ -10,6 +12,10 @@ This file covers the cases a mechanical fix cannot resolve: - Operator classes such as `text_pattern_ops` — `dsql_lint` leaves these in place with no diagnostic, so strip them by hand. DSQL rejects them with `ERROR: 0A000: opclass not supported for index keys` +- Sort direction — `index_sort_direction`; auto-fixed, but the fix changes which `ORDER BY` the + index serves, and for `ASC` it is reported as neither an error nor a warning. See + [Index Key Sort Order](#index-key-sort-order). NULLS placement is **not** part of this rule and + is never stripped — see [NULLS placement is retained](#nulls-placement-is-retained). Partial indexes (`WHERE`) and expression indexes are **supported** — add `ASYNC` and keep them as written. No redesign is needed. @@ -17,6 +23,7 @@ written. No redesign is needed. Sources: - [Asynchronous Indexes](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-create-index-async.html) +- [CREATE INDEX syntax support](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/create-index-syntax-support.html) - [DSQL SQL Dialect Blog](https://aws.amazon.com/blogs/database/dsql-sql-dialect-how-amazon-aurora-dsql-differs-from-single-instance-postgresql/) ## Table of Contents @@ -27,8 +34,9 @@ Sources: 4. [Partial Indexes](#partial-indexes) 5. [Expression Indexes](#expression-indexes) 6. [Index Limits](#index-limits) -7. [Monitoring Async Index Status](#monitoring-async-index-status) -8. [Conversion Decision Flowchart](#conversion-decision-flowchart) +7. [Index Key Sort Order](#index-key-sort-order) +8. [Monitoring Async Index Status](#monitoring-async-index-status) +9. [Conversion Decision Flowchart](#conversion-decision-flowchart) --- @@ -154,8 +162,9 @@ CREATE INDEX idx_logs_created ON logs USING brin (created_at); CREATE INDEX ASYNC idx_logs_created ON logs (created_at); -- If the table is very large and you need to limit index size, --- use a composite index with the most selective column first: -CREATE INDEX ASYNC idx_logs_tenant_created ON logs (tenant_id, created_at DESC); +-- use a composite index with the most selective column first. +-- No ASC/DESC on index keys — see "Index Key Sort Order" below. +CREATE INDEX ASYNC idx_logs_tenant_created ON logs (tenant_id, created_at); ``` --- @@ -236,11 +245,14 @@ to be IMMUTABLE anyway. ## Index Limits -| Limit | Value | -| --------------------- | ----- | -| Max indexes per table | 24 | -| Max columns per index | 8 | -| Max PK/index key size | 1 KiB | +| Limit | Value | SQLSTATE | Message when exceeded | +| --------------------- | ------------------------------------- | -------- | ------------------------------------------------------- | +| Max indexes per table | 24, **incl. the PK** | `54000` | `more than 24 indexes per table are not allowed` | +| Max columns per index | 8 | `54011` | `more than 8 column keys in an index are not supported` | +| Max PK/index key size | ~1,981 bytes observed; docs say 1 KiB | `54000` | `key size too large` | + +For what takes one of the 24 slots, the key-size budget and the rest of the limit set, see +[limits-and-error-codes.md](../limits-and-error-codes.md#exceeded-limits). **Strategy when approaching 24 index limit:** @@ -251,6 +263,77 @@ to be IMMUTABLE anyway. --- +## Index Key Sort Order + +**DSQL rejects `ASC`/`DESC` on index keys** — both of them, with +`ERROR: 0A000: specifying sort order not supported for index keys`. `dsql_lint` reports this as rule +`index_sort_direction` and `fix=true` strips the direction, but **the severity depends on which +direction you wrote**: + +| Input | `fix_result.status` | `summary` | +| ----------------- | -------------------- | ------------------------------------ | +| `created_at DESC` | `fixed_with_warning` | `{errors: 0, warnings: 1, fixed: 0}` | +| `created_at ASC` | `fixed` | `{errors: 0, warnings: 0, fixed: 1}` | + +So a pipeline that gates on `errors == 0`, or that consumes only `fixed_sql`, proceeds without +surfacing that the statement changed — and for `ASC` even a stricter pipeline gating on +`warnings == 0` proceeds, because `ASC` raises no warning at all. Review every diagnostic rather +than either counter, and see [dsql-lint.md](../dsql-lint.md) for what zero diagnostics does and +does not mean. + +```sql +-- PostgreSQL +CREATE INDEX idx_logs_tenant_created ON logs (tenant_id, created_at DESC); + +-- DSQL: ERROR: 0A000: specifying sort order not supported for index keys +-- Drop the direction: +CREATE INDEX ASYNC idx_logs_tenant_created ON logs (tenant_id, created_at); +``` + +**This costs no capability for a uniformly-ordered `ORDER BY`.** The planner reads an index +backwards, so a single direction-free index serves both directions. Against +`(tenant_id, created_at)`, with a projection the index covers: + +| `ORDER BY` | Plan | +| --------------------------------- | ------------------------------------------------------------- | +| `tenant_id, created_at` | `Index Only Scan` | +| `tenant_id DESC, created_at DESC` | `Index Only Scan Backward` | +| `tenant_id, created_at DESC` | `Incremental Sort` above the scan, `Presorted Key: tenant_id` | +| `tenant_id DESC, created_at` | `Incremental Sort` above a backward scan | + +**The projection matters as much as the direction.** The plans above hold for +`SELECT tenant_id, created_at`. A secondary index is not covering, so `SELECT *` adds a `Sort` above +a primary-key scan in _all four_ cases — the ordering is lost to the column lookup, not to the +direction. Use `INCLUDE` to cover the projection, or accept the sort. + +The rule is **direction uniformity over a leading prefix** — uniformity alone is not enough. An +`ORDER BY` is served by a scan when its keys are a leading prefix of the index keys _and_ all point +the same way. `ORDER BY created_at` is perfectly uniform and still gets a full `Sort` against +`(tenant_id, created_at)`, because `created_at` is not a prefix. A _mixed_-direction `ORDER BY` over +a prefix keeps the `Incremental Sort`, and no DSQL index can remove it, because index keys carry no +direction to match against. If a mixed ordering is on a hot path, either sort in the application or +store an inverted column (for example a negated numeric, or a computed descending rank) and order on +it uniformly. + +### NULLS placement is retained + +`NULLS FIRST` / `NULLS LAST` **is** accepted on an index key, and `dsql_lint` keeps it — only +`ASC`/`DESC` are removed. A backward scan flips the NULLS placement along with the direction, so +an index serves exactly two orderings: its own placement ascending, and the opposite placement +descending. A default index (`NULLS LAST`) therefore covers the two default forms: + +| Index | Served by forward scan | Served by backward scan | +| ----------------- | ------------------------ | ---------------------------- | +| `(a)` | `ORDER BY a` | `ORDER BY a DESC` | +| `(a NULLS FIRST)` | `ORDER BY a NULLS FIRST` | `ORDER BY a DESC NULLS LAST` | + +**Only add `NULLS FIRST` to serve a query that explicitly asks for it.** An index declared +`NULLS FIRST` no longer satisfies the plain `ORDER BY a` or `ORDER BY a DESC` — both fall back to +a `Sort` — so adding one to fix a single query can cost the default ordering on every other. +Nullable columns need no special handling otherwise: the default index covers both directions. + +--- + ## Monitoring Async Index Status Indexes created with ASYNC are not immediately usable. Monitor: @@ -273,7 +356,8 @@ WHERE NOT indisvalid; ``` Is it a btree index? -├── Yes → CREATE INDEX ASYNC (preserve columns, INCLUDE, sort order, WHERE, IMMUTABLE expressions) +├── Yes → CREATE INDEX ASYNC (preserve columns, INCLUDE, WHERE, IMMUTABLE expressions; +│ drop ASC/DESC — a backward scan serves uniformly-descending ORDER BY) │ ├── Is it GIN? │ ├── For JSONB containment → unindexed at runtime; expression index for key equality diff --git a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/multi-region.md b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/multi-region.md index 74755300..48d79bd0 100644 --- a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/multi-region.md +++ b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/multi-region.md @@ -69,8 +69,8 @@ contention across regions — partition data by geography where possible. ## Quotas -| Quota | Value | -| --------------------------------- | ----------------------------------------------------------------------------------------- | -| Multi-region clusters per account | 5 (increasable) | -| Cluster topology | Two peered clusters in two endpoint Regions, plus one shared witness Region (no endpoint) | -| Storage per cluster | 10 TiB (up to 256 TiB) | +| Quota | Value | +| --------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Multi-region clusters per account, per Region | 5 (increasable) | +| Cluster topology | Two peered clusters in two endpoint Regions, plus one shared witness Region (no endpoint) | +| Storage per cluster | 10 TiB (up to 256 TiB) | diff --git a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/schema-objects.md b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/schema-objects.md index 9eca6b8a..9fe67737 100644 --- a/plugins/databases-on-aws/skills/dsql/references/pg-migrations/schema-objects.md +++ b/plugins/databases-on-aws/skills/dsql/references/pg-migrations/schema-objects.md @@ -166,7 +166,10 @@ CREATE TABLE events ( created_at timestamptz NOT NULL DEFAULT now(), data jsonb ); -CREATE INDEX ASYNC idx_events_tenant_created ON events (tenant_id, created_at DESC); +-- No ASC/DESC on index keys — DSQL rejects sort direction with +-- 0A000: specifying sort order not supported for index keys. +-- A backward scan still serves a uniformly-descending ORDER BY on these keys. +CREATE INDEX ASYNC idx_events_tenant_created ON events (tenant_id, created_at); ``` **Note:** DSQL's PK-ordered storage and distributed architecture handle data distribution diff --git a/plugins/databases-on-aws/skills/dsql/references/troubleshooting.md b/plugins/databases-on-aws/skills/dsql/references/troubleshooting.md index 7b9a2be2..ad8ef0a6 100644 --- a/plugins/databases-on-aws/skills/dsql/references/troubleshooting.md +++ b/plugins/databases-on-aws/skills/dsql/references/troubleshooting.md @@ -8,30 +8,59 @@ Before referring to any listed error, use the routing below and consult ## Table of Contents -1. [Connection and Authorization](#connection-and-authorization) -2. [Cluster Lifecycle](#cluster-lifecycle) -3. [Foreign Key Addition or Validation Fails](#foreign-key-addition-or-validation-fails) -4. [Incompatibility](#incompatibility) -5. [Protocol Compatibility](#protocol-compatibility) -6. [Additional Resources](#additional-resources) +1. [Limits and Error Codes](#limits-and-error-codes) +2. [Connection and Authorization](#connection-and-authorization) +3. [Cluster Lifecycle](#cluster-lifecycle) +4. [Foreign Key Addition or Validation Fails](#foreign-key-addition-or-validation-fails) +5. [Incompatibility](#incompatibility) +6. [Protocol Compatibility](#protocol-compatibility) +7. [Additional Resources](#additional-resources) + +## Limits and Error Codes + +The limit, rejection and quota tables, with each SQLSTATE and exact message, are in +[limits-and-error-codes.md](limits-and-error-codes.md). ## Connection and Authorization -### Token Expiration +### Error: "FATAL: unable to accept connection, ..." + +**Cause:** One of several conditions. `unable to accept connection` is a **shared prefix** — match +it case-insensitively, since the two connection-quota variants capitalise `Unable` and omit the +`FATAL:` prefix — then match the whole line before deciding whether to retry. -### Error: "Token has expired" +`HINT` is a **separate protocol field**, not part of the message line, so read it from the driver's +error object rather than by substring-matching the message: `err.diag.message_hint` (psycopg), +`pgErr.Hint` (Go `pgx`), `err.hint` (node-postgres). psycopg's string rendering happens to include +`HINT:`, so a substring match works there and nowhere else. -**Cause:** Authentication token older than 15 minutes -**Solutions:** +| Full message | Meaning | Retry? | +| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------- | +| `FATAL: unable to accept connection, access denied` plus `HINT: Signature expired: ...` | The token is stale | Once, with a fresh token | +| `FATAL: unable to accept connection, access denied` with no `Signature expired` hint | Wrong credentials, region, or IAM permissions | No — regenerating the token will not help | +| `FATAL: unable to accept connection, waking up cluster, please retry later` | The cluster is `INACTIVE` — see [Cluster Lifecycle](#cluster-lifecycle) | Yes, after polling for `ACTIVE` | +| `Unable to accept connection, too many open connections.` (`53300`) | Cluster connection limit reached (10,000) | Yes, after releasing connections | +| `Unable to accept connection, rate exceeded.` (`53400`) | Connection rate limit reached (100/s sustained) | Yes, with backoff — not with a new token | + +The last two rows are from the quotas documentation and are **not verified here**; the first three +were reproduced against a cluster. An agent that matches only on `unable to accept connection` +retries an authorization failure indefinitely, and treats connection-pool exhaustion as an auth +problem. + +**When the `Signature expired` hint is present**, the token is stale. Refresh connections within +15 minutes and pick one of: - Auto-regenerate tokens per connection or query OR - Use connection pool hooks to refresh before expiration OR - Implement retry logic with token regeneration -**Additional Recommendations:** +**MUST** retry at most once with a fresh token. The token is a SigV4 pre-signed URL, so if the client +clock is behind by more than the token lifetime every freshly minted token is already outside the +server's window and the hint persists no matter how many you generate. If `Signature expired` +survives a fresh token, **SHOULD** check the host clock (NTP) rather than regenerating again. -- Refresh connections within 15 minutes -- Auto-reconnect after observing auth errors +For the other rows, regenerating the token does not help — resolve the IAM, region, cluster-state, +or connection-count condition the matched row names. ### Connection Timeouts @@ -98,20 +127,26 @@ action; **MUST NOT** route it through the `40001` OCC retry loop. When migrating from PostgreSQL, remember DSQL doesn't support: -- **SERIAL types** - Use `GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY` with sequences instead +- **SERIAL types** — `42704: type "serial" does not exist`. Use + `GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY` with sequences instead. The message names a + missing _type_, not an unsupported feature, so it is easy to misread as a typo - **Extensions** - No PL/pgSQL, PostGIS, pgvector, etc. - **Triggers** - Implement logic in application layer - **Temporary tables** - Use regular tables or application-level caching - **TRUNCATE** - Use `DELETE FROM table` instead -- **Multiple databases** - Single `postgres` database per cluster +- **Multiple databases** — `0A000: unsupported statement: Createdb`. Single `postgres` database + per cluster - **Custom types** - Limited type system support - **Partitioning** - Manage data distribution in application -See [full list of unsupported features](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html). +See [full list of unsupported features](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html), +and [Rejections and Constraint Violations](limits-and-error-codes.md#rejections-and-constraint-violations) for the exact +message each rejection produces. -### Error: "Datatype array not supported" +### Error: "datatype text[] not supported" -**Cause:** Using `TEXT[]` or other array column types +**Cause:** Using `TEXT[]` or other array column types. SQLSTATE `0A000`; the rejected type is +echoed back, so `integer[]` reports `datatype integer[] not supported`. **Solution:** Serialize the array into a single column — DSQL has no array column type. PREFER `JSONB`; MAY use `TEXT` for opaque columns. ASK the user which format fits the access pattern. - **PREFER `JSONB`** — the application queries inside the value (`@>`/`?`/`?|`/`?&`, `jsonb_array_elements_text`, or indexed JSONB paths); values are normalized on write. Insert: `INSERT INTO t (tags) VALUES ($1::jsonb)` with `JSON.stringify(arr)`. Query: `jsonb_array_elements_text(tags)`. @@ -119,36 +154,55 @@ See [full list of unsupported features](https://docs.aws.amazon.com/aurora-dsql/ - **`JSON` is valid** when writes dominate (no parse/sort overhead on write), byte-exact input matters (audit, replay, duplicate keys), or only `->`/`->>` is needed. - **When migrating:** keep existing `JSON` columns as `JSON`; upgrade to `JSONB` only when JSONB-only operators or indexed paths are needed. -### Error: "Please use CREATE INDEX ASYNC" +### Error: "unsupported mode. please use CREATE INDEX ASYNC." + +**Cause:** Creating an index without the `ASYNC` keyword. `CREATE INDEX CONCURRENTLY` is rejected +separately, with `CONCURRENTLY not supported for CREATE INDEX`. Both are SQLSTATE `0A000`. -**Cause:** Creating index without ASYNC keyword **Solution:** ```sql -- Wrong -CREATE INDEX idx_name ON table(column); +CREATE INDEX idx_logs_created_a ON logs (created_at); +CREATE INDEX CONCURRENTLY idx_logs_created_b ON logs (created_at); -- Correct -CREATE INDEX ASYNC idx_name ON table(column); +CREATE INDEX ASYNC idx_logs_created ON logs (created_at); ``` -### Error: "Transaction exceeds 3000 rows" +### Error: "transaction row limit exceeded" -**Cause:** Modifying too many rows in single transaction +**Cause:** Modifying more than 3,000 rows in a single transaction. SQLSTATE `54000`. **Solution:** -1. Batch operations into chunks of 500-1000 rows +1. Start from chunks of 500-1000 rows 2. Process each batch separately 3. Add WHERE clause to limit scope -### Error: "OC001 - Concurrent DDL operation" +Treat that chunk size as a starting point, not a guarantee. The same transaction is also bounded by +size and age, and every secondary index entry written counts toward the size. On a table carrying +several secondary indexes that overhead decides the chunk size; see +[Exceeded Limits](limits-and-error-codes.md#exceeded-limits) for how to size it. + +### Error: "schema has been updated by another transaction (OC001)" + +**Cause:** The session's cached schema catalog is older than a catalog change another session +committed — any DDL, `GRANT` or `REVOKE`, concurrent or already finished. Reads are hit as well as +writes. It is SQLSTATE `40001`, the same code as an OCC write conflict, told apart only by the +marker: + +| Message | Marker | Cause | +| -------------------------------------------------------- | ------- | ------------------------------------------- | +| `change conflicts with another transaction (OC000)` | `OC000` | Concurrent DML on the same rows | +| `schema has been updated by another transaction (OC001)` | `OC001` | Any catalog change — DDL, `GRANT`, `REVOKE` | -**Cause:** Multiple DDL operations on same resource **Solution:** -1. Wait for current DDL to complete -2. Retry with exponential backoff -3. Execute DDL operations sequentially +1. Retry the transaction in the same `40001` loop — the retry refreshes the catalog cache, so a + one-shot change clears on the first retry +2. If it recurs across retries, serialize the catalog changes rather than widening the backoff + +See [occ-retry-patterns.md](occ-retry-patterns.md) for the mechanism and the retry code. ## Protocol Compatibility @@ -159,6 +213,10 @@ CREATE INDEX ASYNC idx_name ON table(column); - Use officially tested drivers from [aws-samples/aurora-dsql-samples](https://github.com/aws-samples/aurora-dsql-samples) - Test client compatibility before production deployment +A protocol message over 10 MiB is a separate failure with no error message at all — the +connection is closed and the client sees only `SSL SYSCALL error: EOF detected`. See +[Rejections and Constraint Violations](limits-and-error-codes.md#rejections-and-constraint-violations). + ## Additional Resources - [Aurora DSQL troubleshooting guide](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/troubleshooting.html#troubleshooting-connections) diff --git a/tools/evals/databases-on-aws/README.md b/tools/evals/databases-on-aws/README.md index d904fccc..0d67661d 100644 --- a/tools/evals/databases-on-aws/README.md +++ b/tools/evals/databases-on-aws/README.md @@ -14,7 +14,7 @@ their recorded results, and runner files: tools/evals/databases-on-aws/ ├── README.md # This file — top-level index └── dsql/ # Aurora DSQL skill evals - ├── evals.json # Tier 2: functional evals (22 prompts, 93 assertions) + ├── evals.json # Tier 2: functional evals (28 prompts, 122 assertions) ├── dsql_lint_evals.json # dsql_lint workflow (4 prompts, 20 assertions) ├── pg_migration_evals.json # PostgreSQL migrations (17 prompts, 90 assertions) ├── pg_migration_hallucination_evals.json # Migration hallucinations (3 prompts, 14 assertions) @@ -96,7 +96,7 @@ mise exec -- python tools/evals/databases-on-aws/dsql/scripts/run_functional_eva --verbose ``` -**What it checks** (22 eval prompts, 93 assertions total): +**What it checks** (28 eval prompts, 122 assertions total): | Eval | Focus | Grader | Key assertions | | ------------------------------ | --------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -122,6 +122,12 @@ mise exec -- python tools/evals/databases-on-aws/dsql/scripts/run_functional_eva | 20. Direct constraint changes | Constraint migration | LLM judge | Direct `DROP CONSTRAINT`, CHECK `NOT VALID`, async validation, no table recreation | | 21. Direct column options | Column migration | LLM judge | Direct `DROP NOT NULL` and default changes, no table recreation | | 22. Joined SELECT FOR UPDATE | ORM routing (Django) | LLM judge | Keeps joined/non-key locking query, explains commit-time OCC checks, uses targeted-row primary-key accounting toward the 10 MiB transaction-size limit, covers lock clauses, retries SQLSTATE 40001 | +| 23. Index key sort order | Index conversion | LLM judge | Strips `ASC`/`DESC`, keeps `ASYNC`, explains the backward scan serves a uniform `ORDER BY` but not a mixed one, no counter-based lint gating, retains `NULLS` placement | +| 24. Transaction size exceeded | Troubleshooting | LLM judge | Identifies the 10 MiB limit, checked at `COMMIT` and not against raw bytes, splits the batch, also cites the 3,000-row limit, does not suggest raising it | +| 25. EOF at COMMIT | Troubleshooting | LLM judge | Commit outcome unknown, bare retry can double-debit, reconnect and confirm, addresses the unconfirmable case, not an ordinary `40001` retry | +| 26. Catalog change (OC001) | OCC retry | LLM judge | `GRANT`/`REVOKE` raise `OC001` like DDL, the session's cached catalog is stale, change before or during the job, retry clears a one-shot, serialize if persistent, stays in the `40001` loop | +| 27. Key size too large | Limits | LLM judge | Budget combined across key columns, ~1,981 bytes for one column, secondary index shares the primary key's budget, bytes not characters, shorten or hash | +| 28. Index count on migration | Limits | LLM judge | Counts the PK and `UNIQUE` indexes inside the 24, concludes 25 does not fit, never 24 secondary plus the PK, consolidates rather than raising | ### Grader modes @@ -130,7 +136,7 @@ Every eval consumed by `run_functional_evals.py` **MUST** declare one of two gra `"schema_version": 2`; result artifacts report schema and grading-protocol version `2` because the stricter grading and incomplete-run semantics are not directly comparable with older runs. -- **Regex / tool-call**: fast, cheap, deterministic. Each accepted assertion maps to a registered rule; the schema rejects assertions without one. Dedicated rules validate exact tool behavior and safety constraints. Compatibility rules retained for legacy corpora use polarity-aware keyword scoring. Limit rules bind values to their subject, such as 3,000 rows per transaction, 24 indexes per table, and 8 columns per index. +- **Regex / tool-call**: fast, cheap, deterministic. Each accepted assertion maps to a registered rule; the schema rejects assertions without one. Dedicated rules validate exact tool behavior and safety constraints. Compatibility rules retained for legacy corpora use polarity-aware keyword scoring. Limit rules bind values to their subject, such as 3,000 rows per transaction, 24 indexes per table, and 8 columns per index. Whether an answer counts the primary key inside the 24 is left to eval 28's judge. - **LLM judge**: runs a tool-free `claude -p` once per expectation with the complete redacted final answer, bounded call and result metadata, a complete tool-name inventory, the user prompt, and the assertion. Answers longer than 18,000 redacted characters are ungraded instead of being truncated. Tool-result bodies are explicitly untrusted and cannot independently satisfy assertions about what the agent presents or explains. The judge returns `{passed, evidence}`. Use it for semantic assertions where paraphrasing, negation, or synonym coverage makes regex brittle, such as "Does NOT recommend X." Each assertion incurs model-dependent cost and latency. Select the judge independently of the subject via `--judge-model` (defaults to the CLI default) diff --git a/tools/evals/databases-on-aws/dsql/evals.json b/tools/evals/databases-on-aws/dsql/evals.json index bfac8e8f..fcaaaaa4 100644 --- a/tools/evals/databases-on-aws/dsql/evals.json +++ b/tools/evals/databases-on-aws/dsql/evals.json @@ -33,7 +33,7 @@ { "id": 3, "prompt": "I need to add 30 indexes to my DSQL table for different query patterns. Is that possible?", - "expected_output": "Explains the 24 index per table limit, verifies via awsknowledge, suggests alternatives like composite indexes or query redesign", + "expected_output": "Explains the 24 index per table limit, counting the primary key, so 23 secondary indexes; verifies via awsknowledge, suggests alternatives like composite indexes or query redesign", "expectations": [ "Calls awsknowledge search_documentation with an index-related query", "Mentions the 24 indexes per table limit", @@ -276,6 +276,83 @@ "Recommends retrying the whole transaction with backoff when a conflict returns SQLSTATE 40001" ], "grader": "llm_judge" + }, + { + "id": 23, + "prompt": "I'm porting this index to Aurora DSQL: CREATE INDEX idx_logs ON logs (tenant_id, created_at DESC); It fails. What do I do, and do I lose the descending ordering?", + "expected_output": "Identifies that DSQL rejects sort direction on index keys, strips ASC/DESC, and explains that a uniformly-descending ORDER BY is still served by a backward scan while a mixed-direction one is not", + "expectations": [ + "States that DSQL rejects ASC/DESC on index keys and that the direction must be removed, keeping CREATE INDEX ASYNC", + "Explains that a uniformly-ordered ORDER BY is still served because the planner can scan the index backwards, so the descending ordering is not lost", + "States that a mixed-direction ORDER BY still requires a sort step that no DSQL index can remove", + "Does not claim that dsql_lint reports the stripped direction as a warning in every case, or advise gating a pipeline solely on the error or warning count", + "Does not advise removing NULLS FIRST or NULLS LAST, which DSQL accepts and dsql_lint retains" + ], + "grader": "llm_judge" + }, + { + "id": 24, + "prompt": "My DSQL batch insert fails with: ERROR: transaction size limit 10mb exceeded, DETAIL: Current transaction size 11mb > 10mb. I'm only inserting 2,000 rows. How do I fix it?", + "expected_output": "Identifies the 10 MiB per-transaction limit, explains the check happens at COMMIT and is not a raw byte count, and recommends splitting the batch", + "expectations": [ + "Identifies the 10 MiB per-transaction size limit as the cause and recommends splitting the write into smaller committed batches", + "States that the limit is checked at COMMIT rather than per statement", + "Notes that the size the limit applies to is not the raw byte count of the data, so the raw size does not reliably predict whether a batch commits", + "Also mentions the separate 3,000 row-modification limit per transaction", + "Does not advise raising the limit, since it is not configurable" + ], + "grader": "llm_judge" + }, + { + "id": 25, + "prompt": "My Python app sent a COMMIT to DSQL and got 'SSL SYSCALL error: EOF detected' instead of a reply. The transaction debits an account balance. Should I just retry it?", + "expected_output": "States that the commit outcome is unknown, that a bare retry can double-apply the write, and that the agent must confirm the outcome or report it rather than blindly retrying", + "expectations": [ + "States that an EOF at or after COMMIT leaves the commit outcome unknown because the connection closed before the server replied", + "Explicitly warns that a bare retry can apply the debit twice", + "Advises reconnecting and confirming whether the write committed before resending", + "Addresses what to do when the outcome cannot be determined, rather than only prescribing confirmation", + "Does not classify the EOF as an ordinary transient error to be fed into the 40001 retry loop" + ], + "grader": "llm_judge" + }, + { + "id": 26, + "prompt": "My DSQL migration only runs GRANT SELECT statements, no DDL. A long-running job that reads a large table and writes summary rows back keeps failing at COMMIT with 'schema has been updated by another transaction (OC001)' and SQLSTATE 40001. There's no DDL anywhere, so why?", + "expected_output": "Explains that GRANT and REVOKE are catalog changes that raise OC001 just as DDL does, that the trigger is the session's stale cached catalog rather than row contention, and that a retry clears a one-shot change", + "expectations": [ + "States that GRANT and REVOKE modify the schema catalog and raise OC001 just as DDL statements do, so the absence of DDL does not rule it out", + "Explains that the cause is the session's cached schema catalog being older than a committed catalog change, and accepts either case as correct: a GRANT that completed before the transaction started, or one that committed during it", + "States that the reporting transaction should retry, because a retry refreshes the session's catalog cache and a one-shot catalog change clears", + "Advises serializing the catalog changes if the error persists across retries", + "Does not advise excluding OC001 from the 40001 retry loop or treating it as non-retryable" + ], + "grader": "llm_judge" + }, + { + "id": 27, + "prompt": "In DSQL I get 'key size too large' when inserting into a table whose primary key is (tenant_name text, region text, bucket text). What's the actual budget, and does adding a secondary index on a long text column get its own budget?", + "expected_output": "Explains the combined key-size budget across all key columns, that a single column reaches roughly 1,981 bytes, and that a secondary index shares the budget with the table's primary key columns rather than getting a separate one", + "expectations": [ + "States that the key-size budget is combined in bytes across every key column, not per column", + "Gives the single-column ceiling as roughly 1,981 bytes and notes that each additional key column reduces the budget", + "States that a secondary index shares the budget with the table's primary key columns rather than getting a separate allowance", + "Notes that the budget is in bytes rather than characters, so multi-byte UTF-8 values fit fewer characters", + "Recommends shortening or hashing the key columns rather than attempting to raise the limit" + ], + "grader": "llm_judge" + }, + { + "id": 28, + "prompt": "I'm migrating a PostgreSQL orders table to Aurora DSQL. It has a primary key, a UNIQUE constraint on order_number, and 23 other indexes for reporting queries. Will it fit, and if not, what should I change?", + "expected_output": "Explains that the 24-index limit counts every index on the table, including the primary key and the index behind the UNIQUE constraint, so the table needs 25 slots and does not fit; suggests consolidating indexes", + "expectations": [ + "States that the 24-index limit counts every index on the table, including the primary key and the index behind the UNIQUE constraint", + "Concludes that the table needs 25 indexes, so it does not fit and at least one index must be dropped or merged", + "Does NOT state that 24 secondary indexes are allowed in addition to the primary key", + "Suggests consolidating indexes, for example into composite indexes, rather than trying to raise the limit" + ], + "grader": "llm_judge" } ] } diff --git a/tools/evals/databases-on-aws/dsql/scripts/run_functional_evals.py b/tools/evals/databases-on-aws/dsql/scripts/run_functional_evals.py index 8b45e2cf..c044c43d 100644 --- a/tools/evals/databases-on-aws/dsql/scripts/run_functional_evals.py +++ b/tools/evals/databases-on-aws/dsql/scripts/run_functional_evals.py @@ -3323,7 +3323,9 @@ def _build_judge_evidence(run_result: dict) -> str: r"\b(?:do|does|did|is|are|was|were|should|must|can)\s+not|" r"\b(?:don't|doesn't|didn't|isn't|aren't|wasn't|weren't|" r"shouldn't|mustn't)\b" - r")\s+(?:[a-z0-9_-]+\s+){0,12}$", + # Chained words must start alphanumeric, so the dash in "No - DSQL + # supports 24" ends the chain like a comma or an em dash does. + r")\s+(?:[a-z0-9][a-z0-9_-]*\s+){0,12}$", re.IGNORECASE, ) POSITIVE_UPPER_BOUND = re.compile( @@ -3332,6 +3334,11 @@ def _build_judge_evidence(run_result: dict) -> str: r"no\s+(?:\w+\s+){0,4}(?:may|can|should|must)\s+exceed)\s*$", re.IGNORECASE, ) +POSITIVE_UPPER_BOUND_SUFFIX = re.compile( + r"^\s*(?:cannot|can't|may\s+not|must\s+not|should\s+not)\s+" + r"(?:\w+\s+){0,4}exceed\b", + re.IGNORECASE, +) POST_MATCH_NEGATION = re.compile( r"^\s*(?:" r"(?:is|are|was|were|does|do|did|should|must|can)\s+" @@ -3396,6 +3403,10 @@ def _match_is_positive(value: str, match: re.Match) -> bool: suffix = value[match.end():match.end() + 80] if POSITIVE_UPPER_BOUND.search(prefix): prefix = "" + # Symmetric with the prefix case: "a table can't exceed 24 indexes" states an + # upper bound, so the trailing "can't" must not negate the preceding match. + if POSITIVE_UPPER_BOUND_SUFFIX.search(suffix): + suffix = "" return ( NEGATED_MENTION.search(prefix) is None and POST_MATCH_NEGATION.search(suffix) is None @@ -4935,18 +4946,79 @@ def grade_eval( evidence = "No mention of 10 MiB data size limit found" # --- Assertion: 24 indexes --- + # Checks only that the cap is stated, as 24 per table or 23 secondary. + # Whether the answer counts the primary key inside the 24 turns on + # paraphrase and negation, so eval 28 leaves that to the LLM judge. elif rule is AssertionRule.INDEXES_PER_TABLE: - if _has_positive_statement( + # "DSQL allows 23 secondary indexes" names no table but states the + # same per-table cap. + subject = r"\b(?:tables?|DSQL)\b" + bound = ( + r"\b(?:limit|maximum|max|at\s+most|up\s+to|cap(?:ped|s)?|" + r"no\s+more\s+than|" + r"can\s+have|supports?|allows?|accepts?|permits?|holds?|" + r"per\s+tables?)" + ) + # "cannot have more than 24" is an upper bound, but the shared + # negation check reads it as a denial, so rewrite it to "at most". + capped_text = re.sub( + r"\b(?:cannot|can\s+not|(?:do|does|may|must|will)\s+not" + r"|(?:ca|do|does|must|wo)n['’]t)\s+" + r"(?:exceed|(?:\w+\s+)?more\s+than)\b", + "at most", text, - r"\b24\s+(?:secondary\s+)?(?:indexes|indices)\b", - r"\btables?\b", - r"\b(?:limit|maximum|max|at\s+most|up\s+to|" - r"cannot\s+exceed|can't\s+exceed|no\s+more\s+than)", + flags=re.IGNORECASE, + ) + # The "no" in "a table with no primary key accepts 24" is not a + # negation of the cap beside it. + capped_text = re.sub( + r"\b(?:no|without\s+(?:a|any))[\s-]+(?:primary\s+keys?|PKs?)\b", + "PK-less table", + capped_text, + flags=re.IGNORECASE, + ) + index_noun = r"(?:indexes|indices|index)" + # Number-first allows one modifier ("24 secondary indexes", "24-index") + # so "24 columns per index" stays out. Noun-first crosses a short run + # with no other digit, and rejects another unit after the number + # ("index builds take up to 24 hours"). + gap = r"[^.!?\n\d]{0,40}?" + not_other_unit = ( + r"(?![\s-]+(?:hours?|hrs?|minutes?|mins?|seconds?|secs?|days?" + r"|columns?|rows?|bytes?|characters?|connections?)\b)" + ) + secondary_adj = ( + r"(?:secondary|non-?primary(?:[- ]key)?|non-?PK|additional|extra" + r"|user(?:-defined)?)" + ) + # 23 counts only with the qualifier: a bare "23 indexes per table" + # is wrong, since 23 is the secondary count and 24 the total. + cap_shapes = ( + rf"\b24\b[^\w.!?\n]{{0,4}}(?:[\w-]+\s+)?{index_noun}\b", + rf"\b{index_noun}\b{gap}\b24\b{not_other_unit}", + rf"\b23\s+{secondary_adj}\s+{index_noun}\b", + rf"\b{secondary_adj}\s+{index_noun}\b{gap}\b23\b", + ) + if any( + _has_positive_statement(capped_text, shape, subject, bound) + for shape in cap_shapes ): passed = True - evidence = "Found a positive 24-index limit in response" + evidence = "Found a positive 24-index (or 23 secondary) limit" + elif _has_positive_statement( + capped_text, + rf"\b(?:23|24)\s+(?:\w+(?:[- ]\w+)?\s+)?{index_noun}\b", + subject, + bound, + ): + # Separates "stated a cap in a shape this rule does not + # recognise" from "never mentioned the limit", so a red eval + # is triaged as a wrong number rather than a missing doc. + evidence = "Stated an index cap in an unrecognised form" else: - evidence = "No mention of 24 indexes per table limit found" + evidence = ( + "No mention of 24 indexes per table (or 23 secondary) found" + ) # --- Assertion: 8 columns per index --- elif rule is AssertionRule.COLUMNS_PER_INDEX: diff --git a/tools/evals/databases-on-aws/dsql/scripts/test_run_functional_evals.py b/tools/evals/databases-on-aws/dsql/scripts/test_run_functional_evals.py index eea56d35..759dbe55 100644 --- a/tools/evals/databases-on-aws/dsql/scripts/test_run_functional_evals.py +++ b/tools/evals/databases-on-aws/dsql/scripts/test_run_functional_evals.py @@ -177,6 +177,256 @@ def test_safe_query_grading_rejects_explicit_f_string_injection() -> None: assert safe["summary"]["passed"] == 1 # nosec B101 +def test_indexes_per_table_evidence_distinguishes_unrecognised_from_absent() -> None: + """The two failure paths must each say *why* they failed. + + Collapsing "stated a cap in an unrecognised form" into "never mentioned the + limit" sends whoever triages a red eval after a missing-doc or routing + problem when the real defect is a wrong number. Nothing else in the suite + asserts these strings, so a refactor that merged the branches would stay + green. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + + unrecognised = RUNNER.grade_eval( + eval_item, + _regex_run_result("A table supports at most 23 unique indexes."), + ) + absent = RUNNER.grade_eval( + eval_item, + _regex_run_result("DSQL requires CREATE INDEX ASYNC."), + ) + + assert unrecognised["summary"]["failed"] == 1 # nosec B101 + assert ( # nosec B101 + "unrecognised form" in unrecognised["expectations"][0]["evidence"] + ) + assert absent["summary"]["failed"] == 1 # nosec B101 + assert "No mention" in absent["expectations"][0]["evidence"] # nosec B101 + + +def test_indexes_per_table_accepts_doc_quoting_shapes() -> None: + """A correct answer must pass however it is laid out. + + Eval 3 requires an `awsknowledge` documentation call, so the answer tends to + arrive as a quoted limits table or bullet list rather than prose. Requiring + the number and the index noun to be adjacent, in that order, in one sentence + failed exactly the shapes the eval induces -- a markdown row puts a cell + boundary between them, a bullet puts the noun first, and a bolded number puts + asterisks. Each entry below was observed to fail before. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + correct = ( + "| Indexes per table | 24 (includes the primary key) |", + "The maximum number of indexes per table is 24, including the " + "primary key.", + "A table can't exceed 24 indexes, including the primary key.", + "The limit is **24** indexes per table, including the primary key.", + "A table can have 24 indexes, including the primary key.", + "Limits:\n- Indexes per table: 24, including the primary key", + "A DSQL table accepts 23 secondary indexes.", + ) + + for text in correct: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["passed"] == 1, text # nosec B101 + + +def test_indexes_per_table_accepts_the_singular_noun() -> None: + """"24-index limit" is a correct answer, not a missing one. + + Matching only the plural graded both forms below as "No mention of 24 + indexes per table found", which is the same defect -- failing a correct + answer over its shape -- that the doc-quoting cases above cover. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + correct = ( + "A DSQL table respects the 24-index limit per table, including the " + "primary key.", + "Max 24 index per table, including the primary key.", + ) + + for text in correct: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["passed"] == 1, text # nosec B101 + + + + + + + + +def test_indexes_per_table_accepts_upper_bound_wording() -> None: + """A negated bound states the cap; it does not deny it. + + "cannot have more than 24" and "doesn't allow more than 24" read as + negations to the shared check, and a curly apostrophe in "can’t exceed" + matched no bound at all, so each was graded as no mention. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + correct = ( + "A table cannot have more than 24 indexes, including the primary key.", + "A table can’t exceed 24 indexes, including the primary key.", + "Aurora DSQL doesn't allow more than 24 indexes on a single table, and " + "the primary key counts toward that limit, which leaves you 23 " + "secondary indexes.", + "A table cannot have more than 24 indexes — the primary key counts as " + "one of them.", + # a negated index mention earlier must not hide the cap after it + "Unlike standard PostgreSQL, which has no fixed index limit, Aurora " + "DSQL allows **at most 24 indexes per table**, counting the primary " + "key. That leaves 23 for secondary indexes.", + ) + + for text in correct: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["passed"] == 1, text # nosec B101 + + +def test_indexes_per_table_accepts_correct_pk_framings() -> None: + """Answers that place the PK beside 23, or name DSQL, state the cap.""" + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + correct = ( + "A table can have 23 secondary indexes plus the primary key.", + # names DSQL rather than the table, and adds the total + "DSQL allows 23 secondary indexes plus the primary key.", + "DSQL allows 23 secondary indexes plus the primary key, 24 in total.", + "A table can have at most 24 indexes including the primary key. In " + "addition to the primary key, create only the secondary indexes you " + "need.", + "A table supports at most 24 indexes, including the primary key. In " + "addition to the primary key, you can create 23 secondary indexes.", + ) + + for text in correct: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["passed"] == 1, text # nosec B101 + + +def test_indexes_per_table_accepts_the_no_pk_caveat() -> None: + """The "no" in "a table with no primary key" does not negate the cap. + + A table with no PK really does take 24 secondary indexes, and answers add + it as a caveat to the rule. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + correct = ( + "No. Aurora DSQL allows at most 24 indexes per table, including the " + "primary key, so a table with a primary key gets 23 secondary " + "indexes. A table with no primary key can use all 24 for secondary " + "indexes.", + "No, 30 is over the cap. The limit is 24 indexes per table. If the " + "table has a primary key, it uses one of those slots, leaving 23 " + "secondary indexes; without a primary key you could create 24 " + "secondary indexes.", + "Not possible: the maximum is 24 indexes per table, counting the " + "primary key. That leaves 23 secondary indexes (24 if the table has " + "no primary key).", + "DSQL caps each table at 24 indexes including the primary key, i.e. " + "23 secondary indexes. Note: a table created without a primary key " + "can have 24 secondary indexes, since there is no PK index taking a " + "slot.", + "No. The maximum is 24 indexes per table, including the primary key. " + "In practice that means 23 secondary indexes on a table with a " + "primary key, or 24 on a table without one.", + "24 indexes per table is the hard cap, including the primary key (23 " + "secondary indexes). Edge case: a no-PK table accepts 24 secondary " + "indexes.", + "The limit is 24 indexes per table, including the primary key, so 23 " + "secondary indexes with a PK. Tables without a declared primary key " + "use a hidden row ID that does not count toward the limit, so they " + "accept 24 secondary indexes.", + "No: max 24 indexes per table including the primary key -> 23 " + "secondary indexes. (Without a primary key the full 24 are available " + "for secondary indexes, but every DSQL table should have a primary " + "key.)", + "No. A table can have at most 24 indexes, including the primary key. " + "A table without a primary key can have 24 secondary indexes.", + "Not quite: the cap is 24 indexes per table, including the primary " + "key, so with a PK you can add 23 more. A table with no primary key " + "can hold 24 secondary indexes.", + "**No.** Aurora DSQL limits a table to **24 indexes**, and the " + "primary key counts as one of them, leaving 23 for secondary indexes. " + "If you skip the primary key, you can create 24 secondary indexes.", + # the leading "No -" answers the question; it does not negate the cap + "No - DSQL supports up to 24 indexes per table, including the primary " + "key (so 23 secondary indexes for a table with a PK, 24 for one " + "without).", + # the caveat on its own, with no inclusive rule beside it + "A table with no primary key accepts 24 secondary indexes.", + ) + + for text in correct: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["passed"] == 1, text # nosec B101 + + +def test_indexes_per_table_ignores_unrelated_counts() -> None: + """Relaxing adjacency must not make any nearby 24 count as the cap. + + The gap between the number and the index noun admits no other digit, so a + sentence that happens to mention 24 of something else alongside indexes is + still "no mention" rather than a stated limit. + """ + eval_item = { + "prompt": "How many indexes can a DSQL table have?", + "expectations": ["Mentions the 24 indexes per table limit"], + "grader": "regex", + } + + unrelated = ( + "A table supports 24 columns and 8 indexes per index key.", + # the number counts the noun right after it, not a later "index" + "You can have up to 24 columns per index on a table.", + "A DSQL migration can take up to 24 hours per table index build.", + "A large table's index build can take up to 24 hours to finish.", + "Building an index on a big table can take up to 24 hours.", + ) + + for text in unrelated: + graded = RUNNER.grade_eval(eval_item, _regex_run_result(text)) + assert graded["summary"]["failed"] == 1, text # nosec B101 + + # 23 is the secondary count, not the total, so a bare "23 indexes per + # table" is wrong and must not satisfy the cap just because the relaxed + # ordering lets the noun precede the number. + bare_23 = RUNNER.grade_eval( + eval_item, + _regex_run_result( + "The maximum number of indexes per table is 23." + ), + ) + + assert bare_23["summary"]["failed"] == 1 # nosec B101 + + + + def test_separate_ddl_grading_rejects_combined_transaction() -> None: eval_item = { "prompt": "Create a DSQL schema.", @@ -1994,10 +2244,50 @@ def capture_judge_evidence(**kwargs): 1, ), ( + "Mentions the 24 indexes per table limit", + "A table supports at most 23 secondary indexes.", + 1, + ), + ( + # Whether the 24 counts the PK is eval 28's judge's call, not this + # rule's. "Mentions the 24 indexes per table limit", "A table supports at most 24 secondary indexes.", 1, ), + ( + "Mentions the 24 indexes per table limit", + "A table has at most 24 total indexes.", + 1, + ), + ( + "Mentions the 24 indexes per table limit", + "The cap is 24 indexes per table including the primary key, so you " + "get 23 secondary indexes.", + 1, + ), + ( + # "cap"/"capped at" is as common as "limit" and used to grade as no + # mention at all. + "Mentions the 24 indexes per table limit", + "Each table is capped at 24 indexes including the primary key.", + 1, + ), + ( + "Mentions the 24 indexes per table limit", + "A table may have at most 23 non-PK indexes.", + 1, + ), + ( + "Mentions the 24 indexes per table limit", + "A table supports at most 23 non-primary-key indexes.", + 1, + ), + ( + "Mentions the 24 indexes per table limit", + "A table supports at most 24 combined indices.", + 1, + ), ( "Mentions the 8 columns per index limit", "Each index has a maximum of 8 columns.",