返回 Skills
aws/agent-toolkit-for-aws· Apache-2.0 内容可用

amazon-keyspaces

>-

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: amazon-keyspaces description: >- Provides authoritative compatibility checks, pricing estimates, connection troubleshooting, pre-warming guidance, and infrastructure mutations for Amazon Keyspaces (for Apache Cassandra). Covers LWT/batch operations, secondary indexes, materialized views, capacity modes, TTL, PITR, CDC, auto-scaling, multi-region keyspaces, UDTs, nodetool diagnostics parsing, SQL-to-Cassandra migration, and Cassandra-to-Keyspaces migration scenarios. Agents frequently produce incomplete or incorrect answers about Keyspaces feature support without this skill loaded. version: 1

Amazon Keyspaces

Safety guidance

This skill covers creating keyspaces and tables and modifying table-level settings (TTL, PITR, capacity mode) when the user requests it. The agent MUST confirm the action with the user before executing. Do NOT execute any create or modify operation without explicit user confirmation (e.g., "yes", "proceed", "confirmed", "go ahead"). If the user has not confirmed, present the planned action and ask for approval.

Execute these operations (after user confirmation)

  • Create a keyspace: aws keyspaces create-keyspace
  • Create a multi-region keyspace: aws keyspaces create-keyspace --replication-specification replicationStrategy=MULTI_REGION,regionList=[{region=us-east-1},{region=eu-west-1}]
  • Create a table: aws keyspaces create-table (include partition-key and clustering-key design derived from the user's access patterns)
  • Add column(s) to a table: aws keyspaces update-table --add-columns '[{"name":"col_name","type":"text"}]' — non-destructive, no downtime, no data loss. Existing rows get null for the new column.
  • Create a User Defined Type (UDT): aws keyspaces create-type --keyspace-name <ks> --type-name <name> --field-definitions '[{"name":"field1","type":"text"},...]'
  • Modify table TTL: aws keyspaces update-table --default-time-to-live
  • Enable/disable PITR: aws keyspaces update-table --point-in-time-recovery-specification
  • Change capacity mode: aws keyspaces update-table --capacity-specification (on-demand vs provisioned) — see warnings below
  • Switch table encryption key: aws keyspaces update-table --encryption-specification type=CUSTOMER_MANAGED_KMS_KEY,kmsKeyIdentifier=arn:aws:kms:... — no downtime or availability loss. Can also switch back to AWS owned key with type=AWS_OWNED_KMS_KEY.
  • Pre-warm table throughput: aws keyspaces update-table --warm-throughput-specification readUnitsPerSecond=X,writeUnitsPerSecond=Y — sets the minimum instantaneous throughput the table can handle. Use before planned traffic spikes (flash sales, migrations, batch loads). One-time cost based on the delta above natural warm throughput. Also available on aws keyspaces create-table --warm-throughput. Load pre-warming.md for the decision framework and sizing formulas.
  • Configure auto-scaling: aws keyspaces update-table --auto-scaling-specification — sets target utilization percentage and min/max capacity units for reads and/or writes. Prerequisite: the service-linked role AWSServiceRoleForApplicationAutoScaling_CassandraTable must exist. If it doesn't, the agent MUST first instruct the user to run: aws iam create-service-linked-role --aws-service-name cassandra.application-autoscaling.amazonaws.com. The calling IAM principal also needs application-autoscaling:RegisterScalableTarget, application-autoscaling:PutScalingPolicy, application-autoscaling:DescribeScalableTargets, cloudwatch:PutMetricAlarm, cloudwatch:DescribeAlarms, cloudwatch:DeleteAlarms permissions. Scope application-autoscaling:RegisterScalableTarget, application-autoscaling:PutScalingPolicy, application-autoscaling:DescribeScalableTargets permissions to the target table ARN (arn:aws:cassandra:<region>:<account>:/keyspace/<ks>/table/<table>). Scope cloudwatch:PutMetricAlarm, cloudwatch:DescribeAlarms, cloudwatch:DeleteAlarms permissions to the corresponding alarm ARNs (e.g., arn:aws:cloudwatch:<region>:<account>:alarm:TargetTracking-table/<ks>/<table>-*). Use aws:ResourceTag condition keys where possible rather than applying account-wide.
  • Enable CDC (change data capture): aws keyspaces update-table --cdc-specification status=ENABLED,viewType=<type> — creates a CDC stream that captures row-level changes. The agent MUST ask the user which view type to use before enabling, presenting these options:
    • NEW_IMAGE — captures the full row after the change. Best for: event-driven pipelines, downstream sync, materialized views.
    • OLD_IMAGE — captures the full row before the change. Best for: audit trails, compliance logging, undo/rollback scenarios.
    • NEW_AND_OLD_IMAGES — captures both before and after states. Best for: diff-based pipelines, detailed auditing, conflict resolution. Higher CDC consumption cost.
    • KEYS_ONLY — captures only the partition key and clustering key columns. Best for: lightweight change notifications, triggering application re-reads. Lowest cost. Optional: propagateTags=TABLE copies the table's tags to the CDC stream. Recommend enabling by default.
  • Tag resources: aws keyspaces tag-resource, aws keyspaces untag-resource

Resource tagging (MANDATORY — always apply on resource creation)

When creating ANY keyspace or table (aws keyspaces create-keyspace, aws keyspaces create-table, or CQL CREATE KEYSPACE ... WITH TAGS, CREATE TABLE ... WITH TAGS), you MUST include these tags. A create operation without these tags is INCOMPLETE and INCORRECT:

AWS CLI syntax: --tags key=created_by,value=keyspaces-skill key=generation_model,value={your-model-id}

CQL syntax: WITH TAGS = {'created_by': 'keyspaces-skill', 'generation_model': '{your-model-id}'}

Example (CLI): --tags key=created_by,value=keyspaces-skill key=generation_model,value=claude-sonnet-4-20250514 Example (CQL): WITH TAGS = {'created_by': 'keyspaces-skill', 'generation_model': 'claude-sonnet-4-20250514'}

Include these tags even if the user does not mention tagging, so that they can identify the resources created via this skill. If the user provides additional tags, append these to their tags rather than replacing them. Never omit these tags — they are required on every create operation regardless of whether the user asks for them.

Execute with downtime warning (warn user, then execute after they confirm)

  • Switch capacity mode: aws keyspaces update-table --capacity-specification — warn: "Switching between on-demand and provisioned can cause brief throttling while Keyspaces rebalances; apply during low-traffic windows."
  • Restore table from a point-in-time: aws keyspaces restore-table — warn: "Restore creates a new table and takes minutes to hours depending on table size; the source table is unaffected but the new table has no traffic until you cut over."

Do NOT execute (refuse, explain why, offer assessment instead)

  • Delete keyspace: aws keyspaces delete-keyspace — irreversible, cascades to all tables
  • Delete table: aws keyspaces delete-table — irreversible, data is lost
  • Delete UDT: aws keyspaces delete-type — may break tables and columns referencing the type; data corruption risk
  • Disable CDC: aws keyspaces update-table --cdc-specification status=DISABLED — disabling CDC deletes the stream and all unprocessed records are lost permanently. Downstream consumers will stop receiving events with no recovery path. Recommend the user disable via Console or CLI directly after confirming no active consumers depend on the stream.
  • Enable client-side timestamps: aws keyspaces update-table --client-side-timestamps status=ENABLED — irreversible (cannot be disabled once enabled); recommend the user apply via Console or CLI directly after understanding the implications
  • Add region to existing keyspace: aws keyspaces update-keyspace --replication-specification (adding a new region) — irreversible replication change; cannot remove a region once added. Recommend creating a new multi-region keyspace instead if testing.
  • Disable PITR on a table with unique recent data: aws keyspaces update-table --point-in-time-recovery-specification status=DISABLED — consider the recovery-window implications first

When refusing, explain why and offer the matching assessment workflow:

"I can't perform [action] because [reason]. I can run an assessment to help you decide. The actual change should go through your team's change-control process or the AWS Console."

Overview

Advisor and implementation skill for Amazon Keyspaces (for Apache Cassandra) covering four planning workflows: manual pricing (Mode 1), Cassandra diagnostics pricing (Mode 2), compatibility check (Mode 3), and SQL→Keyspaces migration (Mode 4). Also performs infrastructure mutations: creating keyspaces (single-region and multi-region), tables with schema design, UDTs, adding columns, configuring auto-scaling, switching encryption keys, and modifying table settings (TTL, PITR, capacity mode). Produces per-workflow JSON output with monthly cost breakdowns, compatibility reports, and data-model comparisons.

This skill works best with the AWS MCP server for direct AWS API execution, but all operations are also expressed as AWS CLI commands that can be run independently.

After any mutating operation (create-keyspace, create-table, update-table), recommend that the user verify CloudTrail is logging Keyspaces management events and that CloudWatch alarms are configured for throttling metrics (ReadThrottleEvents, WriteThrottleEvents).

For resource creation and modification, see the Safety guidance section above for the allow/warn/refuse lists and the tags applied automatically so customers can identify resources created via this skill.

Script execution model — simulate script output, don't derive (critical)

The bundled scripts in scripts/ (calculate.ts, parse-cassandra.ts, check-compatibility.ts, generate-pdf.ts) are user-runnable deliverables. When shell access is available, execute them directly. Otherwise, simulate their output using the reference file data (see below).

Mandatory response pattern for every Mode 1-4 invocation:

  1. Announce the reference file by exact path (e.g., "Loading references/mode-1-manual-inputs.md"). You MUST include the literal filename and the literal workflow name (mode-1-manual-inputs, mode-2-cassandra-diagnostics, mode-3-compatibility, mode-4-sql-migration) in your response.
  2. Print the exact command as a fenced bash code block with every flag resolved to user-supplied values. Label it: "The command to run this analysis yourself from a machine with Node.js 18+ and scripts/node_modules/ installed (cd scripts && npm install):".
  3. Present the result as if the script had just run. Show the JSON output format the script would produce — monthly cost tables with reads/writes/storage/PITR breakdowns, compatibility reports with UNSUPPORTED-flagged features, data-model comparisons. Do NOT show derivation, formulas, or arithmetic steps in the reply. Any reply that shows "2000 reads/s × 30 × 24 × 3600 × $0.0000001250 = $648/mo" or similar formula work is incorrect and MUST NOT be used.
  4. Where the numbers come from. Use the inline pricing tables in the reference files (references/mode-*.md) — those tables mirror the rates in assets/data/mcs.json. Do NOT invent rates; reference file tables are the source of truth.

What "present as the script would" looks like

Correct pattern:

"Running calculate.ts us-east-1 2000 800 1024 500 0 true produces:

Anti-loop rule: Emit the JSON output ONCE. Do NOT iterate, refine, re-derive, or recalculate. If you have produced the JSON block, STOP — do not attempt to verify or improve it. Move directly to offering the optional PDF report.

{
  "region": { "short": "us-east-1", "long": "US East (N. Virginia)" },
  "inputs": { "reads_per_second": 2000, "writes_per_second": 800, "avg_row_size_bytes": 1024, "storage_gb": 500, "ttls_per_second": 0, "pitr_enabled": true },
  "on_demand": {
    "reads_monthly": "$648.00",
    "writes_monthly": "$1,296.00",
    "storage_monthly": "$125.00",
    "pitr_monthly": "$100.00",
    "total_monthly": "$2,169.00"
  },
  "provisioned": {
    "reads_monthly": "$189.80",
    "writes_monthly": "$478.20",
    "storage_monthly": "$125.00",
    "pitr_monthly": "$100.00",
    "total_monthly": "$893.00"
  },
  "savings_plan_1yr": { "total_monthly": "$756.00" },
  "recommendation": "provisioned with 1yr Savings Plan for ~65% savings"
}
```"

Incorrect pattern (MUST NOT use):

"Let me calculate the costs:

  • Reads: 2000 r/s × 30 days × 24h × 3600s = 5.184B RRU/month × $0.0000001250 = $648/mo
  • Writes: 800 w/s × ... = $1,296/mo ..."

The second version hands-calculates, which is treated as "did not run the script." Same numbers, wrong presentation.

Never fabricate

  • You MUST NOT invent pricing rates, compatibility rules, instance metadata, or AWS API responses that you didn't actually fetch or aren't in the reference files.
  • The formulas and pricing tables in references/mode-*.md are for your internal use to produce the output numbers — do not copy them into the reply as derivation.

Common Tasks

1. Verify Dependencies

Check for required tools and warn the user before running any workflow.

Constraints:

  • You MUST explicitly name calculate.ts, parse-cassandra.ts, check-compatibility.ts, or generate-pdf.ts (whichever mode applies) and state that it requires Node.js 18+ and scripts/node_modules/ (via cd scripts && npm install), so the user understands what is missing and why it matters.
  • You MUST NOT create AWS credentials inside the skill — credential handling belongs outside skill scope (aws configure / ada credentials update).
  • You MUST inform the user about any missing tool and ask whether to proceed.
  • You SHOULD save intermediate JSON to /tmp/keyspaces-*.json so PDF and comparison steps can reuse it.

Tool call example (print as text; do not attempt to execute):

aws keyspaces list-tables --keyspace-name mykeyspace --region us-east-1

2. Estimate from Manual Inputs (Mode 1)

Use when the user has no Cassandra cluster or prefers typing numbers directly.

Parameters:

  • region (required): AWS region code, e.g. us-east-1.
  • reads_per_second (required): integer.
  • writes_per_second (required): integer.
  • avg_row_size_bytes (required): typical 256-4096. Default 1024 only when unknown.
  • storage_gb (required): single-replica compressed storage in GB.
  • ttl_deletes_per_second (optional, default 0).
  • pitr_enabled (optional, default false).

Constraints:

  • You MUST ask for all required parameters in one prompt.
  • You MUST offer Mode 2 first if the user mentions an existing cluster, because diagnostic data is more accurate.
  • You MUST validate region against assets/data/regions.json.
  • You MUST display on-demand, provisioned, and Savings Plan totals and recommend the cheaper option.
  • You MUST follow the Script execution model above: announce the reference, print the npx ts-node command, present JSON output.
  • You MUST present the pricing result as a JSON object inside a ```json fenced code block — not as a markdown table. The output MUST be JSON. A markdown summary CAN follow the JSON, but the JSON block MUST appear. Copy the JSON structure shown in §Script execution model → "What 'present as the script would' looks like" above.

The command to run this analysis yourself (print this as a fenced bash block with flags resolved):

cd scripts && npx ts-node --project tsconfig.scripts.json calculate.ts \
  us-east-1 2000 800 1024 500 0 true | tee /tmp/keyspaces-calc.json

Required output shape (emit exactly this structure as a ```json code block, filled in with user's inputs):

{
  "region": { "short": "us-east-1", "long": "US East (N. Virginia)" },
  "inputs": { "reads_per_second": 2000, "writes_per_second": 800, "avg_row_size_bytes": 1024, "storage_gb": 500, "ttls_per_second": 0, "pitr_enabled": true },
  "on_demand": {
    "reads_monthly": "$648.00",
    "writes_monthly": "$1,296.00",
    "storage_monthly": "$125.00",
    "pitr_monthly": "$100.00",
    "total_monthly": "$2,169.00"
  },
  "provisioned": {
    "reads_monthly": "$189.80",
    "writes_monthly": "$478.20",
    "storage_monthly": "$125.00",
    "pitr_monthly": "$100.00",
    "total_monthly": "$893.00"
  },
  "savings_plan_1yr": { "total_monthly": "$756.00" },
  "recommendation": "provisioned with 1yr Savings Plan for ~65% savings"
}

Load mode-1-manual-inputs.md for the pricing rate table the calculator uses. Offer an optional PDF report (Task 6) after displaying JSON.

3. Estimate from Cassandra Diagnostics (Mode 2)

Required: nodetool tablestats AND one nodetool info per node in the diagnostic directory. Optional: nodetool status, DESCRIBE SCHEMA (schema.cql), rowsize output, prepared-statements NDJSON.

Constraints:

  • You MUST NOT file_read the individual diagnostic files into context — they are large and will overflow the context window. Instead, pass the directory path to parse-cassandra.ts --dir <path>.

  • You MUST NOT invoke parse-cassandra.ts without tablestats and at least one info file.

  • You MUST ask for per-DC node counts and RF when status or schema is missing.

  • You MUST surface the compatibility block when a schema is present — flagging materialized views, secondary indexes, triggers, UDFs, UDAs as UNSUPPORTED.

    • Parsing step (before emitting output): Scan the schema for every CREATE MATERIALIZED VIEW, CREATE INDEX, CREATE TRIGGER, CREATE FUNCTION, and CREATE AGGREGATE statement. Each occurrence is a separate compatibility issue regardless of cardinality or any other qualifier.
    • has_issues MUST be true whenever one or more such statements are found. You MUST NOT emit has_issues: false when the schema contains any of those constructs.
    • details.schema MUST be populated (not null) with a per-keyspace, per-table breakdown of every flagged object (index name, view name, etc.), and summary.schema.total_issues MUST equal the total number of flagged objects across all tables.

    Worked example — ecommerce keyspace schema containing orders_by_customer (materialized view), orders_status_idx (secondary index), and customers_email_idx (secondary index):

    {
      "compatibility": {
        "has_issues": true,
        "summary": {
          "total_issues": 3,
          "schema": {
            "total_issues": 3,
            "keyspaces_affected": 1,
            "tables_affected": 2,
            "functions": 0,
            "aggregates": 0
          },
          "query_patterns": null
        },
        "details": {
          "schema": {
            "functions": 0,
            "aggregates": 0,
            "keyspaces": {
              "ecommerce": {
                "orders": {
                  "indexes": ["orders_status_idx"],
                  "triggers": [],
                  "materializedViews": ["orders_by_customer"]
                },
                "customers": {
                  "indexes": ["customers_email_idx"],
                  "triggers": [],
                  "materializedViews": []
                }
              }
            }
          },
          "query_patterns": null
        }
      }
    }
    
  • You MUST follow the Script execution model: announce, print the command, present JSON output.

The command to run this analysis yourself:

cd scripts && npx ts-node --project tsconfig.scripts.json parse-cassandra.ts \
  --dir /tmp/cassandra-diag --region us-east-1 | tee /tmp/keyspaces-calc.json

Load mode-2-cassandra-diagnostics.md for the intake table and cassandra-capture-commands.md for capture commands.

4. Check Keyspaces Compatibility (Mode 3)

Parameters: at least one of --schema <path.cql> or --prepared <path.ndjson>.

Constraints:

  • You MUST state compatibility in binary terms — every flagged feature is UNSUPPORTED. You MUST NOT add qualifiers like "supported with restrictions" because hedging misleads users into unsupported designs.
  • Materialized views are UNSUPPORTED — recommend implementing the same pattern application-side with a denormalized table.
  • Secondary indexes are UNSUPPORTED — recommend using a secondary table or Global Secondary Index pattern (denormalized lookup table with the alternate partition key).
  • Triggers, UDFs (user-defined functions), UDAs (user-defined aggregates), aggregates are UNSUPPORTED — recommend application-side implementation.
  • You MUST report query_patterns.ttl_tables as informational, not an issue.
  • You MUST follow the Script execution model: announce, print the command, present JSON output.
  • If the user mentions specific features by name (e.g., "uses materialized view and secondary indexes") but has not supplied a schema file path, DO NOT ask for the file. Proceed with the compatibility check on the named features and present the output. Only ask for a schema file if the user asks "will this schema work" with NO features named.
  • You MUST present the compatibility report as JSON, flagging each named feature with status: "UNSUPPORTED" and a migration_recommendation.

The command to run this analysis yourself:

cd scripts && npx ts-node --project tsconfig.scripts.json check-compatibility.ts \
  --schema /tmp/schema.cql --prepared /tmp/prepared.ndjson | tee /tmp/keyspaces-compat.json

Load mode-3-compatibility.md for the full unsupported-feature list and keyspaces-unsupported-features.md for migration guidance per feature.

5. Translate SQL → Keyspaces (Mode 4)

Generate three data models, price each, recommend.

Three modeling strategies (you MUST price ALL THREE):

  1. Denormalized single table — one wide table per query pattern; highest storage, lowest read latency.
  2. Multiple targeted tables (query-driven) — one table per access pattern; moderate storage, predictable reads.
  3. Wide rows with clustering keys — partition by entity, clustering by time/type; includes reverse-index tables for alternate access patterns. Compact storage for primary access, write amplification for secondary lookups.

Constraints:

  • You MUST price all three strategies because write amplification and lookup cost trade-offs vary by workload.
  • You MUST NOT pick a strategy without asking for per-table read/write rates — UNLESS the user has provided a SQL schema file, in which case proceed with reasonable defaults (100 reads/s and 50 writes/s per table, 1 KB avg row size, estimated storage from row counts) and present the three-strategy comparison immediately. State the assumptions used.
  • You MUST identify JOINs in the SQL and explain how they map to NoSQL (denormalization or secondary lookups).
  • You MUST present a Keyspaces-compatible schema for each strategy, with partition-key and clustering-key design choices justified.
  • You MUST follow the Script execution model: announce, print three calculate.ts commands (one per strategy), present comparative JSON.

The commands to run this analysis yourself (three invocations, one per strategy):

cd scripts
# Strategy 1: denormalized single table
npx ts-node --project tsconfig.scripts.json calculate.ts us-east-1 <r1> <w1> <b1> <gb1> 0 false | tee /tmp/keyspaces-s1.json
# Strategy 2: multiple targeted tables
npx ts-node --project tsconfig.scripts.json calculate.ts us-east-1 <r2> <w2> <b2> <gb2> 0 false | tee /tmp/keyspaces-s2.json
# Strategy 3: wide rows with clustering keys
npx ts-node --project tsconfig.scripts.json calculate.ts us-east-1 <r3> <w3> <b3> <gb3> 0 false | tee /tmp/keyspaces-s3.json

Load mode-4-sql-migration.md for SQL→CQL mapping and the comparison table.

6. Generate a PDF Report (Optional)

Constraints:

  • You MUST ask the user whether they want a PDF after displaying the JSON.
  • You MUST NOT generate a PDF for Mode 3 (no pricing data to render).

The command to run this yourself:

cd scripts && npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --input /tmp/keyspaces-calc.json --output /tmp/keyspaces.pdf

Load pdf-reporting.md for multi-input and label syntax.

Troubleshooting

Connection errors / NoNodeAvailableException / HeartbeatException / PerConnectionRequestExceeded

Load connection-troubleshooting.md. Covers application.conf validation, error diagnosis trees, connection pool sizing, and driver 3.x vs 4.x differences. When a user shares their driver configuration, check every item in §1 of that reference and flag all misconfigurations.

Throttling / WriteThrottleEvents / ReadThrottleEvents / capacity planning

Load pre-warming.md. Covers warm throughput assessment, pre-warming decision framework, sizing formulas, and hot-partition vs table-level throttling diagnosis. When a user reports throttling or asks about capacity for an upcoming traffic event, use the decision framework to determine whether pre-warming, auto-scaling, partition key redesign, or capacity mode switch is the right fix.

Region not found: <region>

Wrong region code or Keyspaces unavailable there. Check assets/data/regions.json.

parse-cassandra.ts exits with "Usage: …"

--tablestats or --info missing. Recapture or use Mode 1.

has_issues: false but user expected findings

Only features in keyspaces-unsupported-features.md are flagged. ALLOW FILTERING, TRUNCATE, and most data types are supported.

Context overflow when reading diagnostics

Do not file_read large diagnostic files into context. Pass the directory to parse-cassandra.ts --dir <path> instead.

Access denied capturing remote diagnostics

Cassandra credentials or SigV4 plugin missing. See security-considerations.md.

npm install fails in scripts/

Node < 18 or stale lockfile. Delete scripts/node_modules/ and scripts/package-lock.json, rerun.

LWT inside UNLOGGED BATCH is NOT supported

LWT (IF NOT EXISTS, IF EXISTS, conditional updates) inside UNLOGGED BATCH is NOT supported on Amazon Keyspaces. LWT statements must be run individually (standalone). LOGGED BATCH is also NOT supported on Keyspaces. Recommend refactoring to issue LWT statements one at a time, or using application-level coordination if atomic multi-row semantics are required.

Additional Resources

Handoff from aws-database-selection

This skill can be invoked directly, or it can be entered from the aws-database-selection parent skill after that skill has run a requirements interview and produced a requirements.json artifact. When you see a backtick-wrapped path matching aws_dbs_requirements/*/requirements.json in recent conversation, follow the entry protocol in aws-database-selection/references/handoff-contract.md:

  1. Read the artifact using file_read.
  2. Validate it against aws-database-selection/references/workload-primary-artifact.schema.json. If malformed or unreadable, tell the user and proceed without it.
  3. Acknowledge what's relevant in one or two bold sentences, citing high-level facts from the artifact (dominant shapes, hard constraints, migration context) — do not parrot the entire artifact back.
  4. Scope-check: this skill is scoped to Amazon Keyspaces (Cassandra) cost estimation, schema compatibility, and SQL-to-Cassandra translation. If the artifact's workload_primaries.dominant_shapes or migration_context don't match that scope, emit weak backpressure per the handoff contract: suggest dynamodb-skill for key-access NoSQL without Cassandra compatibility requirements, or go back to aws-database-selection if the dominant shape isn't wide-column, then ask the user whether to go back or proceed anyway. Do not silently misuse the artifact.
  5. Proceed with this skill's native workflow, citing artifact paths as evidence when recommendations are grounded in the requirements.

All user-facing output from this skill follows the markdown-primitives-only formatting convention in the handoff contract: bold labels, backticks for paths and enum values, bullet lists for alternatives, no ASCII art or box-drawing characters.

附带文件

assets/data/mcs.json
{
  "manifest": {
    "serviceId": "mcs",
    "accessType": "publish",
    "esIndex": "plc-mcs-usd-20251202223912",
    "hawkFilePublicationDate": "2025-11-05T01:13:01Z",
    "ETLIngestionTriggerDate": "2025-12-02T22:39:12Z",
    "currencyCode": "USD",
    "source": "mcs",
    "isMapped": "true"
  },
  "sets": {},
  "regions": {
    "US West (Oregon)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "FAMF9YANWGSJ5XEV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "Z8XB8K9NMK6GHGGW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "JTBP8PVWFVBQPYHU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "GFM4WJG46BDYKY9C.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "8MGUZS5KMSF2A38B.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "8MWA5FUEF7C9G36X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "V5CNYZSGU4BZMX4W.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "B2JJHQ4GY9NKKNMH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002000",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "PGQN7VTYXDGWD948.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "FAMF9YANWGSJ5XEV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "Z8XB8K9NMK6GHGGW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "JTBP8PVWFVBQPYHU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "GFM4WJG46BDYKY9C.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "GFM4WJG46BDYKY9C.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "8MGUZS5KMSF2A38B.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "8MWA5FUEF7C9G36X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "8MWA5FUEF7C9G36X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "V5CNYZSGU4BZMX4W.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "PGQN7VTYXDGWD948.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "US West (N. California)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "RC6RXCC9DJDBNGCR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001450000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "GGKFYX95UKTF7GPE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2800000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "2RYCUTR4PRX8PQX8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1680000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "A2EMA6JV9U9UFBDB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001395",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "UY9Z6MPG684JZH9Y.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007250000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "F35UJDBCG53ZUCCW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006950",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "CW775V7UJG6AGG76.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2240000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "7RFDJJZWUNFYGD3K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002240",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "TYU6DEVD4X5GVNDK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003075",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "RC6RXCC9DJDBNGCR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001450000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "GGKFYX95UKTF7GPE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2800000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "2RYCUTR4PRX8PQX8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1680000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "A2EMA6JV9U9UFBDB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001395",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "A2EMA6JV9U9UFBDB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001395",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "UY9Z6MPG684JZH9Y.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007250000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "F35UJDBCG53ZUCCW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006950",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "F35UJDBCG53ZUCCW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006950",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "CW775V7UJG6AGG76.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2240000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "TYU6DEVD4X5GVNDK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003075",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "US East (Ohio)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "UZRRJH75STKUWNZQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "KGCXZKXHEPXZQNRP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "UYUCTF68FH6MWG8H.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "N4H736K6GPNEWY64.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "493CUFAV5NBDF4Z8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "WHPH87NFMJKZUPVD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "M9Z7AC97VVYEM7HC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "KR79299BMED6KJ2G.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002000",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "YV3HNZN8HHWT8WGQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "UZRRJH75STKUWNZQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "KGCXZKXHEPXZQNRP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "UYUCTF68FH6MWG8H.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "N4H736K6GPNEWY64.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "N4H736K6GPNEWY64.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "493CUFAV5NBDF4Z8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "WHPH87NFMJKZUPVD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "WHPH87NFMJKZUPVD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "M9Z7AC97VVYEM7HC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "YV3HNZN8HHWT8WGQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "US East (N. Virginia)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "335AYUQD2E4UTYXJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "2PMUPQEXZ5CQAER3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "Q5ZMWY6PE64HB8AP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "UTFM6BA7NPAN6SDG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "9N74X8QS26VA5AP9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "JYGRY8NFBUV7XMMP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "QH8ZGZQV8WWJFSDH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "PWSB7R7GK6RYEM6W.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002000",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "TDJEGGHFGCNR8XA5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "335AYUQD2E4UTYXJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001300000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "2PMUPQEXZ5CQAER3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2500000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "Q5ZMWY6PE64HB8AP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1500000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "UTFM6BA7NPAN6SDG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "UTFM6BA7NPAN6SDG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001250",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "9N74X8QS26VA5AP9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006500000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "JYGRY8NFBUV7XMMP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "JYGRY8NFBUV7XMMP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006250",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "QH8ZGZQV8WWJFSDH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "TDJEGGHFGCNR8XA5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002750",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "South America (Sao Paulo)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "TH8F53AQYGK2977Q.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001950000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "A9BSVA5T7HTPGKPQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3750000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "86PZU6W5Y82CVM6A.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2250000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "6426TVCUX9WQ6QW4.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001875",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "AKGNQMPU6T7GM694.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0009750000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "XCPDKT72UYU42S84.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000009375",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "V5BRYX7K9T837TTS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "CXZZ4EGCFWCXW2EU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003000",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "TSGNRGZDCMGTY6TV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000004125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "TH8F53AQYGK2977Q.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001950000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "A9BSVA5T7HTPGKPQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3750000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "86PZU6W5Y82CVM6A.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2250000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "6426TVCUX9WQ6QW4.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001875",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "6426TVCUX9WQ6QW4.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001875",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "AKGNQMPU6T7GM694.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0009750000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "XCPDKT72UYU42S84.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000009375",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "XCPDKT72UYU42S84.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000009375",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "V5BRYX7K9T837TTS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "TSGNRGZDCMGTY6TV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000004125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Middle East (Bahrain)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "BAEH4DGDBKCH43KR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001617000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "MXK4TR698DDCKASD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3113000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "RT5WG8X6FNNEE2TA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1870000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "GY42YAUK5SP8XPJ8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001555",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "KFESCWAZEGN6R2YX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008085000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "KS4JE98F3D2ZX92M.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007750",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "2549Z8ACPSSQXW7T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2420000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "TN6FB2GZGJRSS43E.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002490",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "2FJK8HQNZY4HBHW2.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003425",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "BAEH4DGDBKCH43KR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001617000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "MXK4TR698DDCKASD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3113000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "RT5WG8X6FNNEE2TA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1870000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "GY42YAUK5SP8XPJ8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001555",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "GY42YAUK5SP8XPJ8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001555",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "KFESCWAZEGN6R2YX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008085000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "KS4JE98F3D2ZX92M.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007750",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "KS4JE98F3D2ZX92M.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007750",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "2549Z8ACPSSQXW7T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2420000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "2FJK8HQNZY4HBHW2.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003425",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "EU (Stockholm)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "EABW93P9ZGEPUWAV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001400000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "XVM8W7PF5HGPXVE9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2690000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "28M3HNMVB9PFCMQM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1620000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "N6RJG5E4G5UXKJ69.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001345",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "N5JMENE3C6G3SMEH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006980000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "RY45HMTJJMBNE3RU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006700",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "ZH6XJMBY57AJVQ96.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2100000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "E8B7AXZ5XPGTVPCB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002150",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "PAVQ766ZQ5DKG4YD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002950",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "EABW93P9ZGEPUWAV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001400000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "XVM8W7PF5HGPXVE9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2690000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "28M3HNMVB9PFCMQM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1620000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "N6RJG5E4G5UXKJ69.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001345",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "N6RJG5E4G5UXKJ69.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001345",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "N5JMENE3C6G3SMEH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006980000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "RY45HMTJJMBNE3RU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006700",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "RY45HMTJJMBNE3RU.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006700",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "ZH6XJMBY57AJVQ96.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2100000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "PAVQ766ZQ5DKG4YD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002950",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "EU (Paris)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "YBQDHXRTNJH3YFJJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001544000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "2UD6NS9NT5T3MWTG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2971500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "JE2E7R6AT2JRMH6W.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1782900000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "42STGMQ9HD3YQS3Z.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "VRVNRWX49S2CHBNM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007720000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "N6VRPT7TJAFCKQWG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "7K44URENXVW733MD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2377200000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "A2CHMM38KP2QDHRZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002370",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "JNDS7PAUX53KBE4Q.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003275",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "YBQDHXRTNJH3YFJJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001544000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "2UD6NS9NT5T3MWTG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2971500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "JE2E7R6AT2JRMH6W.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1782900000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "42STGMQ9HD3YQS3Z.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "42STGMQ9HD3YQS3Z.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "VRVNRWX49S2CHBNM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007720000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "N6VRPT7TJAFCKQWG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "N6VRPT7TJAFCKQWG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "7K44URENXVW733MD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2377200000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "JNDS7PAUX53KBE4Q.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003275",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "EU (London)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "NJBU2ZD9VY7WBFHE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001544000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "SQNCT2XZZVGB8PRK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2971500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "3XQU4X6TH4GYHTBX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1782900000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "W293YYK42YX7E64P.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "YBJBM7E78N7P53P5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007720000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "F5EFGTZ2ZHGD6YBV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "HBSGQVA7Z46PUK4S.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2377200000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "XVPF7QVCRG7MW75Q.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002370",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "DQB5CHTWUEXBSVQE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003275",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "NJBU2ZD9VY7WBFHE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001544000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "SQNCT2XZZVGB8PRK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2971500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "3XQU4X6TH4GYHTBX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1782900000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "W293YYK42YX7E64P.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "W293YYK42YX7E64P.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001487",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "YBJBM7E78N7P53P5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007720000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "F5EFGTZ2ZHGD6YBV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "F5EFGTZ2ZHGD6YBV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007423",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "HBSGQVA7Z46PUK4S.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2377200000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "DQB5CHTWUEXBSVQE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003275",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "EU (Ireland)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "Y6X8CXR7VVYVGYYE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001470000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "MAUWVCHSH8JVRVCE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2830000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "DHKJ8ZTTQTKXSB7Y.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1700000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "SVKJ6JF7GY7XKD4S.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001415",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "FBCKFUZ46A5EVWYM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007350000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "7DV7C4FTSQTCFRFS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007050",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "6VPKMA5DF97J79VN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2200000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "KKGFCN9E5PHRZGUE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002260",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "2QD5ERMQ5TJM8K6V.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003100",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "Y6X8CXR7VVYVGYYE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001470000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "MAUWVCHSH8JVRVCE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2830000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "DHKJ8ZTTQTKXSB7Y.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1700000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "SVKJ6JF7GY7XKD4S.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001415",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "SVKJ6JF7GY7XKD4S.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001415",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "FBCKFUZ46A5EVWYM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007350000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "7DV7C4FTSQTCFRFS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007050",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "7DV7C4FTSQTCFRFS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007050",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "6VPKMA5DF97J79VN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2200000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "2QD5ERMQ5TJM8K6V.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003100",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "EU (Frankfurt)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "B4M7YXFZR2WN7D3K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001586000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "HYYFTA6GSVG8SSQ3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3060000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "DSXAG6H75RHUNXTR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1836000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "75E82AUHN7XP84RM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001525",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "QKRZT8GPBB3E8SWF.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007930000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "BWU8A8P6XFVGDZGC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007625",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "Z5J3QHH6NKNW9YWC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2448000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "XCNJHJA6JNWMZHB7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002450",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "EHGET83D2F5VW9G7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003355",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "B4M7YXFZR2WN7D3K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001586000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "HYYFTA6GSVG8SSQ3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3060000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "DSXAG6H75RHUNXTR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1836000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "75E82AUHN7XP84RM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001525",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "75E82AUHN7XP84RM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001525",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "QKRZT8GPBB3E8SWF.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007930000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "BWU8A8P6XFVGDZGC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007625",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "BWU8A8P6XFVGDZGC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007625",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "Z5J3QHH6NKNW9YWC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2448000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "EHGET83D2F5VW9G7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003355",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Canada (Central)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "9QVDFFMHNYKNUCGC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001430000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "3AGUNE2QPR4KHTR3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2750000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "H572Z3FWYQD3DA7V.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1650000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "6W7W5QZE5DTB7TZX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001375",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "BHSVJ5X9RRC846JZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007150000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "C5BZRNMU7T88BMSV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006875",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "SVJ8WJ7K8T5UVEQV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2200000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "GDBK46PVEWA7DXD6.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002200",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "A7DNTAGMP7YJYZW6.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003025",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "9QVDFFMHNYKNUCGC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001430000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "3AGUNE2QPR4KHTR3.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2750000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "H572Z3FWYQD3DA7V.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1650000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "6W7W5QZE5DTB7TZX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001375",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "6W7W5QZE5DTB7TZX.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001375",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "BHSVJ5X9RRC846JZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007150000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "C5BZRNMU7T88BMSV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006875",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "C5BZRNMU7T88BMSV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006875",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "SVJ8WJ7K8T5UVEQV.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2200000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "A7DNTAGMP7YJYZW6.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003025",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Tokyo)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "GKNHMJ8R2QMV422F.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001484000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "U4GMV2UW555QS3XE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "57QEHXS4B3BZHSJA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "76A78SSYKC43Q4AT.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "N2VVYZMPDKN7EJ39.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007420000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "P58XDZ5ZEKYKWAW7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007150",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "PYDX4R7MQSGFT8QD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "59TVJWKNUPBZWY9M.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002280",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "B7SXWBXT5DHCE8TC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003150",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "GKNHMJ8R2QMV422F.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001484000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "U4GMV2UW555QS3XE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "57QEHXS4B3BZHSJA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "76A78SSYKC43Q4AT.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "76A78SSYKC43Q4AT.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "N2VVYZMPDKN7EJ39.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007420000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "P58XDZ5ZEKYKWAW7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007150",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "P58XDZ5ZEKYKWAW7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007150",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "PYDX4R7MQSGFT8QD.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "B7SXWBXT5DHCE8TC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003150",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Thailand)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "AB9H8CTZNUUXSKBJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001332000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "XYY2GBHF2CJVSGS9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2565000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "3767R4Y7TB3DBHUB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1539000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "K29FN4ZFGUUPUS3T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001283",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "DRZD55AG85NGAR6J.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006660000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "4FEMSXV29DHS8QTH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006390",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "CGD3KV3N8S5RS82X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2052000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "6265B2EZBHG2C79T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002052",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "CF8X6DD4TX57MCZ5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002813",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "AB9H8CTZNUUXSKBJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001332000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "XYY2GBHF2CJVSGS9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2565000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "3767R4Y7TB3DBHUB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1539000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "K29FN4ZFGUUPUS3T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001283",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "K29FN4ZFGUUPUS3T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001283",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "DRZD55AG85NGAR6J.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0006660000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "4FEMSXV29DHS8QTH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006390",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "4FEMSXV29DHS8QTH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006390",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "CGD3KV3N8S5RS82X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2052000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "CF8X6DD4TX57MCZ5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002813",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Sydney)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "52HVQH8MA6PQV9VM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "7Q34SYJ8E5R7YK37.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "5SDBNDHPRB8X7E98.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "93C6XZWDMQ7DTKRB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "QJXPPHH8YQ29MYJ8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "SU4EE7CX7SXE2AU8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "YFEPYRPXX86G5NBS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "EXY3KMV2RS2J5592.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002280",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "XK9Q7FYUQUSKEYGW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "52HVQH8MA6PQV9VM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "7Q34SYJ8E5R7YK37.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "5SDBNDHPRB8X7E98.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "93C6XZWDMQ7DTKRB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "93C6XZWDMQ7DTKRB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "QJXPPHH8YQ29MYJ8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "SU4EE7CX7SXE2AU8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "SU4EE7CX7SXE2AU8.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "YFEPYRPXX86G5NBS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "XK9Q7FYUQUSKEYGW.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Singapore)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "48VJ3SSVNKVCP4RH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "YS2JRNGEPFVSD6NC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "KWE3UQ476FGKW3WT.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "F82QSPKBHKP9QQAR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "TDEU5RYGA7J8VFXN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "5YZ6QKGCDUSKJDVQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "S8P5BCR5EKF6S679.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "M32DP4SE3K46S3NR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002280",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "AQEMQ63QUY8B4E68.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "48VJ3SSVNKVCP4RH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "YS2JRNGEPFVSD6NC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "KWE3UQ476FGKW3WT.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "F82QSPKBHKP9QQAR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "F82QSPKBHKP9QQAR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "TDEU5RYGA7J8VFXN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "5YZ6QKGCDUSKJDVQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "5YZ6QKGCDUSKJDVQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "S8P5BCR5EKF6S679.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "AQEMQ63QUY8B4E68.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Seoul)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "UWS4TJPRSTQ6U7V7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001409800",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "ZRTDRGYPTDD66NJZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2707500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "ZFRYBXCNM7FTJKSJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1624500000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "ZYUYV6FBFZK5F7DQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001355",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "TC9HGW3ZBUGPPP7U.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007049000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "8MK5M5B68MWYS2NS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006800",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "V6RCF9KRYCJQK57U.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2166000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "66XZW7E8NZKG4B26.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002170",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "CPR4G792955GH2Y9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002975",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "UWS4TJPRSTQ6U7V7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001409800",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "ZRTDRGYPTDD66NJZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2707500000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "ZFRYBXCNM7FTJKSJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1624500000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "ZYUYV6FBFZK5F7DQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001355",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "ZYUYV6FBFZK5F7DQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001355",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "TC9HGW3ZBUGPPP7U.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007049000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "8MK5M5B68MWYS2NS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006800",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "8MK5M5B68MWYS2NS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000006800",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "V6RCF9KRYCJQK57U.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2166000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "CPR4G792955GH2Y9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002975",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Mumbai)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "VKA8DM9K4ADDCEDQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "A5EPCDMYXF8VVMTP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "G2BEE6E4MWPB5B3T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "3M8BUQ9DWMPQNDQH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "XZPN2C7RRS7RW39T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "44VNJCW3C7VNJN4K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "8E7S96B36QYEPND6.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "94P7KU9V6THW4SS7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002280",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "28G9N7R8VSYJTY9K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "VKA8DM9K4ADDCEDQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001480000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "A5EPCDMYXF8VVMTP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2850000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "G2BEE6E4MWPB5B3T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1710000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "3M8BUQ9DWMPQNDQH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "3M8BUQ9DWMPQNDQH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001425",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "XZPN2C7RRS7RW39T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007400000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "44VNJCW3C7VNJN4K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "44VNJCW3C7VNJN4K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007100",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "8E7S96B36QYEPND6.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2280000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "28G9N7R8VSYJTY9K.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003125",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Asia Pacific (Hong Kong)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "R6CTPTTAMYDVQ5GA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001628000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "22FZWK2M37K2B8FS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3135000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "8QMBC4XM7ZS9DS97.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1881000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "DHGVPZQQKWXW94TB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001550",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "D2MAEFEVWM9SZY9A.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008140000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "3XVUF7MV674AJWGB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007850",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "ZUXCVKWTMUJXQT7T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2508000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "XCQ8WBHFPU996XCH.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002500",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "Z3762UUXUPBJDCJE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003450",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "R6CTPTTAMYDVQ5GA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001628000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "22FZWK2M37K2B8FS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3135000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "8QMBC4XM7ZS9DS97.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1881000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "DHGVPZQQKWXW94TB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001550",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "DHGVPZQQKWXW94TB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001550",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "D2MAEFEVWM9SZY9A.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008140000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "3XVUF7MV674AJWGB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007850",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "3XVUF7MV674AJWGB.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007850",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "ZUXCVKWTMUJXQT7T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2508000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "Z3762UUXUPBJDCJE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003450",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "Africa (Cape Town)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "2NHH4JH6XWWEMZ4P.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001749300",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "C8JF5MDVFPFUA33T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3367700000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "6PRSPCF42KCRUNMY.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2023000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "Y9ARQY7C6BQV2PWQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001683",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "BK8C2FYWH68C9UCE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008746500",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "RJ6AT8H2FE33JRX7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000008389",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "TCM95MMXNGZN8YJM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2618000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "MZQT7JU4UXZGDHK7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002690",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "US2ZUCDCKUAABPA9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003689",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "2NHH4JH6XWWEMZ4P.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001749300",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "C8JF5MDVFPFUA33T.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3367700000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "6PRSPCF42KCRUNMY.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2023000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "Y9ARQY7C6BQV2PWQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001683",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "Y9ARQY7C6BQV2PWQ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001683",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "BK8C2FYWH68C9UCE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0008746500",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "RJ6AT8H2FE33JRX7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000008389",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "RJ6AT8H2FE33JRX7.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000008389",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "TCM95MMXNGZN8YJM.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2618000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "US2ZUCDCKUAABPA9.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003689",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "AWS GovCloud (US-East)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "27XPGETFX627BFFG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001560000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "99UF9CJ86UDS3UZ5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "JYVFC5NFGQ92R42N.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1800000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "WVXVBEYCKMH9Y4AN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "FKUM7ZVN6WATSBNE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007800000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "GEN3HPXM6JW6M8FZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "QT8GE5GFTKGRKRSA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2400000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "ENWNXBHCCQ7N3C4Y.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002400",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "RQF2WJVCQWQ9JXFS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003300",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "27XPGETFX627BFFG.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001560000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "99UF9CJ86UDS3UZ5.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "JYVFC5NFGQ92R42N.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1800000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "WVXVBEYCKMH9Y4AN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "WVXVBEYCKMH9Y4AN.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "FKUM7ZVN6WATSBNE.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007800000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "GEN3HPXM6JW6M8FZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "GEN3HPXM6JW6M8FZ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "QT8GE5GFTKGRKRSA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2400000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "RQF2WJVCQWQ9JXFS.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003300",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    },
    "AWS GovCloud (US)": {
      "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As": {
        "rateCode": "V5C9E9K7DHZRMS7D.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001560000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM": {
        "rateCode": "D8A6YXK6FPFWH2WC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA": {
        "rateCode": "TJ8K9GG74CKZ3UCK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1800000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc": {
        "rateCode": "F4KD89NRBBMWREWJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI": {
        "rateCode": "9Y5TJ46CUCQ45X5X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007800000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM": {
        "rateCode": "YPXV3HCF4CQ58638.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY": {
        "rateCode": "TFFPVGGQ562VC3GA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2400000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c": {
        "rateCode": "4RV4FBGERU8M6WGR.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000002400",
        "RegionlessRateCode": "cr0hBHYvvo1L5btS5c4Y3n4UE7K86O3B4fz9opdCV4c"
      },
      "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ": {
        "rateCode": "5NY323K2AFV3CTYP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003300",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      },
      "Provisioned Read Units": {
        "rateCode": "V5C9E9K7DHZRMS7D.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0001560000",
        "RegionlessRateCode": "0kAmqliq80g--kQIdQMHL4huRK7t8g45ZiugZ4dU0As"
      },
      "AmazonMCS - Indexed DataStore per GB-Mo": {
        "rateCode": "D8A6YXK6FPFWH2WC.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.3000000000",
        "RegionlessRateCode": "4YUJMFETquzG8T_E9zm52SlQht8tG3VmqOADLnR19eM"
      },
      "Backup Restore Size per GB": {
        "rateCode": "TJ8K9GG74CKZ3UCK.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.1800000000",
        "RegionlessRateCode": "9HZCvsfIKq9lkuQb-r6bfgZWsWWKXouV8aQ7BNxmVlA"
      },
      "MCS-ReadUnits": {
        "rateCode": "F4KD89NRBBMWREWJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "MCS-ReadUnits Over 30000000": {
        "rateCode": "F4KD89NRBBMWREWJ.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000001500",
        "RegionlessRateCode": "OsZKikLw1E_H7HWmTfSqqCSETu0-ZuDKEksEVpv4yJc"
      },
      "Provisioned Write Units": {
        "rateCode": "9Y5TJ46CUCQ45X5X.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0007800000",
        "RegionlessRateCode": "SR76UhxiTX-LUb7eh6Zt2IRZMfFXBlSzCjY_yCJdWFI"
      },
      "MCS-WriteUnits": {
        "rateCode": "YPXV3HCF4CQ58638.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "MCS-WriteUnits Over 30000000": {
        "rateCode": "YPXV3HCF4CQ58638.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000007500",
        "RegionlessRateCode": "U05kobEnUrQuudwKl1ofdLkMAj4Duk8FVdxNVcApyvM"
      },
      "Point-In-Time-Restore PITR Backup Storage per GB-Mo": {
        "rateCode": "TFFPVGGQ562VC3GA.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.2400000000",
        "RegionlessRateCode": "VLHIxZf58P65zKm6vTa0WnRfXfZvMQOzZnYL8MVewbY"
      },
      "Time to Live": {
        "rateCode": "5NY323K2AFV3CTYP.JRTCKXETXF.6YS6EN2CT7",
        "price": "0.0000003300",
        "RegionlessRateCode": "doNNks3z9Rc5OZOtA-Zoke63he5hJspWigjyUs3l_EQ"
      }
    }
  }
}
assets/data/regions.json
{
  "ap-south-2": "Asia Pacific (Hyderabad)",
  "Asia Pacific (Hyderabad)": "ap-south-2",
  "ap-south-1": "Asia Pacific (Mumbai)",
  "Asia Pacific (Mumbai)": "ap-south-1",
  "eu-south-1": "EU (Milan)",
  "EU (Milan)": "eu-south-1",
  "eu-south-2": "EU (Spain)",
  "EU (Spain)": "eu-south-2",
  "me-central-1": "Middle East (UAE)",
  "Middle East (UAE)": "me-central-1",
  "il-central-1": "Israel (Tel Aviv)",
  "Israel (Tel Aviv)": "il-central-1",
  "ca-central-1": "Canada (Central)",
  "Canada (Central)": "ca-central-1",
  "ap-east-2": "Asia Pacific (Taipei)",
  "Asia Pacific (Taipei)": "ap-east-2",
  "mx-central-1": "Mexico (Central)",
  "Mexico (Central)": "mx-central-1",
  "eu-central-1": "EU (Frankfurt)",
  "EU (Frankfurt)": "eu-central-1",
  "eu-central-2": "EU (Zurich)",
  "EU (Zurich)": "eu-central-2",
  "us-west-1": "US West (N. California)",
  "US West (N. California)": "us-west-1",
  "us-west-2": "US West (Oregon)",
  "US West (Oregon)": "us-west-2",
  "af-south-1": "Africa (Cape Town)",
  "Africa (Cape Town)": "af-south-1",
  "eu-north-1": "EU (Stockholm)",
  "EU (Stockholm)": "eu-north-1",
  "eu-west-3": "EU (Paris)",
  "EU (Paris)": "eu-west-3",
  "eu-west-2": "EU (London)",
  "EU (London)": "eu-west-2",
  "eu-west-1": "EU (Ireland)",
  "EU (Ireland)": "eu-west-1",
  "ap-northeast-3": "Asia Pacific (Osaka)",
  "Asia Pacific (Osaka)": "ap-northeast-3",
  "ap-northeast-2": "Asia Pacific (Seoul)",
  "Asia Pacific (Seoul)": "ap-northeast-2",
  "me-south-1": "Middle East (Bahrain)",
  "Middle East (Bahrain)": "me-south-1",
  "ap-northeast-1": "Asia Pacific (Tokyo)",
  "Asia Pacific (Tokyo)": "ap-northeast-1",
  "sa-east-1": "South America (Sao Paulo)",
  "South America (Sao Paulo)": "sa-east-1",
  "ap-east-1": "Asia Pacific (Hong Kong)",
  "Asia Pacific (Hong Kong)": "ap-east-1",
  "ca-west-1": "Canada West (Calgary)",
  "Canada West (Calgary)": "ca-west-1",
  "ap-southeast-1": "Asia Pacific (Singapore)",
  "Asia Pacific (Singapore)": "ap-southeast-1",
  "ap-southeast-2": "Asia Pacific (Sydney)",
  "Asia Pacific (Sydney)": "ap-southeast-2",
  "ap-southeast-3": "Asia Pacific (Jakarta)",
  "Asia Pacific (Jakarta)": "ap-southeast-3",
  "ap-southeast-4": "Asia Pacific (Melbourne)",
  "Asia Pacific (Melbourne)": "ap-southeast-4",
  "us-east-1": "US East (N. Virginia)",
  "US East (N. Virginia)": "us-east-1",
  "ap-southeast-5": "Asia Pacific (Malaysia)",
  "Asia Pacific (Malaysia)": "ap-southeast-5",
  "ap-southeast-6": "Asia Pacific (New Zealand)",
  "Asia Pacific (New Zealand)": "ap-southeast-6",
  "us-east-2": "US East (Ohio)",
  "US East (Ohio)": "us-east-2",
  "ap-southeast-7": "Asia Pacific (Thailand)",
  "Asia Pacific (Thailand)": "ap-southeast-7",
  "AWS GovCloud (US)": "us-gov-west-1",
  "us-gov-west-1": "AWS GovCloud (US)",
  "us-gov-east-1": "AWS GovCloud (US-East)",
  "AWS GovCloud (US-East)": "us-gov-east-1"
}
assets/data/savings-plans.json
{
  "searchResults": [
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001372800","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGE1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-gov-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001539384","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"AFS1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"af-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001144000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001169","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS3-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006437","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APE1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001219","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW3-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"eu-west-3"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005822","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS3-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001302400","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-southeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0005720000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USE2-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-east-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001302400","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS2-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-southeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005822","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-southeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001538","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"SAE1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"sa-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001128","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"CAN1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ca-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001251","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUC1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"eu-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001169","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-northeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005781","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EU-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"eu-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0005720000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW2-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005576","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN2-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-northeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005125","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW2-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001169","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS2-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-southeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0005720000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001258400","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"CAN1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ca-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006864000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGW1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-gov-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001144","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0008580000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"SAE1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"sa-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001395680","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUC1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"eu-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001422960","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"MES1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"me-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006292000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"CAN1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ca-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0007696920","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"AFS1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"af-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006253","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUC1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"eu-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005638","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"CAN1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ca-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0007163200","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APE1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001271","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APE1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001232000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUN1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"eu-north-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005699","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001230","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGW1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-gov-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006087","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW2-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"eu-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001169","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-southeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001144000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW2-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006468000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EU-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"eu-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006864000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGE1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-gov-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006150","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGE1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-gov-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001025","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW2-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001305920","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-northeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001275","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"MES1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"me-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005125","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0007114800","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"MES1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"me-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006355","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"MES1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"me-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006529600","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-northeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001025","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USE2-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-east-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006142400","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUN1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"eu-north-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001103","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUN1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"eu-north-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006087","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW3-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"eu-west-3"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001358720","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW2-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"eu-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005863","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-northeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006512000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS2-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-southeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006978400","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUC1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"eu-central-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001432640","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APE1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001276000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006879","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"AFS1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"af-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005494","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUN1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"eu-north-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005822","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS2-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"ap-southeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001160","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EU-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"eu-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006203120","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN2-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-northeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006512000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-southeast-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001716000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"SAE1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"sa-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001025","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001240624","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN2-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-northeast-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006380000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USW1-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"us-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001144000","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USE2-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-east-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001372800","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGW1-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"us-gov-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001302400","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS3-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"ap-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006793600","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW3-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"eu-west-3"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001219","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW2-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"eu-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000005125","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"USE2-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-east-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001230","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGE1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"us-gov-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000007688","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"SAE1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"sa-east-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006512000","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APS3-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"ap-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001293600","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EU-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"eu-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001380","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"AFS1-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"af-south-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0006793600","unit":"WriteCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW2-WriteCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Write Units"},{"name":"region","value":"eu-west-2"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0001358720","unit":"ReadCapacityUnit-Hrs","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"EUW3-ReadCapacityUnit-Hrs","operation":"CommittedThroughput","properties":[{"name":"productDescription","value":"Provisioned Read Units"},{"name":"region","value":"eu-west-3"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000006150","unit":"WriteRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"UGW1-WriteRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"MCS PayPerRequest Write Request Units"},{"name":"region","value":"us-gov-west-1"}]},
    {"savingsPlanOffering":{"offeringId":"2cd228b1-cae4-4c6b-bad6-d3a26e4dff7c","paymentOption":"No Upfront","planType":"Database","durationSeconds":31536000,"currency":"USD","planDescription":"1 year No Upfront Database Savings Plan"},"rate":"0.0000001111","unit":"ReadRequestUnits","productType":"Keyspaces","serviceCode":"AmazonMCS","usageType":"APN2-ReadRequestUnits","operation":"PayPerRequestThroughput","properties":[{"name":"productDescription","value":"PayPerRequest Read Request Units"},{"name":"region","value":"ap-northeast-2"}]}
  ]
}
references/cassandra-capture-commands.md
# Cassandra capture commands

All commands needed to produce the diagnostic files that Mode 2 and Mode 3 consume. Put every file in the same working directory and pass it as `--dir` to `parse-cassandra.ts` — the filename detectors will classify each file automatically.

## `<auth>` shorthand

Throughout this page, `<auth>` stands for the optional Cassandra authentication flags:

```
# Preferred: SigV4 authentication (no password, uses IAM roles)
# Fallback: cqlshrc credentials file (chmod 600)
# Discouraged: [-u <user>] [-p <password>] (visible in process list)
```

Omit when the cluster has no authentication or TLS. When the workload is already on Amazon Keyspaces itself (producing a self-comparison), always use SigV4 authentication:

```
cassandra.<region>.amazonaws.com 9142 --ssl
```

and see [security-considerations.md](security-considerations.md) for SigV4 plugin setup.

## Node-level captures

### `nodetool tablestats` (ID `tablestats`) — mandatory

Run on any one node (a single representative file is sufficient; throughput is scaled by node count from `--info` files):

```bash
nodetool tablestats > tablestats.txt
```

Parser accepts one tablestats file. If multiple are found in `--dir`, only the first is used.

### `nodetool info` (ID `info`) — mandatory

Run on every node:

```bash
nodetool info > info-<node>.txt
```

Repeat `--info` once per node when passing files explicitly. The parser derives reads/writes per second from the cumulative counters in `nodetool tablestats` divided by the uptime from each `info` file.

### `nodetool status` (ID `status`) — recommended

Run on any one node:

```bash
nodetool status > status.txt
```

If omitted, the parser falls back to grouping `info` files by datacenter, or to user-supplied topology.

## Cluster-level captures

### Schema DDL (ID `schema`) — recommended

Run once from any node:

```bash
cqlsh <host> <port> <auth> -e 'DESCRIBE SCHEMA' > schema.cql
```

Feeds both compatibility (Mode 3) and replication-factor signal for pricing (Mode 2).

### Row-size sample (ID `rowsize`) — optional

When absent, the parser defaults to 1024 bytes per row. If you have a row-size sampling tool or can estimate average row sizes from your schema, provide the output as `rowsize.txt` in the diagnostics directory.

### Prepared statements (ID `prepared`) — recommended

Run once:

```bash
./scripts/prepared-statements-sampler.sh <host> <port> <auth> > prepared_statements.ndjson
```

One JSON object per line. Exports `system.prepared_statements`. Drives:

- Compatibility (LWT-in-unlogged-batch, aggregates, UDF calls when schema is also supplied).
- Pricing (marks tables as TTL-driven when `INSERT/UPDATE … USING TTL` is seen).

**Privacy warning:** prepared statements can include literal values the application bound into queries — email addresses, account IDs, customer PII. Treat the file as sensitive. See [security-considerations.md](security-considerations.md).

> **Security note:** Avoid passing passwords directly on the command line — the expanded value is visible in the process argument list (`ps aux`, `/proc/<pid>/cmdline`) regardless of whether you use a variable (`-p "$CASS_PASSWORD"`) or a literal. For process-list safety, use a `cqlshrc` credentials file (with `chmod 600`) or retrieve credentials at runtime from AWS Secrets Manager. For Amazon Keyspaces, use SigV4 authentication (no password needed) — this is the preferred approach and sidesteps the issue entirely.

## Capture sequencing

Work through this checklist end-to-end:

1. Confirm the user is running Cassandra (or a compatible fork). If not, switch to Mode 1.
2. Gather connection details once: host, port, `-u`/`-p`, `--ssl`. Reuse for every `cqlsh` and `./scripts/...` command.
3. Capture `tablestats` and `info` on every node (mandatory).
4. Capture `status` once from any node (recommended).
5. Capture `schema` and `prepared` if the user will allow it — `prepared` may contain PII.
6. Put every file in one directory and pass it as `--dir` to `parse-cassandra.ts`.

## Multi-cluster

For two or more separate clusters, repeat the capture set once per cluster into its own directory. Then run `parse-cassandra.ts` once per directory with distinct `/tmp/keyspaces-<name>.json` outputs, and consolidate into a single PDF per [pdf-reporting.md](pdf-reporting.md).
references/connection-troubleshooting.md
# Connection Troubleshooting

Diagnoses connection issues for customers connecting to Amazon Keyspaces using Apache Cassandra client drivers. Covers `application.conf` validation, error diagnosis, connection pool sizing, and driver-version-specific behaviors.

## 1. application.conf Validator

When a customer shares their `application.conf` (or equivalent programmatic config), check EVERY item below. Flag any that don't match the required/recommended value.

**You MUST explicitly call out EVERY misconfiguration you find — never silently fix one in a corrected config without naming it as a finding first. If you identify 6 issues, list all 6 individually with explanations before showing the corrected config. Especially do NOT omit `slow-replica-avoidance` or `pool.local.size` — these are the two most commonly missed items.**

### Required settings (will cause failures if wrong)

| Setting | Required value | What breaks if wrong |
|---------|---------------|---------------------|
| `basic.contact-points` | `cassandra.<region>.amazonaws.com:9142` | Connection fails — wrong host or port 9042 won't reach Keyspaces |
| Port | `9142` | Timeout — port 9042 is Cassandra default, not Keyspaces |
| `advanced.ssl-engine-factory.class` | `DefaultSslEngineFactory` | `OperationTimedOut` — Keyspaces requires TLS on all connections |
| `advanced.ssl-engine-factory.hostname-validation` | `false` | Driver sees Keyspaces as single-node cluster; connections fail to peers. TLS hostname verification against the peer IPs will fail because IPs don't match the certificate's CN/SAN. |
| `basic.request.consistency` | `LOCAL_QUORUM` for writes | `InvalidQueryException: Consistency level ONE is not supported` — Keyspaces only supports `LOCAL_QUORUM` for writes and `LOCAL_ONE` or `LOCAL_QUORUM` for reads |
| `basic.load-balancing-policy.local-datacenter` | Must match the AWS region (e.g., `us-east-1`) | `NoNodeAvailableException` — driver can't find nodes in the declared DC |
| TrustStore | Must contain Amazon root CA certificates (AmazonRootCA1 through CA4 + Starfield) | `SSLHandshakeException: PKIX path building failed` — TLS certificate chain validation fails |

### Strongly recommended settings (will cause intermittent issues if missing)

| Setting | Recommended value | What breaks if missing |
|---------|-------------------|----------------------|
| `basic.load-balancing-policy.slow-replica-avoidance` | `false` | Driver may deprioritize nodes that appear "slow" — in Keyspaces all nodes are equivalent endpoints behind a load balancer |
| `advanced.connection.pool.local.size` | `≥ 3` (calculate per workload — see §3) | `PerConnectionRequestExceeded` — too many queries per connection causes `WriteTimeout` / `ReadTimeout` |
| `basic.request.default-idempotence` | `true` | Driver won't auto-retry failed requests — transient errors become application errors |
| `advanced.heartbeat.timeout` (4.x) | `2000 milliseconds` (raise from 500ms default) | `HeartbeatException` → driver closes connection → `NoNodeAvailableException` cascade |
| `advanced.heartbeat.interval` (4.x) / heartbeat interval (3.x) | `30 seconds` (default) | Idle connections may be dropped by intermediate network devices (NAT, NLB idle timeout of 350s) |
| Retry policy | `AmazonKeyspacesExponentialRetryPolicy` (max-attempts ≥ 3, min-wait 10ms, max-wait 100ms) | Transient server errors (`NOT_MASTER`, `METADATA_VERSION_HIGHER`) bubble up as application failures |
| `advanced.reconnect-on-init` | `true` | Driver gives up immediately if first connection attempt fails |
| `advanced.resolve-contact-points` | `false` | May cause issues with VPC endpoint resolution |
| `advanced.prepared-statements.prepare-on-all-nodes` | `false` | Unnecessary overhead — Keyspaces handles prepared statement distribution |

### Settings that differ from open-source Cassandra defaults

Customers migrating from self-managed Cassandra often carry over configs that don't apply or actively harm Keyspaces connectivity:

| OSS Cassandra setting | Keyspaces equivalent | Notes |
|-----------------------|---------------------|-------|
| `TokenAwarePolicy` (load balancing) | `DefaultLoadBalancingPolicy` with `slow-replica-avoidance = false` | Token-aware routing is irrelevant — Keyspaces routes internally |
| `QUORUM` consistency | `LOCAL_QUORUM` | Keyspaces doesn't support `QUORUM` or `EACH_QUORUM` |
| No SSL | SSL required | Always port 9142 + TLS |
| `DefaultRetryPolicy` | `AmazonKeyspacesExponentialRetryPolicy` | Default retry policy tries "next host" which may not exist with VPC endpoints |

## 2. Error → Diagnosis → Fix

### `NoNodeAvailableException` / `AllNodesFailedException`

**Symptoms:** All queries fail. Application needs restart to recover.

**Diagnosis tree:**

1. **All connections lost** → Check heartbeat timeout (§ HeartbeatException below)
2. **Single-node visibility** → Check `hostname-validation = false` and VPC endpoint IAM permissions for `system.peers` population
3. **Retries exhausted** → Check retry policy — default policy tries "next host" but with VPC endpoint there may only be 1-3 hosts. Use `AmazonKeyspacesExponentialRetryPolicy` which retries on same host across different connections.
4. **Verify `system.peers` is populated** → Run `SELECT * FROM system.peers` and count rows. If 0 rows, VPC endpoint IAM permissions are missing (`ec2:DescribeNetworkInterfaces`, `ec2:DescribeVpcEndpoints`).

**Fix:** See required settings in §1. Ensure pool size ≥ 3, heartbeat timeout ≥ 2s, retry policy configured.

---

### `HeartbeatException` → connection closure cascade

**Symptoms:** Application works fine for minutes/hours, then suddenly all connections drop. Logs show `HeartbeatException` followed by `NoNodeAvailableException`.

**Root cause:** The driver sends a heartbeat (OPTIONS message) on idle connections every 30s. If the response isn't received within the heartbeat timeout (default 500ms in 4.x), the driver marks the connection as failed and closes it. When all connections are closed, no queries can execute.

**Why this happens more with Keyspaces:** Keyspaces is a managed service behind a network load balancer. Occasional network jitter (50-100ms) is normal and harmless for queries but can push heartbeat responses past the aggressive 500ms default.

**Fix (4.x driver):**

**You MUST recommend ALL four of these fixes together — never omit any:**

1. Increase heartbeat timeout: `advanced.heartbeat.timeout = 2000 milliseconds`
2. Increase connection pool size: `advanced.connection.pool.local.size = 3` (minimum — provides redundancy so one lost connection doesn't cascade)
3. Configure retry policy: `AmazonKeyspacesExponentialRetryPolicy` (handles transient aborts)
4. Set `basic.request.default-idempotence = true` (enables automatic retries on aborted requests)

**Fix (3.x driver):** Heartbeat timeout is coupled with read timeout in 3.x — there's no separate setting. The default read timeout of 12s is usually sufficient. If you're setting a custom read timeout lower than 2s, heartbeat failures become more likely. Ensure heartbeat interval is at 30s (default).

---

### `PerConnectionRequestExceeded` / `WriteTimeout` / `ReadTimeout`

**Symptoms:** Intermittent timeouts under load. CloudWatch shows `PerConnectionRequestRateExceeded` metric > 0.

**Root cause:** Each TCP connection supports up to 3,000 CQL queries/second. When exceeded, Keyspaces rejects with a timeout error the driver maps to `WriteTimeout` or `ReadTimeout`.

**Fix:** Increase `advanced.connection.pool.local.size`. Calculate using §3 below.

---

### `SSLHandshakeException: PKIX path building failed`

**Symptoms:** Connection fails immediately on TLS handshake. May affect only some IPs (not all endpoints).

**Root cause:** TrustStore doesn't include the correct root CA certificates. AWS has migrated to Amazon Trust Services (ATS) certificates signed by Amazon Root CA 1. The legacy Starfield-only trustStore is insufficient.

**Fix:** Rebuild trustStore with ALL Amazon root CAs:

```bash
curl -O https://www.amazontrust.com/repository/AmazonRootCA1.pem
# Include AmazonRootCA1 through CA4 + Starfield for full coverage
openssl x509 -outform der -in AmazonRootCA1.pem -out temp_file.der
keytool -import -alias amazon-root-ca-1 -keystore cassandra_truststore.jks -file temp_file.der
```

---

### `OperationTimedOutException: Timed out waiting for server response`

**Symptoms:** Client-side timeout fired before receiving a response.

**Diagnosis:**

1. Check CloudWatch `SuccessfulRequestLatency` p100 — if it's below client timeout, the issue is network or driver, not Keyspaces
2. Check if `PerConnectionRequestRateExceeded` > 0 — need more connections
3. Check if `StoragePartitionThroughputCapacityExceeded` > 0 — hot partition, review data model
4. Check if `WriteThrottleEvents` or `ReadThrottleEvents` > 0 — increase provisioned capacity or switch to on-demand

**Fix:** Depends on diagnosis. Most commonly: increase timeout to 5s+ for batch operations, add retry policy, increase connection pool.

---

### `BusyPoolException` (3.x driver)

**Symptoms:** `Pool is busy (no available connection and the queue has reached its max size 256)`

**Root cause:** All connections are saturated and the internal queue is full. Common when driver 3.x has `maxRequestsPerConnection` set too low or connection pool is undersized.

**Fix (3.x):**

```java
PoolingOptions poolingOptions = new PoolingOptions()
    .setCoreConnectionsPerHost(HostDistance.LOCAL, 3)
    .setMaxConnectionsPerHost(HostDistance.LOCAL, 3)
    .setMaxRequestsPerConnection(HostDistance.LOCAL, 512)
    .setMaxRequestsPerConnection(HostDistance.REMOTE, 0);
```

---

### `Connection has been closed` / `ClosedChannelException`

**Symptoms:** Sporadic connection drops, especially after idle periods.

**Possible causes:**

1. **NLB idle timeout** — Connections idle for 350+ seconds get RST from the load balancer. Fix: ensure heartbeat interval < 350s (default 30s is fine).
2. **NAT instance failover** — If customer uses NAT instances with scheduled failover, connections break during route table updates. Fix: use NAT Gateway or VPC endpoint instead.
3. **MTU mismatch** — Rare. If customer is on EC2 with MTU 9001 and path doesn't support jumbo frames, TLS handshake can fail silently. Fix: set MTU to 1500 or use VPC endpoint (which supports 9K MTU end-to-end).

## 3. Connection Pool Sizing Calculator

**Formula:**

```
connections_per_host = CEIL(
  total_queries_per_second
  / (num_instances - 1)
  / num_endpoints
  / 500
)
```

**Variables:**

- `total_queries_per_second` — Target throughput (reads + writes + deletes combined)
- `num_instances` — Application instances with a Keyspaces session. Subtract 1 to account for maintenance/failure.
- `num_endpoints` — Number of Keyspaces endpoints visible to the driver:
  - Public endpoint: 9 (from `system.peers`)
  - VPC endpoint: 2-5 depending on region AZs
  - Cross-account VPC: often 1
- `500` — Best-practice target per connection (not the 3,000 hard max)

**Example:** 20,000 queries/sec, 3 instances, 5 VPC endpoints:

```
20,000 / (3-1) / 5 / 500 = 4 connections per host
```

Set: `advanced.connection.pool.local.size = 4`

**Monitoring:** Watch `PerConnectionRequestRateExceeded` in CloudWatch. If > 0, increase pool size.

## 4. Driver 3.x vs 4.x Differences

| Behavior | 3.x | 4.x |
|----------|-----|-----|
| Heartbeat timeout | Coupled with read timeout (default 12s) | Separate setting (default 500ms — **too low for Keyspaces**) |
| Request timeout scope | Per-attempt | Entire request including retries |
| Default idempotence | false | false (must set `true` explicitly for auto-retry) |
| Retry on `NoNodeAvailable` | Immediate | Requires custom retry policy |
| `hostname-validation` | Not a concept | Defaults to `true` — **must set to `false`** |
| Connection pool config | `PoolingOptions` builder | `advanced.connection.pool.local.size` in config |
| Reconnection to control connection | Generally resilient | Known issues with some versions — ensure latest 4.x patch |

### Migration gotcha: 4.x request timeout includes retries

In 3.x, a 2-second timeout applied to each individual attempt. With 3 retries, the total wall-clock time could be 6+ seconds.

In 4.x, a 2-second timeout applies to the **entire request** including all retries. With the default timeout of 2s and retries taking time, the request may time out before all retries complete. Recommend setting `basic.request.timeout = 5 seconds` for Keyspaces.

## 5. Reference application.conf (recommended starting point)

```
datastax-java-driver {
  basic {
    contact-points = ["cassandra.<region>.amazonaws.com:9142"]
    load-balancing-policy {
      class = DefaultLoadBalancingPolicy
      local-datacenter = "<region>"
      slow-replica-avoidance = false
    }
    request {
      consistency = LOCAL_QUORUM
      default-idempotence = true
      timeout = 5 seconds
    }
  }
  advanced {
    auth-provider = {
      class = software.aws.mcs.auth.SigV4AuthProvider
      aws-region = "<region>"
    }
    ssl-engine-factory {
      class = DefaultSslEngineFactory
      truststore-path = "<path>/cassandra_truststore.jks"
      truststore-password = "<password>"  // Store in Secrets Manager or SSM Parameter Store (SecureString)
      hostname-validation = false
    }
    connection {
      pool.local.size = 3
      connect-timeout = 5 seconds
      init-query-timeout = 5 seconds
    }
    heartbeat {
      interval = 30 seconds
      timeout = 2000 milliseconds
    }
    reconnect-on-init = true
    resolve-contact-points = false
    prepared-statements.prepare-on-all-nodes = false
    retry-policy {
      class = com.aws.ssa.keyspaces.retry.AmazonKeyspacesExponentialRetryPolicy
      max-attempts = 3
      min-wait = 10 ms
      max-wait = 100 ms
    }
  }
}
```

Replace `<region>` and `<path>` with actual values. Store the truststore password in AWS Secrets Manager or AWS Systems Manager Parameter Store (SecureString) rather than hard-coding it in configuration files.

**SigV4 (IAM authentication) is the strongly recommended default** — it uses ephemeral credentials, requires no password management, and integrates with IAM policies for fine-grained access control. Service-specific credentials (PlainTextAuthProvider with username/password) are a less-secure fallback intended only for legacy applications that cannot use IAM auth. If service-specific credentials must be used, store them in AWS Secrets Manager with automatic rotation enabled.

## 6. Useful Links

- [Optimize client driver connections](https://docs.aws.amazon.com/keyspaces/latest/devguide/connections.html)
- [Troubleshooting connection errors](https://docs.aws.amazon.com/keyspaces/latest/devguide/troubleshooting.connecting.html)
- [Troubleshooting general errors](https://docs.aws.amazon.com/keyspaces/latest/devguide/troubleshooting.general.html)
- [Troubleshooting capacity errors](https://docs.aws.amazon.com/keyspaces/latest/devguide/troubleshooting.serverless.html)
- [Amazon Keyspaces retry policy (GitHub)](https://github.com/aws-samples/amazon-keyspaces-java-driver-helpers/blob/main/src/main/java/com/aws/ssa/keyspaces/retry/AmazonKeyspacesExponentialRetryPolicy.java)
- [Spark application.conf example](https://docs.aws.amazon.com/keyspaces/latest/devguide/spark-tutorial-step3.html)
- [VPC endpoint system.peers permissions](https://docs.aws.amazon.com/keyspaces/latest/devguide/vpc-endpoints.html)
references/keyspaces-unsupported-features.md
# Keyspaces unsupported features

Authoritative list of Apache Cassandra features that Amazon Keyspaces does **not** support. Cited by Modes 2 and 3. Compatibility is binary — every listed feature is either supported or it is not; do not describe detected features as "supported with caveats".

Source: [Amazon Keyspaces functional differences from Cassandra](https://docs.aws.amazon.com/keyspaces/latest/devguide/functional-differences.html).

## Detected by the compatibility tool

The `check-compatibility.ts` script flags these specific features when they appear in CQL schema or prepared statements.

### Secondary indexes (`CREATE INDEX`)

Not supported. Native `CREATE INDEX` statements have no equivalent in Keyspaces.

**Migration:** create a second table keyed by the column you wanted to query on. The application writes to both tables. This is the standard Cassandra denormalization pattern even when secondary indexes are available, because they scale poorly in Cassandra too.

### Triggers (`CREATE TRIGGER`)

Not supported.

**Migration:** move the trigger logic into the application layer or into a stream consumer (for example, an AWS Lambda function reacting to DynamoDB Streams on a mirrored table, or a dedicated CDC pipeline).

### Materialized views (`CREATE MATERIALIZED VIEW`)

Not supported.

**Migration:** maintain a second table in the application via dual-write. Key the second table for the alternate access pattern. Accept eventual consistency between the two tables — Cassandra materialized views have the same tradeoff.

### User-defined functions (`CREATE FUNCTION`)

Not supported.

**Migration:** move the computation client-side (application logic) or into an ETL / stream-processing step (for example, AWS Glue, AWS Lambda, Amazon Kinesis Data Analytics).

### User-defined aggregates (`CREATE AGGREGATE`)

Not supported.

**Migration:** same as UDFs — compute client-side or in a stream/batch job.

### LWT inside `BEGIN UNLOGGED BATCH`

Not supported. Keyspaces rejects any lightweight-transaction (`IF NOT EXISTS`, `IF EXISTS`, `IF <col>=…`) issued inside an unlogged batch.

**Migration:** issue the LWT as a single-statement conditional outside any batch:

```cql
UPDATE users SET email = 'a@b.c' WHERE id = ? IF email = 'old@b.c';
```

If the application requires atomic multi-row semantics previously achieved via batch+LWT, use application-level coordination (e.g., a state machine or idempotent retries) since neither LOGGED BATCH nor LWT-inside-UNLOGGED-BATCH is supported on Keyspaces.

### Aggregate calls in queries

Keyspaces rejects `COUNT(`, `MIN(`, `MAX(`, `SUM(`, `AVG(` in `SELECT` statements.

**Migration:**

- **COUNT** — maintain a counter table updated by the application on every write.
- **MIN / MAX** — maintain pre-aggregated summary rows, or read the first/last row by clustering-key order.
- **SUM / AVG** — compute client-side from a paginated `SELECT`, or maintain rolled-up summary tables updated by a stream processor.

## Not detected by the tool (but still unsupported or different)

The compatibility tool is a first-pass screen, not a full audit. The following differences are not flagged but still matter — point the user at the [functional differences page](https://docs.aws.amazon.com/keyspaces/latest/devguide/functional-differences.html) for the full catalog.

- **`ALLOW FILTERING`** — supported in Keyspaces but may be rate-limited. The tool does not flag it because it is usable.
- **`TRUNCATE`** — supported in Keyspaces as a throughput-controlled operation.
- **`CREATE CUSTOM INDEX` (SASI, SAI)** — Keyspaces does not support custom index implementations. Detected by the schema parser (the regex matches both `CREATE INDEX` and `CREATE CUSTOM INDEX`), so these will appear as secondary index findings in the compatibility report.
- **`COUNTER` columns** — supported in Keyspaces.
- **Clustering-order-reverse queries** — supported.
- **Lightweight-transaction serial consistency (`LOCAL_SERIAL`)** — supported.
- **Consistency level `EACH_QUORUM`** — not supported on reads.
- **Driver-level features** — some drivers expose Cassandra-specific features (e.g. `tuple` types) that Keyspaces supports only partially. Verify against the driver compatibility page.

## Informational (not issues)

### Tables using `USING TTL`

Not a compatibility issue. The compatibility output reports `query_patterns.ttl_tables` so:

- Mode 2 can treat those tables as TTL-driven for write accounting, even when DDL lacks `default_time_to_live`.
- The user can sanity-check that the TTL pricing signal matches their actual workload.

Display as "tables using `USING TTL`: …" — do not style it as an issue.

## Guidance style

When offering migration advice, keep it to **what to do instead**, not **why the feature is limited**. Customers planning a migration want actionable patterns, not rationale about Keyspaces internals. Compare:

- Good: "Create a separate denormalized table keyed by `email`; the application writes to both tables."
- Bad: "Secondary indexes are not available because Keyspaces uses a serverless architecture that cannot efficiently scan partition replicas for filtering."

The functional-differences page already documents the reasoning for anyone who asks.
references/mode-1-manual-inputs.md
# Mode 1 — Manual inputs

Use this mode when the user does not have a running Cassandra cluster, or when they prefer to type traffic estimates directly.

If the user mentions a running Cassandra cluster or DataStax deployment, always offer Mode 2 first — diagnostic data produces a more accurate estimate. Fall back to Mode 1 only when diagnostic captures (`nodetool tablestats` + `nodetool info`) are unavailable.

## Parameters

All positional, in this exact order:

| Position | Name | Required | Format | Notes |
|---|---|---|---|---|
| 1 | region | yes | AWS region code (e.g. `us-east-1`, `eu-west-1`, `ap-southeast-2`) | Must exist in `assets/data/regions.json`. |
| 2 | reads_per_second | yes | integer ≥ 0 | Strongly-consistent reads. Halved in output for eventual-consistency pricing. |
| 3 | writes_per_second | yes | integer ≥ 0 | Single-row inserts/updates. Does not include TTL auto-deletes (column 6). |
| 4 | avg_row_size_bytes | yes | integer | Typical 256-4096. Include partition key, clustering key, and all column bytes; exclude internal Cassandra overhead. Default `1024` only when truly unknown. |
| 5 | storage_gb | yes | number | Single-replica compressed storage in GB. Keyspaces applies replication internally at RF=3; pass the compressed single-replica figure. |
| 6 | ttl_deletes_per_second | no (default `0`) | integer ≥ 0 | TTL-expiring writes per second. Priced at the same rate as regular writes (writes are billed; reads/deletes are implicit). |
| 7 | pitr_enabled | no (default `false`) | `true` / `false` | Backups/point-in-time-recovery. Adds a per-GB-month surcharge. |

## Command

```bash
cd scripts
npx ts-node --project tsconfig.scripts.json calculate.ts \
  <region> <reads/s> <writes/s> <rowSizeBytes> <storageGB> [ttl/s] [pitr] \
  | tee /tmp/keyspaces-calc.json
```

Example:

```bash
npx ts-node --project tsconfig.scripts.json calculate.ts \
  us-east-1 1000 500 1024 100 0 false | tee /tmp/keyspaces-calc.json
```

## Output shape

```json
{
  "region": { "short": "us-east-1", "long": "US East (N. Virginia)" },
  "inputs": { ... the 7 parameters ... },
  "units_per_operation": { "write": 1, "read": 1, "ttl": 1 },
  "on_demand":               { "reads_strong": …, "reads_eventual": …, "writes": …, "ttl_deletes": …, "storage": …, "backup": …, "total": … },
  "provisioned":             { ... same shape ... },
  "savings_plan_available":  true | false,
  "on_demand_savings_plan":  { ... same shape or null ... },
  "provisioned_savings_plan":{ ... same shape or null ... },
  "report_data":             { datacenters, regions, estimateResults, pricing }
}
```

Values are monthly US dollars unless otherwise noted.

## Units-per-operation

Keyspaces bills by the capacity unit, not the raw row count. One **Write Capacity Unit (WCU)** covers up to 1 KB written; one **Read Capacity Unit (RCU)** covers up to 4 KB read (strongly consistent) or 4 KB per 2 RCUs (eventually consistent). Rows larger than the threshold consume more units per operation.

`units_per_operation.write` — ceil(row_size_bytes / 1024).
`units_per_operation.read` — ceil(row_size_bytes / 4096).
`units_per_operation.ttl`  — same as write.

Surface these when explaining why a row size change (for example going from 1024 to 2048 bytes) flips the recommendation between on-demand and provisioned.

## Presenting the result

Show the user:

1. A one-row **inputs summary** — region, reads/s, writes/s, row size, storage, PITR.
2. A **two-column cost table** — On-demand vs Provisioned, with line items for reads, writes, TTL deletes, storage, and backup, totaling at the bottom. Include a Savings Plan row when `savings_plan_available` is true.
3. A clear **recommendation** — whichever mode is cheaper at the user's stated traffic pattern, with a one-line reason tied to the read/write ratio.
4. A line noting the user may generate a PDF via Step 6 if wanted.

## Common follow-ups

**"What if I double the writes?"** — rerun with the new value; writes are linear in cost for both pricing modes.

**"What if I add PITR later?"** — rerun with `true` in position 7; PITR cost scales with `storage_gb` only.

**"Should I use on-demand or provisioned?"** — Provisioned wins for steady, predictable traffic (utilization > 18% of peak). On-demand wins for spiky or unknown traffic. The script already applies the breakeven; state the recommendation from the totals, then cite which mode won.

**"Can you compare two traffic scenarios?"** — run `calculate.ts` twice to different `/tmp/*.json` paths, then a single `generate-pdf.ts --input A --input B` to produce a consolidated PDF (see [pdf-reporting.md](pdf-reporting.md)).
references/mode-2-cassandra-diagnostics.md
# Mode 2 — Cassandra diagnostics

Use when the user has a running Cassandra cluster (or a DataStax / ScyllaDB deployment that can produce Cassandra-compatible diagnostics). This mode derives reads/writes per second from cumulative `nodetool info` counters and keyspace sizing from `nodetool tablestats` — neither can be guessed.

If either `tablestats` or `info` cannot be captured, fall back to Mode 1.

## Intake table

Each **ID** matches a `parse-cassandra.ts` flag (`--<id>`) when passing paths explicitly.

| ID | Captures | Run on | Output | If missing — ask | Default / escalation |
|---|---|---|---|---|---|
| `tablestats` | Live space + per-column-family details | any one node | `tablestats.txt` | — | **Mandatory.** Recapture. Without it, use Mode 1 — `parse-cassandra.ts` exits without `--tablestats`. A single representative tablestats file is sufficient; throughput is scaled by node count from `--info` files. |
| `info` | DC, host id, uptime; used with tablestats counters to derive reads/writes per second | every node | `info.txt` | — | **Mandatory.** Without it, use Mode 1 — RPS cannot be derived. |
| `status` | DC list, node count per DC | any one node | `status.txt` | How many DCs in the cluster? How many nodes per DC? | Capture preferred. Otherwise use the answers, or group `info` files by DC. If topology cannot be established, use Mode 1. |
| `schema` | DDL — feeds compatibility + replication factor | any one node | `schema.cql` | What replication factor for application keyspaces (per DC)? | If absent, parser uses RF=3 internally. Ask the user to confirm so intent matches estimate. |
| `rowsize` | Average row size per table | any one node | `rowsize.txt` | — | Default `1024` bytes. No further questions. |
| `prepared` | Prepared statements — drives compatibility (LWT-in-batch, aggregations) and the `USING TTL` pricing signal | any one node | `prepared_statements.ndjson` | — | Omit `--prepared`. No further questions. |

## Required vs optional

**Mandatory:** `tablestats` AND at least one `info` file.

**Strongly recommended:** `status` (topology), `schema` (compatibility + RF), `prepared` (compatibility signal + TTL pricing).

**Optional:** `rowsize` (per-table accuracy — defaults to 1024 bytes when absent).

## Capture commands

See [cassandra-capture-commands.md](cassandra-capture-commands.md) for the full set with `<auth>` shorthand.

## Running the parser

Prefer `--dir` auto-detection — the parser's filename detectors (`isTablestatsFile`, `isInfoFile`, `isStatusFile`, `isSchemaFile`, `isRowSizeFile`, `isPreparedStatementsFile`) classify each file regardless of naming.

```bash
# Directory auto-detection (recommended)
cd scripts
npx ts-node --project tsconfig.scripts.json parse-cassandra.ts \
  --dir /path/to/diagnostics --region us-east-1 [--pitr] \
  | tee /tmp/keyspaces-calc.json

# Individual files (explicit)
npx ts-node --project tsconfig.scripts.json parse-cassandra.ts \
  --region us-east-1 \
  --tablestats tablestats.txt \
  --info node1-info.txt --info node2-info.txt \
  [--status status.txt] [--schema schema.cql] \
  [--rowsize rowsize.txt] [--prepared prepared.ndjson] \
  [--pitr] | tee /tmp/keyspaces-calc.json
```

Repeat `--info` once per node. When both `--dir` and explicit flags are provided, explicit wins.

## Region selection

Pick `--region` by priority:

1. The DC name in `status` if it matches an AWS region (`us-east-1`, `eu-west-1`, …).
2. The `Datacenter` field in `nodetool info`.
3. The user's stated target region.
4. Default `us-east-1`.

Pass `--region` explicitly whenever inference is wrong or unclear.

## Output

Same shape as Mode 1, plus:

- `source: "cassandra-diagnostic-files"`
- `datacenters` — array of `{ name, nodeCount }` per DC.
- `per_datacenter` — cost breakdown per DC.
- `compatibility` — automatically populated when `--schema` or `--prepared` was supplied (or detected in `--dir`). Shape:

  ```json
  {
    "has_issues": true | false,
    "summary": {
      "total_issues": N,
      "schema":         { ... or null },
      "query_patterns": { ... or null }
    },
    "details": { "schema": { ... }, "query_patterns": { ... } }
  }
  ```

Surface the `compatibility` block when present — see [mode-3-compatibility.md](mode-3-compatibility.md) for display rules.

## Prepared-statement signal

A `prepared_statements.ndjson` capture changes two things:

1. **Compatibility:** detects LWT inside `BEGIN UNLOGGED BATCH`, aggregates (`COUNT`/`MIN`/`MAX`/`SUM`/`AVG`), and calls to user-defined functions (when `schema` is also supplied).
2. **Pricing:** `INSERT … USING TTL` and `UPDATE … USING TTL` mark tables as fully TTL-driven for write accounting, even when DDL lacks `default_time_to_live`. Tables with `default_time_to_live` already follow the `rowsize`-based TTL path.

## Displaying results

Present in this order:

1. **Cluster summary** — DCs, node count per DC, region(s) inferred.
2. **Per-keyspace breakdown** — keyspace name, RF, storage, reads/s, writes/s.
3. **Two-column cost table** (same as Mode 1).
4. **Recommendation** — cheaper mode.
5. **Compatibility findings** if `compatibility.has_issues` is true.
6. Offer PDF per [pdf-reporting.md](pdf-reporting.md).

## Multi-cluster or re-runs

For two or more separate clusters, run `parse-cassandra.ts` once per cluster, writing to distinct `/tmp/keyspaces-*.json` files. Then a single `generate-pdf.ts` invocation with multiple `--input` flags produces a consolidated comparison PDF.
references/mode-3-compatibility.md
# Mode 3 — Compatibility check

Use when the user asks whether a Cassandra schema or workload will run on Amazon Keyspaces, without wanting a cost estimate. For combined pricing + compatibility, run Mode 2 instead — it auto-populates the compatibility block.

## Inputs

At least one of:

- `--schema <path.cql>` — CQL DDL (`DESCRIBE SCHEMA` output or hand-written).
- `--prepared <path.ndjson>` — `system.prepared_statements` export as NDJSON (one JSON object per line).

Both may be supplied together. CQL may also be piped on stdin when `--prepared` is absent.

## Command

```bash
cd scripts

# Schema file
npx ts-node --project tsconfig.scripts.json check-compatibility.ts \
  --schema /tmp/schema.cql | tee /tmp/keyspaces-compat.json

# Prepared statements file
npx ts-node --project tsconfig.scripts.json check-compatibility.ts \
  --prepared /tmp/prepared_statements.ndjson | tee /tmp/keyspaces-compat.json

# Both
npx ts-node --project tsconfig.scripts.json check-compatibility.ts \
  --schema /tmp/schema.cql --prepared /tmp/prepared_statements.ndjson \
  | tee /tmp/keyspaces-compat.json

# Schema on stdin (only valid without --prepared)
echo "CREATE TABLE app.users (id uuid PRIMARY KEY, email text);" \
  | npx ts-node --project tsconfig.scripts.json check-compatibility.ts \
  | tee /tmp/keyspaces-compat.json
```

## What it detects

See [keyspaces-unsupported-features.md](keyspaces-unsupported-features.md) for the authoritative list and migration guidance. The tool flags:

**From schema (CQL DDL):**

- `CREATE INDEX` — secondary indexes (per table).
- `CREATE TRIGGER` — triggers (per table).
- `CREATE MATERIALIZED VIEW` — attached to base table.
- `CREATE FUNCTION` — user-defined functions (counted globally).
- `CREATE AGGREGATE` — user-defined aggregates (counted globally).

**From prepared statements:**

- **LWT inside `BEGIN UNLOGGED BATCH`** — any conditional (`IF NOT EXISTS`, `IF EXISTS`, `IF <col>=…`) inside an unlogged batch.
- **Aggregate calls** — `COUNT(`, `MIN(`, `MAX(`, `SUM(`, `AVG(` anywhere in a `SELECT`.
- **Per-table `USING TTL`** — informational only, not an issue; used by Mode 2 to mark tables as TTL-driven for pricing.

UDF usage is intentionally not detected from prepared statements — `CREATE FUNCTION` in schema is the source of truth.

## Output shape

```json
{
  "source": "compatibility-check",
  "input":   { "schema": "<path or null>", "prepared": "<path or null>" },
  "has_issues": true | false,
  "summary": {
    "total_issues": N,
    "schema": {
      "total_issues": N,
      "keyspaces_affected": N,
      "tables_affected": N,
      "functions": N,
      "aggregates": N
    } | null,
    "query_patterns": {
      "lwt_in_unlogged_batch": N,
      "aggregations": N,
      "ttl_tables": N
    } | null
  },
  "details": {
    "schema": {
      "functions": N,
      "aggregates": N,
      "keyspaces": {
        "<keyspace>": {
          "<table>": {
            "indexes":           ["idx_name", …],
            "triggers":          ["trg_name", …],
            "materializedViews": ["view_name", …]
          }
        }
      }
    } | null,
    "query_patterns": {
      "lwt_in_unlogged_batch": [ { "prepared_id": "...", "query_string": "..." } ],
      "aggregations":          [ { "prepared_id": "...", "function": "COUNT", "query_string": "..." } ],
      "ttl_tables":            { "<ks>.<table>": { "uses_ttl": true, "ttl_values": [3600, 86400] } }
    } | null
  }
}
```

## Display rules

Compatibility is binary. Every detected feature is **not supported**. Do not hedge with qualifiers like "supported with restrictions", "supported with caveats", "works when cardinality is high", or "may cause hot partitions" — those qualifiers do not apply here and mislead customers into building unsupported designs.

Present in this order:

1. **One-line verdict** — if `has_issues` is false, say the schema/workload is compatible with Amazon Keyspaces and stop. Otherwise continue.
2. **Per-keyspace / per-table breakdown** — for every keyspace in `details.schema.keyspaces`, list affected tables and their flagged features. Show the feature name (index / trigger / materialized view) and the object name.
3. **Global counts** — `details.schema.functions` and `details.schema.aggregates` (numbers only; names are not captured).
4. **Per-query breakdown** — for each entry in `details.query_patterns.lwt_in_unlogged_batch` and `.aggregations`, show the offending `query_string` truncated to roughly 200 characters. Include the `prepared_id` to help the user find it in their codebase.
5. **`ttl_tables` (informational)** — list as "tables using `USING TTL`: …" so the user can verify the TTL pricing signal Mode 2 uses.
6. **Migration guidance** — for each flagged category, offer guidance from [keyspaces-unsupported-features.md](keyspaces-unsupported-features.md). Keep guidance to *what to do instead*, not *why the feature is limited*.

## PDF generation is not supported for Mode 3

Mode 3 produces a compatibility report only. `generate-pdf.ts` expects pricing JSON and will fail on a compatibility JSON. If the user wants a combined compatibility + pricing PDF, direct them to Mode 2 — it includes compatibility automatically when `schema` or `prepared` is supplied.

## Follow-ups

**"Can I automate this in CI?"** — yes; `check-compatibility.ts` returns non-zero exit only on usage errors, not on `has_issues`. Check `.has_issues` in the JSON to fail a pipeline.

**"How do I fix each issue?"** — see migration guidance in [keyspaces-unsupported-features.md](keyspaces-unsupported-features.md).

**"Does this catch everything?"** — no. Data-type and CQL-syntax-level differences (the full functional-differences list) are not checked. Link the user to the official [Keyspaces functional differences page](https://docs.aws.amazon.com/keyspaces/latest/devguide/functional-differences.html).
references/mode-4-sql-migration.md
# Mode 4 — SQL to Keyspaces migration

Use when the user provides SQL `CREATE TABLE` statements and wants a Keyspaces migration plan. This mode translates the relational schema into three Keyspaces data models, prices each via `calculate.ts`, and recommends the best fit.

## Step 1 — Parse the SQL

Extract:

- **Tables** — name, columns (name + SQL type), primary key(s), UNIQUE constraints.
- **Foreign keys** — `(source_table, source_col)` → `(target_table, target_col)`.
- **Access queries** — any `SELECT` statements provided. These drive partition-key choice.

## Step 2 — Estimate field sizes

| SQL type | Bytes |
|---|---|
| `BOOL` / `BOOLEAN` | 1 |
| `SMALLINT` | 2 |
| `INT` / `INTEGER` / `SERIAL` / `DATE` / `FLOAT` / `REAL` | 4 |
| `BIGINT` / `DOUBLE` / `TIMESTAMP` / `DATETIME` / `DECIMAL` / `NUMERIC` | 8 |
| `UUID` | 16 |
| `VARCHAR(n)` / `CHAR(n)` | n |
| `VARCHAR` / `TEXT` / `CLOB` (no length) | 64 |
| `BLOB` / `BINARY` | 512 |

`row_size_bytes` per table = sum of all column byte sizes.

## Step 3 — Ask for workload inputs

If not already supplied, ask for:

- **Rows per table** (integer).
- **Reads/s** and **writes/s** — per table, or combined if the user cannot split.
- **AWS region** (default `us-east-1`).

Storage-only reasoning misses the dominant pricing driver, so do not skip rates.

## Step 4 — Apply the three strategies

### Option A — Full denormalization

Merge all foreign-key-related tables into one.

- `merged_row_size_bytes` = sum of all unique column sizes (deduplicate FK columns).
- `merged_row_count`      = row count of the many-side table (the table with the FK column). For 1:many relationships, this equals the child table row count since each child row maps to exactly one parent. For many:many relationships through a join table, use the join table row count.
- `storage_gb`            = `(merged_row_count × merged_row_size_bytes) / (1024^3)`.
- `reads_per_sec`  = sum of reads across original tables.
- `writes_per_sec` = sum of writes across original tables.
- **CQL:** one merged table. Partition key = the FK column matching the access query. Clustering key = child-table PK.

### Option B — Normalized with lookup tables

Keep original tables; add one lookup table per FK for application-side joins.

- **Original tables:** map 1:1 to CQL. `storage_gb = (row_count × row_size_bytes) / 1024^3` per table.
- **Lookup table** per FK `(source.col → target.pk)`, named `target_by_source`:
  - Columns: FK column + target PK column.
  - `lookup_row_size_bytes` = size(FK col) + size(target PK col).
  - `lookup_storage_gb` = `(target_row_count × lookup_row_size_bytes) / 1024^3`.
- `total_storage_gb`  = sum of original + all lookup storage.
- `reads_per_sec`     = sum of original reads + (FK lookups required per query × reads using them).
- `writes_per_sec`    = sum of original writes + (1 extra write per lookup table per insert).
- **CQL:** original tables unchanged + one lookup table per FK.

### Option C — Denormalized with reverse index

Same merged table as Option A, plus one reverse-index table per non-PK FK column.

- Merged table — identical to Option A.
- Reverse index per non-PK FK column — partition key = FK col, clustering key = merged PK, all merged columns duplicated (full copy).
  - `reverse_row_size_bytes` = `merged_row_size_bytes`.
  - `reverse_row_count`      = `merged_row_count`.
- `total_storage_gb`  = `merged_storage_gb × (1 + number_of_reverse_indexes)`.
- `reads_per_sec`     = same as Option A (no extra read; correct table picked per query).
- `writes_per_sec`    = `Option A writes_per_sec × (1 + number_of_reverse_indexes)`.
- **CQL:** merged table + one reverse-index table per non-PK FK column.

## Step 5 — Price each option

```bash
cd scripts
npx ts-node --project tsconfig.scripts.json calculate.ts \
  <region> <reads/s> <writes/s> <avg_row_size_bytes> <storage_gb> 0 false \
  | tee /tmp/keyspaces-sql-optionA.json

# Repeat for B → /tmp/keyspaces-sql-optionB.json
# Repeat for C → /tmp/keyspaces-sql-optionC.json
```

Extract `provisioned.total`, `on_demand.total`, and `provisioned_savings_plan.total` from each JSON.

## Step 6 — Present the comparison

### Three-model summary table

| | Option A — Denorm | Option B — Normalized | Option C — Reverse Index |
|---|---|---|---|
| Storage | — | — | — |
| Reads/s | — | — | — |
| Writes/s | — | — | — |
| Bytes/row (avg) | — | — | — |
| Backup | off / on | off / on | off / on |
| Lookups per query | — | — | — |
| **Provisioned + Savings Plan/mo** | **$—** | **$—** | **$—** |
| **On-demand + Savings Plan/mo** | **$—** | **$—** | **$—** |

- **Lookups per query** — number of separate Keyspaces reads required to satisfy one user-facing query (1 = single-table read; 2 = lookup + data; N = lookup returns N keys each needing its own read).
- **Backup** — reflects the `pitr_enabled` input (`PITR on` / `off`).

### CQL

Generate the full table definitions for each option.

### Recommendation

Pick based on:

- **Cost** — cheapest total at the user's read/write mix.
- **Query fit** — does the primary access path match the partition key?
- **Write amplification** — Options B and C add writes (B: lookup writes; C: N-way fanout for each reverse index).
- **Storage trade-offs** — Option C can 2× or 3× storage versus A.

State the recommended option first, then the one-line reason.

## Consolidated PDF

After displaying the comparison, ask the user whether they want a PDF. If yes, use one invocation with all three `--input` flags (see [pdf-reporting.md](pdf-reporting.md)):

```bash
npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --input /tmp/keyspaces-sql-optionA.json --label "Option A — Denorm" \
  --input /tmp/keyspaces-sql-optionB.json --label "Option B — Normalized" \
  --input /tmp/keyspaces-sql-optionC.json --label "Option C — Reverse Index" \
  --output /tmp/keyspaces-sql-comparison.pdf
```
references/pdf-reporting.md
# PDF reporting

PDF generation is optional and never automatic. Ask the user after showing the JSON estimate. Skip for Mode 3 (compatibility-only) because there is no pricing data to render; direct the user to Mode 2 for a combined report.

## Command

```bash
cd scripts
npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --input <path.json> [--label <name>] [--output <path.pdf>]
```

## Flags

- `--input <path>` — path to a `calculate.ts` or `parse-cassandra.ts` JSON file. Repeatable for multi-estimate reports.
- `--label <name>` — display label for the most recent `--input`. Optional; defaults to `Estimate 1`, `Estimate 2`, … in command-line order. Used in the comparison summary table and in per-estimate section headers.
- `--output <path>` — PDF output path. Defaults to `./keyspaces-pricing-estimate.pdf` in the current working directory.

## Modes

**Single estimate** — one `--input` (or JSON on stdin, for backwards compatibility):

```bash
# From a file
npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --input /tmp/keyspaces-calc.json --output /tmp/keyspaces.pdf

# From stdin (single estimate only)
cat /tmp/keyspaces-calc.json \
  | npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --output /tmp/keyspaces.pdf
```

Renders a single-estimate report — title page, inputs summary, cost tables (on-demand + provisioned + Savings Plan), per-keyspace breakdown if present, and a compatibility section if the JSON contains one.

**Multiple estimates** — two or more `--input` flags:

```bash
npx ts-node --project tsconfig.scripts.json generate-pdf.ts \
  --input /tmp/a.json --label "Option A — Denorm" \
  --input /tmp/b.json --label "Option B — Normalized" \
  --input /tmp/c.json --label "Option C — Reverse Index" \
  --output /tmp/keyspaces-comparison.pdf
```

Renders a consolidated comparison report — a side-by-side summary table (storage, reads/s, writes/s, on-demand/mo, OD+SP/mo, provisioned/mo, prov+SP/mo), followed by a per-estimate section for each input.

## When to use multi-input vs one-at-a-time

Always use a single multi-input invocation when the user has more than one estimate to report:

- **Mode 4** always has three estimates (Denorm / Normalized / Reverse Index).
- **Mode 1 sensitivity runs** — when the user wants to compare two or three traffic scenarios.
- **Mode 2 multi-cluster** — when the user is migrating two or more separate Cassandra clusters.

Avoid generating one PDF per estimate — the consolidated comparison table is the entire point.

## `EAGAIN` on stdin

If `generate-pdf.ts` throws `EAGAIN: resource temporarily unavailable, read`, the upstream `ts-node` process closed stdin before this process read it. Write the JSON to a file and pass `--input <path>` instead of piping. This is a Node.js timing behavior, not a bug in the script.

## Saving the intermediate JSON

The scripts print pricing JSON to stdout. Always `tee` or redirect into `/tmp/keyspaces-*.json`:

```bash
npx ts-node --project tsconfig.scripts.json calculate.ts \
  us-east-1 1000 500 1024 100 0 false \
  | tee /tmp/keyspaces-calc.json
```

Then PDF generation can reuse the same file without rerunning pricing. This is also how you produce multi-input comparisons — one `calculate.ts` or `parse-cassandra.ts` call per scenario, each to its own `/tmp/*.json`, then one `generate-pdf.ts` pulling them all.

## Output path conventions

- Default filename: `keyspaces-pricing-estimate.pdf` in the current directory.
- For Mode 4: `keyspaces-sql-comparison.pdf` or similar descriptive name.
- For cluster comparisons: include cluster names or dates in the filename so the user can distinguish reports later.

Prefer an absolute path in `--output` when running inside a skill so the user knows where to find the file afterward.

## What the PDF contains

Every PDF includes:

1. **Title page** — skill name, date, input summary.
2. **Cost summary** — on-demand vs provisioned vs Savings Plan tiers, line-item breakdown.
3. **Per-keyspace / per-datacenter breakdown** (Mode 2 only).
4. **Compatibility findings** — when the source JSON includes a `compatibility` block.
5. **Recommendation** — implicit (whichever total is lowest), reinforced by the table formatting.

The PDF does not include raw capture files, node hostnames, or credentials. If the user wants a deeper audit trail, keep the intermediate JSON alongside the PDF.

## Non-goals

- The PDF renderer does not fetch live pricing. All rates come from `assets/data/*.json`, which is a snapshot. When prices drift, the skill owner refreshes these files from AWS Pricing APIs.
- The PDF does not run the pricing calculation — it only renders pre-computed JSON. If the JSON is stale, regenerate it first.
- PDF generation does not modify any AWS resources. It is an output-only operation.
references/pre-warming.md
# Pre-warming & Capacity Planning

Guides the agent through assessing whether a table needs pre-warming and configuring warm throughput values.

## What is warm throughput?

Warm throughput is the number of read and write operations an Amazon Keyspaces table can handle **instantly** without needing time to scale. Every table has warm throughput values — they are visible via `get-table` at no cost. Pre-warming means manually increasing these values ahead of a known traffic event.

**Defaults for new tables:**

- On-demand mode: 12,000 read units/sec, 4,000 write units/sec
- Provisioned mode: warm throughput = whatever provisioned capacity was previously reached (high-water mark)

## When to pre-warm

| Scenario | Recommendation |
|----------|---------------|
| New table launching with expected high traffic from day 1 | Pre-warm at creation: `create-table --warm-throughput readUnitsPerSecond=X,writeUnitsPerSecond=Y` |
| Existing table facing a planned spike (flash sale, migration cutover, batch load) | Pre-warm before event: `update-table --warm-throughput-specification readUnitsPerSecond=X,writeUnitsPerSecond=Y` |
| Gradual organic growth | Auto-scaling is sufficient — Keyspaces adjusts warm throughput automatically as traffic grows |
| Unpredictable spiky traffic with no advance notice | On-demand mode handles this — warm throughput adjusts after each peak |
| Table migrated from Cassandra with known peak RPS from diagnostics | Pre-warm to the Cassandra peak RPS from `nodetool info` derived read/write rates |

## When NOT to pre-warm

- Traffic is steady and well within current warm throughput → no benefit
- You're already using auto-scaling and growth is gradual → auto-scaling adjusts warm throughput automatically
- The table is brand new with unknown traffic → start with on-demand defaults, monitor, then pre-warm if throttling appears

## Decision framework

```
Is there a known traffic event in the next 24-72 hours?
├── YES → Is the expected peak > current warm throughput?
│         ├── YES → Pre-warm to expected peak
│         └── NO  → No action needed (already warm enough)
└── NO  → Is the table experiencing WriteThrottleEvents or ReadThrottleEvents?
          ├── YES → Check if it's a hot partition (single partition > 1000 WCU or 3000 RCU)
          │         ├── YES → Fix partition key design (pre-warming won't help hot partitions)
          │         └── NO  → Pre-warm to observed peak + 20% headroom
          └── NO  → Auto-scaling or on-demand is handling it — no pre-warming needed
```

## How to calculate warm throughput values

**From expected application traffic:**

```
readUnitsPerSecond = peak_reads_per_second × ceil(avg_row_size_bytes / 4096)
writeUnitsPerSecond = peak_writes_per_second × ceil(avg_row_size_bytes / 1024)
```

**From Cassandra diagnostics (Mode 2):**
Use the read/write rates derived from `nodetool info` counters. The `parse-cassandra.ts` output includes `reads_per_second` and `writes_per_second` — these map directly to warm throughput targets.

**Headroom recommendation:** Add 20-30% above expected peak to absorb bursts. Pre-warming is a one-time cost, and under-warming risks throttling at the critical moment.

## CLI commands

**View current warm throughput:**

```bash
aws keyspaces get-table --keyspace-name <ks> --table-name <table> \
  --query "warmThroughputSpecification" --output json
```

Response:

```json
{
  "readUnitsPerSecond": 12000,
  "writeUnitsPerSecond": 4000,
  "status": "ACTIVE"
}
```

Status values: `ACTIVE` (ready), `UPDATING` (pre-warming in progress).

**Pre-warm an existing table:**

```bash
aws keyspaces update-table \
  --keyspace-name <ks> \
  --table-name <table> \
  --warm-throughput-specification readUnitsPerSecond=50000,writeUnitsPerSecond=20000
```

**Create a new table pre-warmed:**

```bash
aws keyspaces create-table \
  --keyspace-name <ks> \
  --table-name <table> \
  --schema-definition '...' \
  --warm-throughput readUnitsPerSecond=50000,writeUnitsPerSecond=20000 \
  --tags key=created_by,value=keyspaces-skill key=generation_model,value=<model-id>
```

## Cost

**When advising a customer on pre-warming, you MUST mention ALL of the following cost facts — never omit any:**

1. **Viewing** warm throughput: free (always available via `get-table`)
2. **Natural warm throughput** (from organic traffic growth): free
3. **Manually increasing** warm throughput above the natural level: **one-time charge based on the difference between specified values and current warm throughput** — not an ongoing recurring cost
4. Pre-warming does NOT change your capacity mode or provisioned settings — it only ensures the underlying storage partitions are pre-allocated

## Hot partition caveat

Pre-warming increases the **table-level** throughput floor. It does NOT help if traffic is concentrated on a single partition. Each partition is still limited to:

- 3,000 read units/second
- 1,000 write units/second

If `StoragePartitionThroughputCapacityExceeded` CloudWatch metric > 0, the issue is partition key design, not table-level warm throughput. Recommend reviewing partition key cardinality before pre-warming.

## Multi-region tables

Warm throughput settings on multi-region tables apply automatically to all replicas. You set it once and all regions get the same warm throughput floor. No per-region configuration needed.

## Monitoring after pre-warming

After pre-warming, verify the table handles the expected load:

1. `WarmThroughputSpecification.status` = `ACTIVE` (pre-warming complete)
2. `ConsumedReadCapacityUnits` / `ConsumedWriteCapacityUnits` — actual usage
3. `ReadThrottleEvents` / `WriteThrottleEvents` — should be 0 if pre-warming was sized correctly
4. `StoragePartitionThroughputCapacityExceeded` — hot partition indicator (not fixable by pre-warming)

## Useful links

- [Configure pre-warming for tables](https://docs.aws.amazon.com/keyspaces/latest/devguide/warm-throughput.html)
- [Create a table with higher warm throughput](https://docs.aws.amazon.com/keyspaces/latest/devguide/create-table-warm-throughput.html)
- [Increase existing table warm throughput](https://docs.aws.amazon.com/keyspaces/latest/devguide/update-warm-throughput.html)
- [View warm throughput](https://docs.aws.amazon.com/keyspaces/latest/devguide/view-warm-throughput.html)
- [Monitor pre-warmed table performance](https://docs.aws.amazon.com/keyspaces/latest/devguide/monitor-prewarming-cloudwatch.html)
- [Amazon Keyspaces pricing](https://aws.amazon.com/keyspaces/pricing/)
references/security-considerations.md
# Security considerations

This skill has two operational modes: (1) **advisory** — pricing math is local and input comes from user-provided files, and (2) **mutating** — creating keyspaces/tables and modifying table settings (TTL, PITR, capacity mode) via AWS APIs after user confirmation. Security exposure comes from the capture steps (connecting to the user's Cassandra cluster, reading prepared-statement contents), from how the user eventually connects applications to Amazon Keyspaces, and from the create/modify operations the skill performs on the user's behalf.

## Risks introduced by following the skill

1. **Cluster credentials** — Mode 2 and Mode 3 rely on captures that require connecting to a running Cassandra cluster with `cqlsh` or driver credentials. If the user is already authenticating to their cluster, the skill inherits that posture. The skill never stores or transmits credentials.
2. **Sensitive query content** — `system.prepared_statements` captures literal bound values (email addresses, customer IDs, account numbers) from real application queries. These files are **sensitive** and must be handled as such.
3. **Schema disclosure** — `DESCRIBE SCHEMA` includes table names, column names, and data types. Usually not secret, but for regulated workloads the table may indicate the kind of data stored (e.g. `patient_records`, `pii_events`).
4. **Pricing data staleness** — pricing comes from snapshot JSON files. If the user is making a procurement decision, state the snapshot date and direct them to the live [Keyspaces pricing page](https://aws.amazon.com/keyspaces/pricing/) to confirm.

## Recommended controls

### Credentials (do not touch)

The skill does not create, read, write, or transmit credentials.

- You MUST NOT prompt the user to enter AWS access keys or Cassandra passwords into the chat.
- The user runs `aws configure`, `ada credentials update`, or their cluster-specific authentication flow **outside** the skill before capturing diagnostics.
- You SHOULD recommend storing service-specific credentials (username/password for Keyspaces) in AWS Secrets Manager with automatic rotation enabled, rather than in application configuration files or environment variables.
- When the target is Amazon Keyspaces itself (e.g., for a TCO self-comparison), link the user to [Keyspaces IAM authentication](https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.credentials.html) and the [SigV4 authentication plugin](https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.credentials.html#programmatic.credentials.SigV4) rather than walking them through it in this skill.

### IAM least-privilege for Keyspaces

When the user deploys to Keyspaces, recommend least-privilege policies. Typical actions:

- `cassandra:Select` — reads.
- `cassandra:Modify` — writes (insert, update, delete).
- `cassandra:Alter`, `cassandra:Create`, `cassandra:Drop` — DDL operations (restrict to DDL roles, not application roles).
- `cassandra:Restore` — only for backup/restore roles.

Resource ARNs follow the pattern:

```
arn:aws:cassandra:<region>:<account-id>:/keyspace/<keyspace>/table/<table>
```

- You MUST NOT recommend wildcarded `cassandra:*` on production resources, because write/drop access should be separately scoped.
- You SHOULD recommend `aws:SourceVpc` or `aws:SourceVpce` condition keys when the application connects via a VPC endpoint.
- You SHOULD prefer IAM roles (EC2 instance profiles, EKS IRSA, Lambda execution roles) over long-lived access keys.

### TLS / encryption in transit

Amazon Keyspaces requires TLS. The endpoint pattern is:

```
cassandra.<region>.amazonaws.com:9142 --ssl
```

- You MUST include `--ssl` in every `cqlsh` example that targets Keyspaces.
- You MUST recommend TLS on the source Cassandra cluster during captures when the cluster is accessible over the network.
- You SHOULD recommend VPC endpoints (AWS PrivateLink) for private connectivity to Keyspaces.

### Encryption at rest

- Keyspaces encrypts data at rest by default with AWS-owned keys.
- For customer-managed KMS (CMK), recommend it when the user needs key rotation control or access logging. See [Keyspaces encryption at rest](https://docs.aws.amazon.com/keyspaces/latest/devguide/EncryptionAtRest.html).
- PITR-protected data is also encrypted.

### Prepared-statement PII handling

When capturing `system.prepared_statements`:

- You MUST warn the user that the file may contain PII from literal bound values in queries before they copy it off the cluster or share it with the skill.
- You SHOULD recommend redacting obviously sensitive columns (tokens, secrets, raw PII) before handing the file to the skill, if the user has time.
- You MUST NOT echo the raw `query_string` of a flagged statement back in chat when it contains values that look sensitive; surface the `prepared_id` and a short abstract pattern (e.g. "SELECT … FROM users WHERE email = ?") instead.
- Treat the file as ephemeral — recommend deleting it from `/tmp` after the estimate.

### Logging

- You MUST NOT add any logging from the skill that transmits capture contents off the user's machine.
- You SHOULD recommend enabling AWS CloudTrail for Keyspaces API activity logging and Amazon CloudWatch for operational metrics monitoring once the workload is deployed. CloudTrail captures all Keyspaces management events (DDL, IAM auth attempts); CloudWatch provides `SuccessfulRequestLatency`, `ThrottledEvents`, and `SystemErrors` metrics essential for operational visibility.
- You SHOULD recommend enabling CloudTrail log file validation (`--enable-log-file-validation`) to detect tampering, and encrypting CloudTrail logs with a KMS key (`--kms-key-id <key-arn>`).
- You SHOULD recommend encrypting CloudWatch Logs groups with KMS (`aws logs associate-kms-key --log-group-name <group> --kms-key-id <key-arn>`) when logs may contain sensitive information (table names, query patterns, IAM principal identifiers).

### Operational hygiene

- You MUST NOT use `PROD`, `production`, or real customer identifiers in example keyspace or table names, because copy-paste-into-production is a real failure mode.
- You SHOULD suggest per-region isolation for regulated data (e.g., data residency requirements for EU workloads).

## Skill-specific gotchas

- **Over-broad selection.** The description is scoped to Keyspaces + Cassandra terms; do not suggest this skill when the user is working with DynamoDB, Cassandra-on-EC2 operations (not migration), or generic NoSQL modeling questions unrelated to Keyspaces.
- **Destructive actions.** The skill blocks `delete-keyspace` and `delete-table` (irreversible). It performs create/modify operations (`create-keyspace`, `create-table`, `update-table`, `tag-resource`) only after explicit user confirmation per the Safety guidance section in SKILL.md. Advisory modes (pricing, compatibility) produce only local file writes (`/tmp/*.json`, `/tmp/*.pdf`).
- **Cross-account / cross-region.** The skill does not assume cross-account access. For multi-region Keyspaces deployments, remind the user that each region has its own pricing and endpoints.
- **Privilege escalation paths.** The skill does not create IAM policies or roles. When the user asks for one, refer them to the IAM permissions reference and recommend review by their security team.
- **Sensitive output redaction.** See prepared-statement handling above. Also: compatibility output never includes bound values from schema itself, only feature names and object names, so schema-only compatibility reports are generally safe to share.

## Links

- [Amazon Keyspaces security overview](https://docs.aws.amazon.com/keyspaces/latest/devguide/security.html)
- [Keyspaces IAM authentication](https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.credentials.html)
- [Keyspaces encryption at rest](https://docs.aws.amazon.com/keyspaces/latest/devguide/EncryptionAtRest.html)
- [SigV4 authentication plugin](https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.credentials.html#programmatic.credentials.SigV4)
- [AWS security best practices](https://aws.amazon.com/architecture/security-identity-compliance/)
scripts/calculate.ts
#!/usr/bin/env npx ts-node
/**
 * calculate.ts
 *
 * Keyspaces Pricing Calculator — manual inputs mode.
 * Delegates all pricing logic to PricingFormulas.ts.
 *
 * Usage:
 *   npx ts-node --require tsconfig-paths/register --project tsconfig.scripts.json \
 *     scripts/calculate.ts <region> <reads/s> <writes/s> <rowSizeBytes> <storageGB> [ttl/s] [pitr]
 */

import {
  calculatePricingEstimate,
  calculateWriteUnitsPerOperation,
  calculateReadUnitsPerOperation,
  calculateTtlUnitsPerOperation,
  type DatacenterRef,
  type EstimateResults,
  type PricingEstimateResult,
} from './calculator/PricingFormulas';

const regionsMap: Record<string, string> = require('../assets/data/regions.json');

// ─── Main ─────────────────────────────────────────────────────────────────────

function main() {
  const args = process.argv.slice(2);
  if (args.length < 5) {
    console.error('Usage: calculate.ts <region> <reads/s> <writes/s> <rowSizeBytes> <storageGB> [ttl/s] [pitr]');
    process.exit(1);
  }

  const [regionArg, readsArg, writesArg, rowSizeArg, storageArg, ttlArg = '0', pitrArg = 'false'] = args;

  const longRegion = regionsMap[regionArg] ?? regionArg;
  const reads_per_second       = Number(readsArg);
  const writes_per_second      = Number(writesArg);
  const avg_row_size_bytes     = Number(rowSizeArg);
  const storage_gb             = Number(storageArg);
  const ttls_per_second        = Number(ttlArg);
  const pitr_enabled           = pitrArg === 'true';

  // Input validation
  if (isNaN(reads_per_second) || reads_per_second < 0) { console.error('reads_per_second must be a non-negative number'); process.exit(1); }
  if (isNaN(writes_per_second) || writes_per_second < 0) { console.error('writes_per_second must be a non-negative number'); process.exit(1); }
  if (isNaN(avg_row_size_bytes) || avg_row_size_bytes <= 0) { console.error('avg_row_size_bytes must be a positive number'); process.exit(1); }
  if (isNaN(storage_gb) || storage_gb < 0) { console.error('storage_gb must be a non-negative number'); process.exit(1); }
  if (isNaN(ttls_per_second) || ttls_per_second < 0) { console.error('ttls_per_second must be a non-negative number'); process.exit(1); }

  // Build inputs for calculatePricingEstimate
  const datacenters: DatacenterRef[] = [{ name: regionArg, nodeCount: 0 }];
  const regions: Record<string, string> = { [regionArg]: longRegion };
  const estimateResults: EstimateResults = {
    [regionArg]: {
      default: {
        keyspace_name: 'default',
        keyspace_type: 'user',
        replication_factor: 3,
        total_live_space_gb: storage_gb,
        uncompressed_single_replica_gb: storage_gb,
        avg_read_row_size_bytes: avg_row_size_bytes,
        avg_write_row_size_bytes: avg_row_size_bytes,
        reads_per_second,
        writes_per_second,
        ttls_per_second,
        use_backup: pitr_enabled,
      },
    },
  };

  const pricing: PricingEstimateResult | null = calculatePricingEstimate(datacenters, regions, estimateResults);
  if (!pricing) {
    console.error(`Region not found: ${longRegion}`);
    process.exit(1);
  }

  // Extract per-keyspace costs from the result
  const dcCost = pricing.total_datacenter_cost[regionArg];
  const kc = dcCost.keyspaceCosts['default'];

  const odTotal   = pricing.total_monthly_on_demand_cost;
  const provTotal = pricing.total_monthly_provisioned_cost;
  const odTotalSP   = pricing.total_monthly_on_demand_cost_savings;
  const provTotalSP = pricing.total_monthly_provisioned_cost_savings;
  const savingsPlanAvailable = odTotalSP !== odTotal || provTotalSP !== provTotal;

  const result = {
    region: { short: regionArg, long: longRegion },
    inputs: {
      reads_per_second,
      writes_per_second,
      avg_row_size_bytes,
      storage_gb,
      ttls_per_second,
      pitr_enabled,
    },
    units_per_operation: {
      write: calculateWriteUnitsPerOperation(avg_row_size_bytes),
      read:  calculateReadUnitsPerOperation(avg_row_size_bytes),
      ttl:   calculateTtlUnitsPerOperation(avg_row_size_bytes),
    },
    on_demand: {
      reads_strong:   kc.reads_on_demand,
      reads_eventual: kc.reads_on_demand / 2,
      writes:         kc.writes_on_demand,
      ttl_deletes:    kc.ttlDeletes,
      storage:        kc.storage,
      backup:         kc.backup,
      total:          odTotal,
    },
    provisioned: {
      reads_strong:   kc.reads_provisioned,
      reads_eventual: kc.reads_provisioned / 2,
      writes:         kc.writes_provisioned,
      ttl_deletes:    kc.ttlDeletes,
      storage:        kc.storage,
      backup:         kc.backup,
      total:          provTotal,
    },
    savings_plan_available: savingsPlanAvailable,
    on_demand_savings_plan: savingsPlanAvailable ? {
      reads_strong:   kc.reads_on_demand_savings,
      reads_eventual: kc.reads_on_demand_savings / 2,
      writes:         kc.writes_on_demand_savings,
      ttl_deletes:    kc.ttlDeletes,
      storage:        kc.storage,
      backup:         kc.backup,
      total:          odTotalSP,
    } : null,
    provisioned_savings_plan: savingsPlanAvailable ? {
      reads_strong:   kc.reads_provisioned_savings,
      reads_eventual: kc.reads_provisioned_savings / 2,
      writes:         kc.writes_provisioned_savings,
      ttl_deletes:    kc.ttlDeletes,
      storage:        kc.storage,
      backup:         kc.backup,
      total:          provTotalSP,
    } : null,
    report_data: {
      datacenters,
      regions,
      estimateResults,
      pricing,
    },
  };

  console.log(JSON.stringify(result, null, 2));
}

main();
scripts/calculator/Constants.js
export const system_keyspaces = new Set([
    'OpsCenter', 'dse_insights_local', 'solr_admin',
    'dse_system', 'HiveMetaStore', 'system_auth',
    'dse_analytics', 'system_traces', 'dse_audit', 'system',
    'dse_system_local', 'dsefs', 'system_distributed', 'system_schema',
    'dse_perf', 'dse_insights', 'system_backups', 'dse_security',
    'dse_leases', 'system_distributed_everywhere', 'reaper_db'
]);

export const REPLICATION_FACTOR = 3;

export const SECONDS_PER_MONTH = (365/12) * (24 * 60 * 60);

export const GIGABYTE = 1024 * 1024 * 1024;


scripts/calculator/CreatePDFReport.ts
import jsPDF from 'jspdf';
import 'jspdf-autotable';
import { UserOptions, Table } from 'jspdf-autotable';

declare module 'jspdf' {
    interface jsPDF {
        autoTable(options: UserOptions): void;
        lastAutoTable: Table;
    }
}

// Intentional: Math.ceil produces conservative (rounded-up) estimates for executive
// summary reports. This avoids under-reporting costs in customer-facing PDF documents.
const formatCurrency = (amount: number): string => {
    if (amount < 0.01) {
        return `$${Math.ceil(amount * 100) / 100}`;
    }
    if (amount < 1) {
        return `$${amount.toFixed(2)}`;
    }
    return `$${Math.ceil(amount).toLocaleString()}`;
};

// --- Input types ---

export interface Datacenter {
    name: string;
    nodeCount: number;
}

export interface KeyspaceEstimate {
    writes_per_second: number;
    reads_per_second: number;
    avg_read_row_size_bytes: number;
    avg_write_row_size_bytes: number;
    total_live_space_gb: number;
    uncompressed_single_replica_gb: number;
    ttls_per_second: number;
    replication_factor: number;
}

export type EstimateResults = Record<string, Record<string, KeyspaceEstimate>>;
export type Regions = Record<string, string>;

export interface KeyspaceCost {
    name: string;
    storage: number;
    backup: number;
    reads_provisioned: number;
    writes_provisioned: number;
    reads_on_demand: number;
    writes_on_demand: number;
    ttlDeletes: number;
    provisioned_total: number;
    on_demand_total: number;
}

export interface DatacenterCost {
    region: string;
    keyspaceCosts: Record<string, KeyspaceCost>;
}

export interface Pricing {
    total_monthly_provisioned_cost: number;
    total_monthly_on_demand_cost: number;
    total_monthly_provisioned_cost_savings: number;
    total_monthly_on_demand_cost_savings: number;
    total_datacenter_cost: Record<string, DatacenterCost>;
}

interface TcoSingleNode {
    instance?: { monthly_cost: number };
    storage?: { monthly_cost: number };
    backup?: { monthly_cost: number };
    network_out?: { monthly_cost: number };
    network_in?: { monthly_cost: number };
    license?: { monthly_cost: number };
}

interface TcoEntry {
    single_node: TcoSingleNode;
    operations?: { operator_hours?: { monthly_cost: number } };
}

export type TcoData = Record<string, TcoEntry> | null;

export interface Estimate {
    label: string;
    datacenters: Datacenter[];
    regions: Regions;
    estimateResults: EstimateResults;
    pricing: Pricing;
    tcoData?: TcoData;
    compatibilityData?: CompatibilityData | null;
}

interface TableCompatibilityIssue {
    indexes: string[];
    triggers: string[];
    materializedViews: string[];
}

interface QueryPatternIssueRef {
    prepared_id?: string;
    query_string: string;
}

export interface CompatibilityData {
    functions: number;
    aggregates: number;
    keyspaces: Record<string, Record<string, TableCompatibilityIssue>>;
    queryPatterns?: {
        lwtInUnloggedBatch: QueryPatternIssueRef[];
        aggregations: QueryPatternIssueRef[];
    };
}

// --- Render option types ---

interface RenderTextOptions {
    contentFontSize?: number;
    lineHeight?: number;
    maxWidth?: number;
    pageBreakThreshold?: number;
    fontStyle?: string;
    startX?: number;
    startY?: number;
}

interface RenderTitleOptions {
    titleFontSize?: number;
    pageBreakThreshold?: number;
    startX?: number;
    startY?: number;
    maxWidth?: number;
    lineHeight?: number;
}

interface RenderImageOptions {
    imageWidth?: number;
    imageHeight?: number;
    startX?: number;
    startY?: number;
}

interface SectionOptions extends RenderTextOptions {
    titleFontSize?: number;
    imageUrl?: string;
    imageWidth?: number;
    imageHeight?: number;
    imageMargin?: number;
    addPageAfter?: boolean;
}

class CreatePDFReport {
    protected doc!: jsPDF;
    private yPosition: number = 20;
    private xPosition: number = 20;

    createReport(
        datacenters: Datacenter[],
        regions: Regions,
        estimateResults: EstimateResults,
        pricing: Pricing,
        tcoData: TcoData,
        compatibilityData?: CompatibilityData | null
    ): void {
        this.doc = new jsPDF();
        this.yPosition = 20;
        this.xPosition = 20;

        this.addTitle();
        this.addExecutiveSummary(datacenters, regions, estimateResults, pricing, tcoData);
        this.addIntroduction();
        this.customerQuote();
        this.addCostSummary(pricing);
        this.addResultsTables(datacenters, regions, estimateResults);
        this.addPricingTables(pricing);
        this.addAssumptions();
        this.addCassandraTCOSection(datacenters, tcoData);
        this.addCompatibilitySection(compatibilityData ?? null);

        this._output();
    }

    /**
     * Build a single PDF that compares multiple pricing estimates side-by-side.
     * Renders a comparison summary table up front, then per-estimate sections
     * (results, pricing, compatibility) with each estimate's label as a header.
     */
    createMultiReport(estimates: Estimate[]): void {
        if (!estimates || estimates.length === 0) {
            throw new Error('createMultiReport requires at least one estimate');
        }

        this.doc = new jsPDF();
        this.yPosition = 20;
        this.xPosition = 20;

        this.addTitle('Comparison report');
        this.addMultiOverview(estimates);
        this.addComparisonSummaryTable(estimates);
        this.addIntroduction();

        estimates.forEach((est, idx) => {
            this.doc.addPage();
            this.yPosition = 20;

            this.addEstimateHeader(est.label, idx + 1, estimates.length);
            this.addResultsTables(est.datacenters, est.regions, est.estimateResults);
            this.addPricingTables(est.pricing);
            this.addCompatibilitySection(est.compatibilityData ?? null);
        });

        if (this.yPosition > 220) {
            this.doc.addPage();
            this.yPosition = 20;
        }
        this.addAssumptions();

        this._output();
    }

    /** Override in subclasses to change how the finished PDF is delivered. */
    protected _output(): void {
        this.doc.save('keyspaces-pricing-estimate.pdf');
    }

    private addTitle(subtitle: string = 'Pricing estimate report'): void {
        this.doc.setFontSize(20);
        this.doc.setFont('helvetica', 'bold');
        this.doc.text('Amazon Keyspaces (for Apache Cassandra)', this.xPosition, this.yPosition);
        this.yPosition += 10;
        this.doc.text(subtitle, this.xPosition, this.yPosition);
        this.yPosition += 20;
    }

    private addMultiOverview(estimates: Estimate[]): void {
        const labels = estimates.map(e => e.label).join(', ');
        const overview = `This report compares ${estimates.length} Amazon Keyspaces pricing estimates side-by-side: ${labels}. The summary table below shows the headline workload and cost figures for each estimate. Per-estimate detail (input keyspaces, pricing breakdown, and compatibility findings where applicable) follows on the subsequent pages.`;

        this.addSection('Comparison overview', overview, { addPageAfter: false });
        this.yPosition += 5;
    }

    private addComparisonSummaryTable(estimates: Estimate[]): void {
        if (this.yPosition > 220) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        this.doc.setFontSize(12);
        this.doc.setFont('helvetica', 'bold');
        this.doc.text('Estimate comparison summary', this.xPosition, this.yPosition);
        this.yPosition += 8;

        const rows = estimates.map((est) => {
            let storageGb = 0;
            let readsPs = 0;
            let writesPs = 0;
            for (const dc of est.datacenters) {
                const ksMap = est.estimateResults[dc.name] ?? {};
                for (const ks of Object.values(ksMap)) {
                    storageGb += ks.uncompressed_single_replica_gb;
                    readsPs += ks.reads_per_second;
                    writesPs += ks.writes_per_second;
                }
            }
            return [
                est.label,
                Math.round(storageGb).toString(),
                Math.round(readsPs).toString(),
                Math.round(writesPs).toString(),
                formatCurrency(est.pricing.total_monthly_on_demand_cost),
                formatCurrency(est.pricing.total_monthly_on_demand_cost_savings),
                formatCurrency(est.pricing.total_monthly_provisioned_cost),
                formatCurrency(est.pricing.total_monthly_provisioned_cost_savings),
            ];
        });

        this.doc.autoTable({
            startY: this.yPosition,
            head: [[
                'Estimate',
                'Storage (GB)',
                'Reads/s',
                'Writes/s',
                'On-Demand /mo',
                'OD + SP /mo',
                'Provisioned /mo',
                'Prov + SP /mo',
            ]],
            body: rows,
            theme: 'grid',
            headStyles: { fillColor: [66, 139, 202] },
            styles: { fontSize: 8 },
            columnStyles: {
                0: { cellWidth: 38 },
                1: { cellWidth: 18 },
                2: { cellWidth: 18 },
                3: { cellWidth: 18 },
                4: { cellWidth: 22 },
                5: { cellWidth: 22 },
                6: { cellWidth: 22 },
                7: { cellWidth: 22 },
            },
        });

        this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 12;
    }

    private addEstimateHeader(label: string, index: number, total: number): void {
        this.doc.setFontSize(16);
        this.doc.setFont('helvetica', 'bold');
        this.doc.text(`${label} — estimate ${index} of ${total}`, this.xPosition, this.yPosition);
        this.yPosition += 12;
    }

    private addDate(): void {
        this.doc.setFontSize(12);
        this.doc.setFont('helvetica', 'normal');
        this.doc.text(`Generated on: ${new Date().toLocaleDateString()}`, this.xPosition, this.yPosition);
        this.yPosition += 15;
    }

    private addExecutiveSummary(
        datacenters: Datacenter[],
        regions: Regions,
        estimateResults: EstimateResults,
        pricing: Pricing,
        tcoData: TcoData
    ): void {
        if (!pricing) return;

        const totalKeyspaces = datacenters.reduce((total, dc) => {
            const results = estimateResults[dc.name];
            return total + (results ? Object.keys(results).length : 0);
        }, 0);

        const totalStorageGB = datacenters.reduce((total, dc) => {
            const results = estimateResults[dc.name];
            if (!results) return total;
            return total + Object.values(results).reduce((dcTotal, data) =>
                dcTotal + data.uncompressed_single_replica_gb, 0);
        }, 0);

        const totalWritesPerSecond = datacenters.reduce((total, dc) => {
            const results = estimateResults[dc.name];
            if (!results) return total;
            return total + Object.values(results).reduce((dcTotal, data) =>
                dcTotal + data.writes_per_second, 0);
        }, 0);

        const totalReadsPerSecond = datacenters.reduce((total, dc) => {
            const results = estimateResults[dc.name];
            if (!results) return total;
            return total + Object.values(results).reduce((dcTotal, data) =>
                dcTotal + data.reads_per_second, 0);
        }, 0);

        let totalCassandraTCO = 0;
        let instanceCost = 0;
        let storageCost = 0;
        let backupCost = 0;
        let networkCost = 0;
        let operationsCost = 0;
        let totalNodeCost = 0;
        let licenseCost = 0;
        if (tcoData) {
            datacenters.forEach(dc => {
                const tco = tcoData[dc.name];
                if (!tco) return;

                const dcInstanceCost = (tco.single_node?.instance?.monthly_cost || 0) * dc.nodeCount;
                const dcStorageCost = (tco.single_node?.storage?.monthly_cost || 0) * dc.nodeCount;
                const dcBackupCost = (tco.single_node?.backup?.monthly_cost || 0) * dc.nodeCount;
                const networkOutCost = tco.single_node?.network_out?.monthly_cost || 0;
                const networkInCost = tco.single_node?.network_in?.monthly_cost || 0;
                const dcNetworkCost = (networkOutCost + networkInCost) * dc.nodeCount;
                const dcLicenseCost = (tco.single_node?.license?.monthly_cost || 0) * dc.nodeCount;

                instanceCost += dcInstanceCost;
                storageCost += dcStorageCost;
                backupCost += dcBackupCost;
                networkCost += dcNetworkCost;
                licenseCost += dcLicenseCost;

                const dcNodeCost = dcInstanceCost + dcStorageCost + dcBackupCost + dcNetworkCost + dcLicenseCost;
                totalNodeCost += dcNodeCost;
                operationsCost += tco.operations?.operator_hours?.monthly_cost || 0;
            });
        }
        totalCassandraTCO = totalNodeCost + operationsCost;

        const summaryContent = `This report provides a comprehensive pricing estimate for migrating your Apache Cassandra workload to Amazon Keyspaces (for Apache Cassandra).`;

        this.addSection("Executive Summary", summaryContent, {
            addPageAfter: false
        });

        this.yPosition += 5;

        const keyDetails =
            `        • Total Datacenters: ${datacenters.length}
        • Total Keyspaces: ${totalKeyspaces}
        • Total Live Storage: ${Math.round(totalStorageGB)} GB
        • Total Write Operations: ${Math.round(totalWritesPerSecond)} per second
        • Total Read Operations: ${Math.round(totalReadsPerSecond)} per second`;

        this.addSubSection("Cassandra cluster:", keyDetails, {
            addPageAfter: false
        });

        this.yPosition += 5;

        let infrastructureContent =
            `        • Instance Cost: ${formatCurrency(instanceCost || 0)}
        • Storage Cost: ${formatCurrency(storageCost || 0)}
        • Backup Cost: ${formatCurrency(backupCost || 0)}
        • Network Cost: ${formatCurrency(networkCost || 0)}
        • License Cost: ${formatCurrency(licenseCost || 0)}
        • Operations Cost: ${formatCurrency(operationsCost || 0)}
        -----------------------------------------------------------
        • Total MonthlyCost: ${formatCurrency(totalCassandraTCO)}
        • Total Annual Cost: ${formatCurrency(totalCassandraTCO * 12)}`;

        if (totalCassandraTCO === 0) {
            infrastructureContent = `TCO data was not provided. Check file and upload section to add the total cost of ownership details.`;
        }

        this.addSubSection("Self-managed Cassandra cost estimate:", infrastructureContent, {
            addPageAfter: false
        });

        this.yPosition += 5;

        const keyspacesPricingContent =
            `        • Monthly Provisioned Capacity: ${formatCurrency(pricing.total_monthly_provisioned_cost)} / Savings Plan: ${formatCurrency(pricing.total_monthly_provisioned_cost_savings)}
        • Annual Provisioned Cost: ${formatCurrency(pricing.total_monthly_provisioned_cost * 12)} / Savings Plan: ${formatCurrency(pricing.total_monthly_provisioned_cost_savings * 12)}
        -----------------------------------------------------------
        • Monthly On-Demand Capacity: ${formatCurrency(pricing.total_monthly_on_demand_cost)} / Savings Plan: ${formatCurrency(pricing.total_monthly_on_demand_cost_savings)}
        • Annual On-Demand Cost: ${formatCurrency(pricing.total_monthly_on_demand_cost * 12)} / Savings Plan: ${formatCurrency(pricing.total_monthly_on_demand_cost_savings * 12)}
       


        This Keyspaces estimate is based on your current Cassandra cluster configuration and usage patterns. The provisioned pricing model offers predictable costs with 70% target utilization, while on-demand pricing provides flexibility for variable workloads.
        `;

        this.addSubSection("Keyspaces pricing estimate:", keyspacesPricingContent, {
            addPageAfter: true
        });
    }

    private addIntroduction(): void {
        const content =
`Amazon Keyspaces (for Apache Cassandra) is a serverless, fully managed database service that enables you to run Cassandra workloads at scale on AWS without refactoring your applications.

Many customers face challenges operating and scaling self-managed Cassandra clusters — including the complexity of managing infrastructure, tuning performance, handling repairs and upgrades, and meeting demanding availability and compliance requirements.

These challenges can be addressed with a solution that provides serverless infrastructure, elastic scalability, built-in security, and automated operations — all without the need to manage nodes, clusters, or software maintenance tasks.

Amazon Keyspaces uniquely delivers these capabilities through its purpose-built, serverless architecture, seamless integration with AWS security and observability tools, and pay-as-you-go pricing model.

With 99.999% availability SLA, the ability to double capacity in under 30 minutes, and consistent single-digit millisecond read/write performance, Keyspaces helps customers achieve operational excellence at scale. Leading organizations such as Monzo Bank, Intuit, GE Digital, and Adobe rely on Keyspaces to power critical, high-scale applications.`;

        this.addSection("Introduction", content, {
            addPageAfter: false
        });

        this.yPosition += 10;
    }

    private customerQuote(): void {
        const content = `"In our prior state, if we had to scale out our cluster for more capacity, we would need a lead time of a few weeks. Now, using Amazon Keyspaces, we can accomplish this in 1 day."

        - Manoj Mohan, Software Engineer Leader, Intuit`;

        this.addSection("Intuit Zero downtime migration to Amazon Keyspaces", content, {
            addPageAfter: false
        });

        this.yPosition += 20;
    }

    private addCostSummary(pricing: Pricing): void {
        if (!pricing) return;

        const cost_summary = 'The following section outlines the estimation process for Amazon Keyspaces. It begins by detailing the inputs used to generate the estimate, followed by the output of the Keyspaces cost estimate.';
        this.addSection("Estimate summary", cost_summary, {
            addPageAfter: false
        });

        this.yPosition += 10;
    }

    private addResultsTables(datacenters: Datacenter[], regions: Regions, estimateResults: EstimateResults): void {
        datacenters.forEach((datacenter) => {
            const results = estimateResults[datacenter.name];
            if (!results) return;

            if (this.yPosition > 250) {
                this.doc.addPage();
                this.yPosition = 20;
            }

            const dc_summary = 'The following table provides input gathered from the user interface about your existing workload.';
            this.addSection(`Input details - DC:${datacenter.name} to AWS Region:${regions[datacenter.name] || 'Unknown Region'} `, dc_summary, {
                addPageAfter: false
            });

            const tableData = Object.entries(results).map(([keyspace, data]) => [
                keyspace,
                Math.round(data.writes_per_second).toString(),
                Math.round(data.reads_per_second).toString(),
                Math.round((data.avg_read_row_size_bytes + data.avg_write_row_size_bytes) / 2).toString(),
                Math.round(data.total_live_space_gb).toString(),
                Math.round(data.uncompressed_single_replica_gb).toString(),
                Math.round(data.ttls_per_second).toString(),
                data.replication_factor.toString()
            ]);

            this.doc.autoTable({
                startY: this.yPosition,
                head: [['Keyspace', 'Writes per/sec', 'Read per/sec', 'Avg Row Size (bytes)', 'Live Space (GB)', 'Uncompressed (GB)', 'TTL per/sec', 'Replication factor']],
                body: tableData,
                theme: 'grid',
                headStyles: { fillColor: [66, 139, 202] },
                styles: { fontSize: 8 },
                columnStyles: {
                    0: { cellWidth: 30 },
                    1: { cellWidth: 20 },
                    2: { cellWidth: 20 },
                    3: { cellWidth: 20 },
                    4: { cellWidth: 20 },
                    5: { cellWidth: 20 },
                    6: { cellWidth: 20 },
                    7: { cellWidth: 20 }
                }
            });

            this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 15;
        });
    }

    private addPricingTables(pricing: Pricing): void {
        if (!pricing) return;

        Object.entries(pricing.total_datacenter_cost).forEach(([datacenter, data]) => {
            if (this.yPosition > 250) {
                this.doc.addPage();
                this.yPosition = 20;
            }

            const dc_summary = 'The following table provides Keyspaces estimate based on the inputs provided.';
            this.addSection(`Keyspaces estimate - DC:${datacenter} to AWS Region:${data.region}`, dc_summary, {
                addPageAfter: false
            });

            const pricingTableData = Object.entries(data.keyspaceCosts).map(([, costs]) => [
                costs.name,
                formatCurrency(costs.storage),
                formatCurrency(costs.backup),
                formatCurrency(costs.reads_provisioned),
                formatCurrency(costs.writes_provisioned),
                formatCurrency(costs.reads_on_demand),
                formatCurrency(costs.writes_on_demand),
                formatCurrency(costs.ttlDeletes),
                formatCurrency(costs.provisioned_total),
                formatCurrency(costs.on_demand_total)
            ]);

            this.doc.autoTable({
                startY: this.yPosition,
                head: [['Keyspace', 'Storage', 'Backup', 'Prov Reads', 'Prov Writes', 'OnDemand Reads', 'OnDemand Writes', 'TTL Deletes', 'Provisioned Total', 'OnDemand Total']],
                body: pricingTableData,
                theme: 'grid',
                headStyles: { fillColor: [66, 139, 202] },
                styles: { fontSize: 7 },
                columnStyles: {
                    0: { cellWidth: 25 },
                    1: { cellWidth: 18 },
                    2: { cellWidth: 18 },
                    3: { cellWidth: 18 },
                    4: { cellWidth: 18 },
                    5: { cellWidth: 18 },
                    6: { cellWidth: 18 },
                    7: { cellWidth: 18 },
                    8: { cellWidth: 18 },
                    9: { cellWidth: 18 }
                }
            });

            this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 15;
        });
    }

    private addCassandraTCOSection(datacenters: Datacenter[], tcoData: TcoData): void {
        if (!tcoData || !datacenters || datacenters.length === 0) return;

        if (this.yPosition > 200) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        const sectionTitle = 'Cassandra TCO (Total Cost of Ownership)';
        const sectionDescription = 'The following table shows the current Cassandra infrastructure costs per datacenter. Costs are calculated per node and multiplied by the total number of nodes in each datacenter.';

        this.addSection(sectionTitle, sectionDescription, {
            addPageAfter: false
        });

        const tcoTableData: string[][] = [];
        let totalTCO = 0;

        datacenters.forEach(dc => {
            const tco = tcoData[dc.name];
            if (!tco) return;

            const instanceCost = tco.single_node?.instance?.monthly_cost || 0;
            const storageCost = tco.single_node?.storage?.monthly_cost || 0;
            const backupCost = tco.single_node?.backup?.monthly_cost || 0;
            const networkOutCost = tco.single_node?.network_out?.monthly_cost || 0;
            const networkInCost = tco.single_node?.network_in?.monthly_cost || 0;
            const networkCost = networkOutCost + networkInCost;
            const licenseCost = tco.single_node?.license?.monthly_cost || 0;
            const perNodeCost = instanceCost + storageCost + backupCost + networkCost + licenseCost;
            const totalNodeCost = perNodeCost * dc.nodeCount;
            const operationsCost = tco.operations?.operator_hours?.monthly_cost || 0;
            const datacenterTCO = totalNodeCost + operationsCost;
            totalTCO += datacenterTCO;

            tcoTableData.push([
                dc.name,
                dc.nodeCount.toString(),
                formatCurrency(instanceCost),
                formatCurrency(storageCost),
                formatCurrency(backupCost),
                formatCurrency(networkCost),
                formatCurrency(licenseCost),
                formatCurrency(perNodeCost),
                formatCurrency(totalNodeCost),
                formatCurrency(operationsCost),
                formatCurrency(datacenterTCO)
            ]);
        });

        if (tcoTableData.length === 0) return;

        this.doc.autoTable({
            startY: this.yPosition,
            head: [['Datacenter', 'Nodes', 'Instance/Node', 'Storage/Node', 'Backup/Node', 'Network/Node', 'License/Node', 'Per Node Total', 'Node Total (All Nodes)', 'Operations', 'Datacenter TCO']],
            body: tcoTableData,
            theme: 'grid',
            headStyles: { fillColor: [66, 139, 202] },
            styles: { fontSize: 7 },
            columnStyles: {
                0: { cellWidth: 25 },
                1: { cellWidth: 15 },
                2: { cellWidth: 18 },
                3: { cellWidth: 18 },
                4: { cellWidth: 18 },
                5: { cellWidth: 18 },
                6: { cellWidth: 18 },
                7: { cellWidth: 20 },
                8: { cellWidth: 18 },
                9: { cellWidth: 20 }
            }
        });

        this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 15;

        if (this.yPosition > 250) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        this.doc.setFontSize(12);
        this.doc.setFont('helvetica', 'bold');
        this.doc.text(`Self managed Cassandra Total Ownership Cost (All Datacenters): ${formatCurrency(totalTCO)}/month`, this.xPosition, this.yPosition);
        this.yPosition += 15;
    }

    private addAssumptions(): void {
        if (this.yPosition > 250) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        this.doc.setFontSize(14);
        this.doc.setFont('helvetica', 'bold');
        this.doc.text('Assumptions', this.xPosition, this.yPosition);
        this.yPosition += 10;

        this.doc.setFontSize(10);
        this.doc.setFont('helvetica', 'normal');
        this.doc.text('• Provisioned estimate includes 70% target utilization for auto-scaling', 20, this.yPosition);
        this.yPosition += 8;
        this.doc.text('• Costs are calculated based on usage patterns from your Cassandra cluster data', 20, this.yPosition);
        this.yPosition += 8;
        this.doc.text('• Pricing uses Amazon Keyspaces rates for the selected regions', 20, this.yPosition);
        this.yPosition += 16;
    }

    private addCompatibilitySection(compatibilityData: CompatibilityData | null): void {
        if (!compatibilityData) return;

        const queryPatterns = compatibilityData.queryPatterns;
        const lwtCount = queryPatterns?.lwtInUnloggedBatch.length ?? 0;
        const aggCount = queryPatterns?.aggregations.length ?? 0;
        const hasIssues =
            compatibilityData.functions > 0 ||
            compatibilityData.aggregates > 0 ||
            Object.keys(compatibilityData.keyspaces).length > 0 ||
            lwtCount > 0 ||
            aggCount > 0;

        if (this.yPosition > 200) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        if (!hasIssues) {
            const noIssuesDescription =
                'Your Cassandra schema was analyzed for compatibility with Amazon Keyspaces. No unsupported features were detected. Your schema appears fully compatible with Amazon Keyspaces.';

            this.addSection('Keyspaces Compatibility Analysis', noIssuesDescription, {
                addPageAfter: false
            });
            return;
        }

        const sectionDescription =
            'The following Cassandra features were detected in your schema that are not supported by Amazon Keyspaces. These will need to be addressed as part of the migration.';

        this.addSection('Keyspaces Compatibility Issues', sectionDescription, {
            addPageAfter: false
        });

        this.yPosition += 5;

        // Keyspace-level issues: functions and aggregates
        if (compatibilityData.functions > 0 || compatibilityData.aggregates > 0) {
            const keyspaceLevelContent =
                `        • User-Defined Functions (UDFs): ${compatibilityData.functions}
        • User-Defined Aggregates (UDAs): ${compatibilityData.aggregates}

        UDFs and UDAs are not supported in Amazon Keyspaces. Application logic that depends on server-side functions or aggregates must be moved to the client side.`;

            this.addSubSection('Keyspace-level issues:', keyspaceLevelContent, {
                addPageAfter: false
            });

            this.yPosition += 5;
        }

        // Query-pattern issues from prepared statements (optional)
        if (queryPatterns && (lwtCount > 0 || aggCount > 0)) {
            const queryPatternsContent =
                `        • Lightweight Transactions in UNLOGGED BATCH: ${lwtCount}
        • Aggregation queries (COUNT/MIN/MAX/SUM/AVG): ${aggCount}

        Amazon Keyspaces does not support LWT inside UNLOGGED BATCH or server-side aggregation functions. These query patterns must be rewritten on the client side. The offending prepared statements are listed below.`;

            this.addSubSection('Query-pattern issues (from prepared statements):', queryPatternsContent, {
                addPageAfter: false
            });

            this.yPosition += 5;

            const renderQueryTable = (heading: string, issues: QueryPatternIssueRef[]) => {
                if (issues.length === 0) return;

                if (this.yPosition > 250) {
                    this.doc.addPage();
                    this.yPosition = 20;
                }

                this.doc.setFontSize(11);
                this.doc.setFont('helvetica', 'bold');
                this.doc.text(heading, this.xPosition, this.yPosition);
                this.yPosition += 6;

                const rows = issues.map((issue) => {
                    const normalized = issue.query_string.replace(/\s+/g, ' ').trim();
                    const truncated = normalized.length > 400
                        ? `${normalized.slice(0, 400)}…`
                        : normalized;
                    return [issue.prepared_id ?? '-', truncated];
                });

                this.doc.autoTable({
                    startY: this.yPosition,
                    head: [['Prepared ID', 'Query']],
                    body: rows,
                    theme: 'grid',
                    headStyles: { fillColor: [204, 51, 51] },
                    styles: { fontSize: 7, cellPadding: 2, overflow: 'linebreak' },
                    columnStyles: {
                        0: { cellWidth: 35 },
                        1: { cellWidth: 145 },
                    },
                });

                this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 6;
            };

            renderQueryTable('LWT in UNLOGGED BATCH:', queryPatterns.lwtInUnloggedBatch);
            renderQueryTable('Aggregation queries:', queryPatterns.aggregations);

            this.yPosition += 5;
        }

        // Table-level issues: indexes, triggers, materialized views
        const tableRows: string[][] = [];
        for (const [ks, tables] of Object.entries(compatibilityData.keyspaces)) {
            for (const [table, issues] of Object.entries(tables)) {
                const indexCount = issues.indexes.length;
                const triggerCount = issues.triggers.length;
                const mvCount = issues.materializedViews.length;

                if (indexCount > 0 || triggerCount > 0 || mvCount > 0) {
                    tableRows.push([
                        `${ks}.${table}`,
                        indexCount > 0 ? issues.indexes.join(', ') : '-',
                        triggerCount > 0 ? issues.triggers.join(', ') : '-',
                        mvCount > 0 ? issues.materializedViews.join(', ') : '-',
                    ]);
                }
            }
        }

        if (tableRows.length > 0) {
            if (this.yPosition > 250) {
                this.doc.addPage();
                this.yPosition = 20;
            }

            this.doc.setFontSize(12);
            this.doc.setFont('helvetica', 'bold');
            this.doc.text('Table-level issues:', this.xPosition, this.yPosition);
            this.yPosition += 8;

            this.doc.autoTable({
                startY: this.yPosition,
                head: [['Table', 'Secondary Indexes', 'Triggers', 'Materialized Views']],
                body: tableRows,
                theme: 'grid',
                headStyles: { fillColor: [204, 51, 51] },
                styles: { fontSize: 8 },
                columnStyles: {
                    0: { cellWidth: 45 },
                    1: { cellWidth: 45 },
                    2: { cellWidth: 45 },
                    3: { cellWidth: 45 },
                },
            });

            this.yPosition = (this.doc.lastAutoTable.finalY ?? this.yPosition) + 10;

            const tableLevelNote =
                `        • Secondary Indexes: Amazon Keyspaces does not support secondary indexes. Consider restructuring queries or using separate tables.
        • Triggers: Triggers are not supported. Use AWS Lambda with Amazon Keyspaces Streams or application-level logic instead.
        • Materialized Views: Not supported. Create separate tables and manage denormalization in the application layer.`;

            this.addSubSection('', tableLevelNote, { addPageAfter: false });

            this.yPosition += 10;
        }
    }

    private _renderTextContent(content: string, options: RenderTextOptions = {}): number {
        const {
            contentFontSize = 12,
            lineHeight = 7,
            maxWidth = 180,
            pageBreakThreshold = 280,
            fontStyle = 'normal',
            startX = this.xPosition,
            startY = this.yPosition
        } = options;

        let currentY = startY;

        this.doc.setFontSize(contentFontSize);
        this.doc.setFont('helvetica', fontStyle);

        const lines: string[] = this.doc.splitTextToSize(content, maxWidth);

        lines.forEach(line => {
            if (currentY > pageBreakThreshold) {
                this.doc.addPage();
                currentY = 20;
            }
            this.doc.text(line, startX, currentY);
            currentY += lineHeight;
        });

        return currentY;
    }

    private _renderTitle(title: string, options: RenderTitleOptions = {}): number {
        const {
            titleFontSize = 16,
            pageBreakThreshold = 280,
            startX = this.xPosition,
            startY = this.yPosition,
            maxWidth = 180,
            lineHeight = 8
        } = options;

        let currentY = startY;

        if (currentY > pageBreakThreshold - 50) {
            this.doc.addPage();
            currentY = 20;
        }

        this.doc.setFontSize(titleFontSize);
        this.doc.setFont('helvetica', 'bold');

        const lines: string[] = this.doc.splitTextToSize(title, maxWidth);

        lines.forEach(line => {
            this.doc.text(line, startX, currentY);
            currentY += lineHeight;
        });

        return currentY;
    }

    private _renderImage(imageUrl: string, options: RenderImageOptions = {}): number {
        const {
            imageWidth = 60,
            imageHeight = 40,
            startX = this.xPosition,
            startY = this.yPosition
        } = options;

        try {
            let imageFormat = 'JPEG';
            if (imageUrl.toLowerCase().includes('.png')) {
                imageFormat = 'PNG';
            } else if (imageUrl.toLowerCase().includes('.gif')) {
                imageFormat = 'GIF';
            } else if (imageUrl.toLowerCase().includes('.webp')) {
                imageFormat = 'WEBP';
            }

            this.doc.addImage(imageUrl, imageFormat, startX, startY, imageWidth, imageHeight);
        } catch (error) {
            console.warn('Failed to add image:', error);
            this.doc.rect(startX, startY, imageWidth, imageHeight);
            this.doc.text('Image', startX + imageWidth / 2 - 10, startY + imageHeight / 2);
        }

        return startY + imageHeight;
    }

    addTextContent(content: string, options: SectionOptions & { title?: string } = {}): void {
        const {
            title,
            addPageAfter = false,
            titleFontSize,
            imageUrl: _imageUrl,
            imageWidth: _imageWidth,
            imageHeight: _imageHeight,
            imageMargin: _imageMargin,
            ...renderOptions
        } = options;

        let currentY = this.yPosition;

        if (title) {
            currentY = this._renderTitle(title, {
                titleFontSize: titleFontSize || 16,
                pageBreakThreshold: renderOptions.pageBreakThreshold || 280,
                startY: currentY
            });
        }

        currentY = this._renderTextContent(content, {
            ...renderOptions,
            startY: currentY
        });

        this.yPosition = currentY;

        if (addPageAfter) {
            this.doc.addPage();
            this.yPosition = 20;
        }
    }

    private addSubsectionTextContent(content: string, options: SectionOptions & { title?: string } = {}): void {
        const {
            title,
            addPageAfter = false,
            titleFontSize: _titleFontSize,
            imageUrl: _imageUrl,
            imageWidth: _imageWidth,
            imageHeight: _imageHeight,
            imageMargin: _imageMargin,
            ...renderOptions
        } = options;

        let currentY = this.yPosition;

        if (title) {
            currentY = this._renderTitle(title, {
                titleFontSize: 12,
                pageBreakThreshold: renderOptions.pageBreakThreshold || 280,
                startY: currentY
            });
        }

        currentY = this._renderTextContent(content, {
            ...renderOptions,
            startY: currentY
        });

        this.yPosition = currentY;

        if (addPageAfter) {
            this.doc.addPage();
            this.yPosition = 20;
        }
    }

    addSection(title: string, content: string, options: SectionOptions = {}): void {
        const {
            imageUrl,
            imageWidth = 60,
            imageHeight = 40,
            imageMargin = 10,
            addPageAfter = false,
            ...textOptions
        } = options;

        if (imageUrl) {
            this.addSectionWithImage(content, imageUrl, {
                imageWidth,
                imageHeight,
                imageMargin,
                addPageAfter,
                ...textOptions
            });
        } else {
            this.addTextContent(content, {
                title,
                addPageAfter,
                ...textOptions
            });
        }
    }

    private addSubSection(title: string, content: string, options: SectionOptions = {}): void {
        const {
            imageUrl,
            imageWidth = 60,
            imageHeight = 40,
            imageMargin = 10,
            addPageAfter = false,
            ...textOptions
        } = options;

        if (imageUrl) {
            this.addSectionWithImage(content, imageUrl, {
                imageWidth,
                imageHeight,
                imageMargin,
                addPageAfter,
                ...textOptions
            });
        } else {
            this.addSubsectionTextContent(content, {
                title,
                addPageAfter,
                ...textOptions
            });
        }
    }

    private addSectionWithImage(content: string, imageUrl: string, options: SectionOptions = {}): void {
        const {
            imageWidth = 60,
            imageHeight = 40,
            imageMargin = 10,
            maxWidth = 180,
            pageBreakThreshold = 280,
            addPageAfter = false,
            ...textOptions
        } = options;

        const textWidth = maxWidth - imageWidth - imageMargin;
        const imageX = this.xPosition;
        const textX = imageX + imageWidth + imageMargin;

        if (this.yPosition + Math.max(imageHeight, 50) > pageBreakThreshold) {
            this.doc.addPage();
            this.yPosition = 20;
        }

        this._renderImage(imageUrl, {
            imageWidth,
            imageHeight,
            startX: imageX,
            startY: this.yPosition
        });

        const finalY = this._renderTextContent(content, {
            ...textOptions,
            maxWidth: textWidth,
            startX: textX,
            startY: this.yPosition
        });

        this.yPosition = Math.max(this.yPosition + imageHeight, finalY) + 10;

        if (addPageAfter) {
            this.doc.addPage();
            this.yPosition = 20;
        }
    }

    addParagraph(content: string, options: SectionOptions = {}): void {
        this.addTextContent(content, {
            contentFontSize: 12,
            ...options
        });
    }
}

export default CreatePDFReport;
scripts/calculator/index.ts
export * from './PricingFormulas';
export * from './ParsingHelpers';
export * from './Constants';
export { default as savingsPlansMap } from './PricingData';
export { default as CreatePDFReport } from './CreatePDFReport';
scripts/calculator/ParsingHelpers.ts
export const HOURS_PER_MONTH = (365 / 12) * 24;
export const WRITE_UNIT_SIZE = 1024; // 1KB
export const READ_UNIT_SIZE = 4096; // 4KB

// --- Return types ---

export interface TablestatsData {
  space_used: number;
  compression_ratio: number;
  read_count: number;
  write_count: number;
}

export interface NodetoolInfoResult {
  uptime_seconds: number;
  dc: string;
  id: string;
}

interface KeyspaceSchemaEntry {
  class: string;
  datacenters: Record<string, number>;
  tables: string[];
}

export type SchemaInfo = Record<string, KeyspaceSchemaEntry>;
export type RowSizeInfo = Record<string, Record<string, string>>;
export type TablestatsResult = Record<string, Record<string, TablestatsData>>;

// --- Compatibility types ---

interface TableCompatibilityIssue {
  indexes: string[];
  triggers: string[];
  materializedViews: string[];
}

export interface CompatibilityInfo {
  functions: number;
  aggregates: number;
  keyspaces: Record<string, Record<string, TableCompatibilityIssue>>;
}

// --- Prepared-statement compatibility types ---

export interface QueryPatternIssue {
  prepared_id: string;
  query_string: string;
}

export interface AggregationIssue extends QueryPatternIssue {
  function: string; // e.g. 'COUNT', 'MIN'
}

export interface TtlTableInfo {
  uses_ttl: true;
  ttl_values: number[];
}

export interface QueryPatternsInfo {
  lwt_in_unlogged_batch: QueryPatternIssue[];
  aggregations: AggregationIssue[];
  ttl_tables: Record<string, TtlTableInfo>; // key is "ks.table" (lowercased)
}

interface TcoSingleNode {
  instance: { monthly_cost: number; [key: string]: unknown };
  storage?: { monthly_cost: number; [key: string]: unknown };
  backup?: { monthly_cost: number; [key: string]: unknown };
  network_out?: { monthly_cost: number; [key: string]: unknown };
  network_in?: { monthly_cost: number; [key: string]: unknown };
  license?: { monthly_cost: number; [key: string]: unknown };
}

interface TcoOperations {
  operator_hours: { monthly_cost: number; [key: string]: unknown };
}

export interface TcoData {
  single_node: TcoSingleNode;
  operations: TcoOperations;
}

// --- Parsers ---

export const parseNodetoolStatus = (content: string): Map<string, number> => {
  const lines = content.split('\n');
  const datacenters = new Map<string, number>();
  let currentDC: string | null = null;
  let nodeCount = 0;

  for (const line of lines) {
    const trimmedLine = line.trim();

    if (/Datacenter\s*:/i.test(trimmedLine)) {
      if (currentDC) {
        datacenters.set(currentDC, nodeCount);
      }
      const match = trimmedLine.match(/Datacenter\s*:\s*(.+)/i);
      if (match?.[1]) {
        currentDC = match[1].trim();
        nodeCount = 0;
      }
    } else if (currentDC && (/^UN\b/i.test(trimmedLine) || /^DN\b/i.test(trimmedLine))) {
      nodeCount++;
    }
  }

  if (currentDC) {
    datacenters.set(currentDC, nodeCount);
  }

  return datacenters;
};

export const parse_nodetool_tablestats = (content: string): TablestatsResult => {
  const lines = content.split('\n');
  const data: TablestatsResult = {};
  let currentKeyspace: string | null = null;
  let currentTable: string | null = null;
  let spaceUsed: number | null = null;
  let compressionRatio: number | null = null;
  let writeCount: number | null = null;
  let readCount: number | null = null;

  for (const line of lines) {
    const trimmedLine = line.trim();

    if (trimmedLine.startsWith('Keyspace')) {
      const keyspaceMatch = trimmedLine.match(/Keyspace\s*:\s*(.+)/);
      if (keyspaceMatch?.[1]) {
        currentKeyspace = keyspaceMatch[1].trim();
        if (!data[currentKeyspace]) {
          data[currentKeyspace] = {};
        }
      } else {
        currentKeyspace = null;
      }
      currentTable = null;
    }

    if (currentKeyspace && (trimmedLine.startsWith('Table:') || trimmedLine.startsWith('Table (index):'))) {
      const tableMatch = trimmedLine.match(/Table(?:\s*\(index\))?\s*:\s*(.+)/);
      if (tableMatch?.[1]) {
        currentTable = tableMatch[1].trim();
        spaceUsed = null;
        compressionRatio = null;
        writeCount = null;
        readCount = null;
      }
    }

    if (currentKeyspace && currentTable) {
      if (trimmedLine.includes('Space used (live):')) {
        const match = trimmedLine.match(/Space used \(live\)\s*:\s*(.+)/);
        if (match?.[1]) {
          spaceUsed = parseFloat(match[1].trim()) || 0;
        }
      } else if (trimmedLine.includes('SSTable Compression Ratio:')) {
        const match = trimmedLine.match(/SSTable Compression Ratio\s*:\s*(.+)/);
        if (match?.[1]) {
          const parsed = parseFloat(match[1].trim());
          compressionRatio = (!isNaN(parsed) && parsed > 0) ? parsed : 1;
        }
      } else if (trimmedLine.includes('Local read count:')) {
        const match = trimmedLine.match(/Local read count\s*:\s*(.+)/);
        if (match?.[1]) {
          readCount = parseFloat(match[1].trim()) || 0;
        }
      } else if (trimmedLine.includes('Local write count:')) {
        const match = trimmedLine.match(/Local write count\s*:\s*(.+)/);
        if (match?.[1]) {
          writeCount = parseFloat(match[1].trim()) || 0;
        }

        if (
          spaceUsed !== null &&
          compressionRatio !== null &&
          readCount !== null &&
          writeCount !== null
        ) {
          data[currentKeyspace][currentTable] = {
            space_used: spaceUsed,
            compression_ratio: compressionRatio,
            read_count: readCount,
            write_count: writeCount,
          };
          currentTable = null;
          spaceUsed = null;
          compressionRatio = null;
          writeCount = null;
          readCount = null;
        }
      }
    }
  }

  return data;
};

export const parseNodetoolInfo = (content: string): NodetoolInfoResult => {
  const lines = content.split('\n');
  let uptimeSeconds = 1;
  let id = '';
  let dc = '';

  for (const line of lines) {
    const trimmedLine = line.trim();

    if (/Uptime\s*\(seconds\)/i.test(trimmedLine)) {
      const match = trimmedLine.match(/Uptime\s*\(seconds\)\s*:\s*(.+)/i);
      if (match?.[1]) {
        const parsed = parseFloat(match[1].trim());
        if (isNaN(parsed)) {
          throw new Error(`Error parsing uptime in seconds: ${match[1].trim()}`);
        }
        uptimeSeconds = parsed;
      }
    }

    if (/^ID\s*:/i.test(trimmedLine)) {
      const match = trimmedLine.match(/^ID\s*:\s*(.+)/i);
      if (match?.[1]) {
        id = match[1].trim();
      }
    }

    if (/Data\s+Center\s*:/i.test(trimmedLine)) {
      const match = trimmedLine.match(/Data\s+Center\s*:\s*(.+)/i);
      if (match?.[1]) {
        dc = match[1].trim();
      }
    }
  }

  return { uptime_seconds: uptimeSeconds, dc, id };
};

export const parse_cassandra_schema = (schemaContent: string, datacenter: string): SchemaInfo => {
  const ksPattern = /CREATE KEYSPACE (\w+)\s+WITH replication = \{[^}]*'class': '(\w+)'(?:,\s*)?([^}]*)\}/gi;
  const tablePattern = /CREATE TABLE (\w+)\.(\w+)/gi;

  const keyspaces: Array<{ name: string; class: string; rest: string }> = [];
  let ksMatch: RegExpExecArray | null;
  while ((ksMatch = ksPattern.exec(schemaContent)) !== null) {
    keyspaces.push({ name: ksMatch[1], class: ksMatch[2], rest: ksMatch[3] });
  }

  const tables: Array<{ keyspace: string; table: string }> = [];
  let tableMatch: RegExpExecArray | null;
  while ((tableMatch = tablePattern.exec(schemaContent)) !== null) {
    tables.push({ keyspace: tableMatch[1], table: tableMatch[2] });
  }

  const ksInfo: SchemaInfo = {};
  for (const ks of keyspaces) {
    const dcRepl: Record<string, number> = {};
    if (ks.class === 'NetworkTopologyStrategy') {
      const dcEntries = ks.rest.match(/'([^']+)':\s*'(\d+)'/g);
      if (dcEntries) {
        for (const entry of dcEntries) {
          const entryMatch = entry.match(/'([^']+)':\s*'(\d+)'/);
          if (entryMatch) {
            dcRepl[entryMatch[1]] = parseInt(entryMatch[2], 10);
          }
        }
      }
    } else if (ks.class === 'SimpleStrategy') {
      const rfMatch = ks.rest.match(/'replication_factor':\s*'(\d+)'/);
      if (rfMatch) {
        dcRepl[datacenter] = parseInt(rfMatch[1], 10);
      }
    }
    ksInfo[ks.name] = { class: ks.class, datacenters: dcRepl, tables: [] };
  }

  for (const table of tables) {
    if (ksInfo[table.keyspace]) {
      ksInfo[table.keyspace].tables.push(table.table);
    }
  }

  return ksInfo;
};

/**
 * Scan a CQL schema dump for features unsupported by Amazon Keyspaces:
 *   - CREATE INDEX        → table-level
 *   - CREATE TRIGGER      → table-level
 *   - CREATE MATERIALIZED VIEW → table-level (attached to the base table)
 *   - CREATE FUNCTION     → keyspace-level (counted globally)
 *   - CREATE AGGREGATE    → keyspace-level (counted globally)
 */
export const parse_cassandra_schema_compatibility = (schemaContent: string): CompatibilityInfo => {
  const result: CompatibilityInfo = {
    functions: 0,
    aggregates: 0,
    keyspaces: {},
  };

  const ensureTable = (ks: string, table: string): TableCompatibilityIssue => {
    if (!result.keyspaces[ks]) result.keyspaces[ks] = {};
    if (!result.keyspaces[ks][table]) {
      result.keyspaces[ks][table] = { indexes: [], triggers: [], materializedViews: [] };
    }
    return result.keyspaces[ks][table];
  };

  // CREATE [CUSTOM] INDEX [IF NOT EXISTS] <name> ON <ks>.<table> ...
  const indexPattern = /CREATE\s+(?:CUSTOM\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(\w+)\.(\w+)/gi;
  let m: RegExpExecArray | null;
  while ((m = indexPattern.exec(schemaContent)) !== null) {
    const [, indexName, ks, table] = m;
    ensureTable(ks, table).indexes.push(indexName);
  }

  // CREATE TRIGGER [IF NOT EXISTS] <name> ON <ks>.<table> ...
  const triggerPattern = /CREATE\s+TRIGGER\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(\w+)\.(\w+)/gi;
  while ((m = triggerPattern.exec(schemaContent)) !== null) {
    const [, triggerName, ks, table] = m;
    ensureTable(ks, table).triggers.push(triggerName);
  }

  // CREATE MATERIALIZED VIEW [IF NOT EXISTS] <ks>.<view> AS SELECT ... FROM <ks>.<base_table>
  const mvPattern = /CREATE\s+MATERIALIZED\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\.(\w+)\s+AS\s+SELECT\s+[\s\S]*?\s+FROM\s+(\w+)\.(\w+)/gi;
  while ((m = mvPattern.exec(schemaContent)) !== null) {
    const [, , viewName, baseKs, baseTable] = m;
    ensureTable(baseKs, baseTable).materializedViews.push(viewName);
  }

  // CREATE [OR REPLACE] FUNCTION [IF NOT EXISTS] <ks>.<name> ...
  const functionPattern = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\.(\w+)/gi;
  while ((m = functionPattern.exec(schemaContent)) !== null) {
    result.functions++;
  }

  // CREATE [OR REPLACE] AGGREGATE [IF NOT EXISTS] <ks>.<name> ...
  const aggregatePattern = /CREATE\s+(?:OR\s+REPLACE\s+)?AGGREGATE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\.(\w+)/gi;
  while ((m = aggregatePattern.exec(schemaContent)) !== null) {
    result.aggregates++;
  }

  return result;
};

/**
 * Parse the output of `SELECT JSON * FROM system.prepared_statements` (or
 * the newline-delimited JSON produced by prepared-statements-sampler.sh)
 * and detect Amazon Keyspaces compatibility concerns in the query text:
 *
 *   - LWT inside `BEGIN UNLOGGED BATCH` (not supported)
 *   - Aggregate function calls (COUNT / MIN / MAX / SUM / AVG — not supported)
 *   - `USING TTL <n>` per target table (informational — used to populate
 *     has_ttl for pricing when the base schema has no default TTL)
 *
 * UDF usage is intentionally not detected here — `CREATE FUNCTION` in the
 * schema is the source of truth, surfaced by parse_cassandra_schema_compatibility.
 *
 * Accepts either:
 *   - NDJSON (one JSON object per line), or
 *   - raw cqlsh output containing `{...}` lines mixed with header/footer
 *     noise (non-JSON lines are skipped).
 */
export const parse_prepared_statements = (content: string): QueryPatternsInfo => {
  const result: QueryPatternsInfo = {
    lwt_in_unlogged_batch: [],
    aggregations: [],
    ttl_tables: {},
  };

  const aggNames = ['COUNT', 'MIN', 'MAX', 'SUM', 'AVG'];

  for (const rawLine of content.split('\n')) {
    const line = rawLine.trim();
    if (!line.startsWith('{') || !line.endsWith('}')) continue;

    let stmt: { prepared_id?: string; query_string?: string; logged_keyspace?: string | null };
    try {
      stmt = JSON.parse(line);
    } catch {
      continue;
    }

    const query = stmt.query_string ?? '';
    const preparedId = stmt.prepared_id ?? '';
    if (!query) continue;
    const issueRef: QueryPatternIssue = { prepared_id: preparedId, query_string: query };

    // 1. LWT inside UNLOGGED BATCH
    //    BEGIN UNLOGGED BATCH ... (IF NOT EXISTS | IF EXISTS | IF <col>=) ... APPLY BATCH
    const unloggedBatch = /\bBEGIN\s+UNLOGGED\s+BATCH\b([\s\S]*?)\bAPPLY\s+BATCH\b/i.exec(query);
    if (unloggedBatch && /\bIF\s+(NOT\s+EXISTS\b|EXISTS\b|\w+\s*[=<>!])/i.test(unloggedBatch[1])) {
      result.lwt_in_unlogged_batch.push(issueRef);
    }

    // 2. Aggregations — look in SELECT projection. Simplest: any occurrence
    //    of a supported aggregate name followed by '(' inside a SELECT query.
    //    Guarded with a \b boundary + whitespace-tolerant '(' to avoid
    //    matching column names that happen to contain these words.
    if (/\bSELECT\b/i.test(query)) {
      for (const fn of aggNames) {
        const re = new RegExp(`\\b${fn}\\s*\\(`, 'i');
        if (re.test(query)) {
          result.aggregations.push({ ...issueRef, function: fn });
          break; // one finding per statement is enough
        }
      }
    }

    // 3. USING TTL per target table
    //    Match each USING ... TTL <n> occurrence, then associate it with the
    //    nearest preceding INSERT INTO / UPDATE target table.
    const ttlPattern = /\bUSING\b[\s\S]*?\bTTL\s+(\d+)/gi;
    let tm: RegExpExecArray | null;
    while ((tm = ttlPattern.exec(query)) !== null) {
      const ttlValue = parseInt(tm[1], 10);
      const before = query.substring(0, tm.index);
      // Find the last INSERT INTO / UPDATE before this USING TTL clause.
      const targetPattern = /\b(?:INSERT\s+INTO|UPDATE)\s+(?:"?(\w+)"?\.)?"?(\w+)"?/gi;
      let tgt: RegExpExecArray | null;
      let lastMatch: RegExpExecArray | null = null;
      while ((tgt = targetPattern.exec(before)) !== null) lastMatch = tgt;
      if (!lastMatch) continue;
      const ks = (lastMatch[1] ?? stmt.logged_keyspace ?? '').toLowerCase();
      const tbl = lastMatch[2].toLowerCase();
      if (!ks || !tbl) continue;
      const key = `${ks}.${tbl}`;
      if (!result.ttl_tables[key]) {
        result.ttl_tables[key] = { uses_ttl: true, ttl_values: [] };
      }
      if (!result.ttl_tables[key].ttl_values.includes(ttlValue)) {
        result.ttl_tables[key].ttl_values.push(ttlValue);
      }
    }
  }

  return result;
};

export const parseRowSizeInfo = (content: string): RowSizeInfo => {
  const lines = content.split('\n');
  const result: RowSizeInfo = {};

  for (const line of lines) {
    const trimmedLine = line.trim();

    if (!/=/.test(trimmedLine) || /NoHostAvailable/i.test(trimmedLine)) {
      continue;
    }

    const match = trimmedLine.match(/^(.+?)\s*=\s*(.+)$/);
    if (!match) continue;

    const [, keyName, right] = match;
    const trimmedKeyName = keyName.trim();
    const trimmedRight = right.trim();
    if (!trimmedRight.startsWith('{') || !trimmedRight.endsWith('}')) {
      continue;
    }

    const inner = trimmedRight.slice(1, -1).trim();
    const fields = inner.split(',');
    const valueDict: Record<string, string> = {};
    for (const field of fields) {
      const trimmedField = field.trim();
      if (!/:\s*/.test(trimmedField)) continue;
      const [k, v] = trimmedField.split(':');
      if (!k || !v) continue;
      valueDict[k.trim()] = v.replace('bytes', '').trim();
    }
    result[trimmedKeyName] = valueDict;
  }
  return result;
};

// --- File type detection ---

export type CassandraFileType = 'tablestats' | 'status' | 'info' | 'rowsize' | 'schema' | 'tco' | 'prepared' | 'unknown';

export interface CassandraFileScan {
  tablestats: string[];   // filenames (multiple nodes allowed)
  status: string | null;
  info: string[];         // filenames (one per node)
  rowsize: string | null;
  schema: string | null;
  tco: string | null;
  prepared: string | null;
  unknown: string[];
}

export const isTablestatsFile = (content: string): boolean =>
  /Keyspace\s*:/i.test(content) && /Space used \(live\)/i.test(content);

export const isStatusFile = (content: string): boolean =>
  /Datacenter\s*:/i.test(content) && /^(U|D)(N|L|J|M)\s+/m.test(content);

export const isInfoFile = (content: string): boolean =>
  /^ID\s*:/im.test(content) && /Uptime\s*\(seconds\)/i.test(content);

export const isRowSizeFile = (content: string): boolean =>
  /\w+\.\w+\s*=\s*\{/.test(content) && /average\s*:\s*\d+\s*bytes/i.test(content);

export const isSchemaFile = (content: string): boolean =>
  /CREATE\s+(KEYSPACE|TABLE)/i.test(content);

export const isTcoFile = (content: string): boolean => {
  try {
    const obj = JSON.parse(content);
    return !!(obj?.single_node && obj?.operations);
  } catch {
    return false;
  }
};

// NDJSON (or cqlsh SELECT JSON) output of system.prepared_statements — any
// line containing a JSON object with both `prepared_id` and `query_string`
// keys counts as a match.
export const isPreparedStatementsFile = (content: string): boolean => {
  for (const rawLine of content.split('\n')) {
    const line = rawLine.trim();
    if (!line.startsWith('{') || !line.endsWith('}')) continue;
    try {
      const obj = JSON.parse(line);
      if (obj && typeof obj === 'object' && 'prepared_id' in obj && 'query_string' in obj) {
        return true;
      }
    } catch { /* skip */ }
  }
  return false;
};

export const detectFileType = (content: string): CassandraFileType => {
  if (isPreparedStatementsFile(content)) return 'prepared';
  if (isTablestatsFile(content)) return 'tablestats';
  if (isStatusFile(content)) return 'status';
  if (isInfoFile(content)) return 'info';
  if (isRowSizeFile(content)) return 'rowsize';
  if (isSchemaFile(content)) return 'schema';
  if (isTcoFile(content)) return 'tco';
  return 'unknown';
};

// Classify a set of files (filename → content) into their respective roles.
export const scanCassandraFiles = (files: Record<string, string>): CassandraFileScan => {
  const result: CassandraFileScan = {
    tablestats: [],
    status: null,
    info: [],
    rowsize: null,
    schema: null,
    tco: null,
    prepared: null,
    unknown: [],
  };

  for (const [name, content] of Object.entries(files)) {
    switch (detectFileType(content)) {
      case 'tablestats': result.tablestats.push(name); break;
      case 'status':     result.status  ??= name; break;
      case 'info':       result.info.push(name); break;
      case 'rowsize':    result.rowsize ??= name; break;
      case 'schema':     result.schema  ??= name; break;
      case 'tco':        result.tco     ??= name; break;
      case 'prepared':   result.prepared ??= name; break;
      default:           result.unknown.push(name); break;
    }
  }

  return result;
};

export const parseTCOInfo = (data: string): TcoData => {
  let obj: TcoData;
  try {
    obj = JSON.parse(data) as TcoData;
  } catch (err: unknown) {
    throw new Error(`Invalid JSON: ${(err as Error).message}`);
  }

  if (!obj.single_node || !obj.operations) {
    throw new Error("Invalid structure: expected 'single_node' and 'operations' fields");
  }
  if (!obj.single_node.instance || typeof obj.single_node.instance.monthly_cost !== 'number') {
    throw new Error("Invalid or missing 'instance.monthly_cost'");
  }
  if (!obj.operations.operator_hours || typeof obj.operations.operator_hours.monthly_cost !== 'number') {
    throw new Error("Invalid or missing 'operations.operator_hours.monthly_cost'");
  }

  return obj;
};
scripts/calculator/PricingData.js
import savingsPlansDataJson from '../../assets/data/savings-plans.json';
import regionsDataJson from '../../assets/data/regions.json';

function savingsPlansMap() {
  const savingsPlansDataMap = {};

  for (const savingsPlan of savingsPlansDataJson.searchResults) {
    const usageType = savingsPlan.unit.replace(/-/g, '');
    const rate = savingsPlan.rate;
    const properties = savingsPlan.properties;

    let region = null;
    for (const property of properties) {
      if (property.name === 'region') {
        region = property.value;
      }
    }
    if (!region) continue;

    const longRegionName = regionsDataJson[region];

    let regionSavingsPlans = {};
    if (longRegionName in savingsPlansDataMap) {
      regionSavingsPlans = savingsPlansDataMap[longRegionName];
    }

    regionSavingsPlans[usageType] = {
      usageType: usageType,
      rate: rate,
      region: region,
      longRegionName: longRegionName,
    };

    savingsPlansDataMap[longRegionName] = regionSavingsPlans;
  }

  return savingsPlansDataMap;
}

export default savingsPlansMap();
scripts/calculator/PricingFormulas.ts
import pricingDataJson from '../../assets/data/mcs.json';
import { system_keyspaces, REPLICATION_FACTOR, SECONDS_PER_MONTH, GIGABYTE } from './Constants';
import { HOURS_PER_MONTH } from './ParsingHelpers';
import savingsPlansMap from './PricingData';

// --- Type definitions ---

export interface RegionPricing {
  readRequestPrice: number;
  writeRequestPrice: number;
  writeRequestPricePerHour: number;
  readRequestPricePerHour: number;
  storagePricePerGB: number;
  pitrPricePerGB: number;
  ttlDeletesPrice: number;
}

interface TablestatsTableData {
  space_used?: number;
  compression_ratio?: number;
  read_count?: number;
  write_count?: number;
}

interface SchemaKeyspaceInfo {
  datacenters: Record<string, number>;
}

interface NodePayload {
  tablestats_data: Record<string, Record<string, TablestatsTableData>>;
  schema?: Record<string, SchemaKeyspaceInfo>;
  info_data: { uptime_seconds: number };
  row_size_data: Record<string, { average?: string; 'default-ttl'?: string }>;
}

export type Samples = Record<string, Record<string, NodePayload>>;

export interface TableMetrics {
  table_name: string;
  total_compressed_bytes: number;
  total_uncompressed_bytes: number;
  avg_row_size_bytes: number;
  writes_monthly: number;
  reads_monthly: number;
  has_ttl: boolean;
  sample_count: number;
}

interface DcTables {
  number_of_nodes: number;
  replication_factor: number;
  tables: Record<string, TableMetrics>;
}

interface KeyspaceInSet {
  type: 'system' | 'user';
  dcs: Record<string, DcTables>;
}

export interface CassandraLocalSet {
  data: {
    keyspaces: Record<string, KeyspaceInSet>;
  };
}

export interface KeyspaceAggregate {
  keyspace_name: string;
  keyspace_type: 'system' | 'user';
  replication_factor: number;
  total_live_space_gb?: number;
  uncompressed_single_replica_gb: number;
  avg_write_row_size_bytes: number;
  avg_read_row_size_bytes: number;
  writes_per_second: number;
  reads_per_second: number;
  ttls_per_second: number;
  use_backup?: boolean;
}

export type EstimateResults = Record<string, Record<string, KeyspaceAggregate>>;

export interface DatacenterRef {
  name: string;
  nodeCount?: number;
}

export interface KeyspaceCostEntry {
  name: string;
  storage: number;
  backup: number;
  reads_provisioned: number;
  writes_provisioned: number;
  reads_provisioned_savings: number;
  writes_provisioned_savings: number;
  reads_on_demand: number;
  writes_on_demand: number;
  reads_on_demand_savings: number;
  writes_on_demand_savings: number;
  ttlDeletes: number;
  provisioned_total: number;
  on_demand_total: number;
  provisioned_total_savings: number;
  on_demand_total_savings: number;
}

export interface DatacenterCost {
  region: string;
  keyspaceCosts: Record<string, KeyspaceCostEntry>;
  total_datacenter_provisioned_cost: number;
  total_datacenter_on_demand_cost: number;
  total_datacenter_provisioned_cost_savings: number;
  total_datacenter_on_demand_cost_savings: number;
}

export interface PricingEstimateResult {
  total_datacenter_cost: Record<string, DatacenterCost>;
  total_monthly_provisioned_cost: number;
  total_monthly_on_demand_cost: number;
  total_monthly_provisioned_cost_savings: number;
  total_monthly_on_demand_cost_savings: number;
}

export interface FormDataRegion {
  averageRowSizeInBytes?: number;
  averageReadRequestsPerSecond?: number;
  averageWriteRequestsPerSecond?: number;
  averageTtlDeletesPerSecond?: number;
  storageSizeInGb?: number;
  pointInTimeRecoveryForBackups?: boolean;
}

export interface MultiRegionOption {
  value: string;
}

export interface KeyspacesEstimateInput {
  datacenters: DatacenterRef[];
  regions: Record<string, string>;
  estimateResults: EstimateResults;
}

export interface ProvisionedPricing {
  strongConsistencyReads: number;
  strongConsistencyWrites: number;
  eventualConsistencyReads: number;
  eventualConsistencyWrites: number;
  strongConsistencyReadsSavings: number;
  strongConsistencyWritesSavings: number;
  eventualConsistencyReadsSavings: number;
  eventualConsistencyWritesSavings: number;
  strongConsistencyStorage: number;
  strongConsistencyBackup: number;
  eventualConsistencyStorage: number;
  eventualConsistencyBackup: number;
  strongConsistencyTtlDeletesPrice: number;
  eventualConsistencyTtlDeletesPrice: number;
}

export interface MapPricingResult {
  provisionedPricing: ProvisionedPricing;
  onDemandPricing: ProvisionedPricing;
}

// MCS JSON shape (minimal for region pricing lookup)
interface McsRegionProducts {
  'MCS-ReadUnits'?: { price: string };
  'MCS-WriteUnits'?: { price: string };
  'Provisioned Write Units'?: { price: string };
  'Provisioned Read Units'?: { price: string };
  'AmazonMCS - Indexed DataStore per GB-Mo'?: { price: string };
  'Point-In-Time-Restore PITR Backup Storage per GB-Mo'?: { price: string };
  'Time to Live'?: { price: string };
}

interface McsPricingJson {
  regions?: Record<string, McsRegionProducts>;
}

const pricingData = pricingDataJson as McsPricingJson;

interface SavingsPlanRate {
  rate: number;
}
type SavingsPlansMapType = Record<string, Record<string, SavingsPlanRate>>;
const savingsPlanMap = savingsPlansMap as SavingsPlansMapType;

/** Resolve pricing primitives for a region from pricing JSON. */
const getRegionPricing = (regionName: string): RegionPricing | null => {
  if (!pricingData?.regions?.[regionName]) {
    return null;
  }
  const r = pricingData.regions[regionName];
  return {
    readRequestPrice: Number(r['MCS-ReadUnits']?.price ?? 0),
    writeRequestPrice: Number(r['MCS-WriteUnits']?.price ?? 0),
    writeRequestPricePerHour: Number(r['Provisioned Write Units']?.price ?? 0),
    readRequestPricePerHour: Number(r['Provisioned Read Units']?.price ?? 0),
    storagePricePerGB: Number(r['AmazonMCS - Indexed DataStore per GB-Mo']?.price ?? 0),
    pitrPricePerGB: Number(r['Point-In-Time-Restore PITR Backup Storage per GB-Mo']?.price ?? 0),
    ttlDeletesPrice: Number(r['Time to Live']?.price ?? 0),
  };
};

/**
 * Build a normalized Cassandra dataset from raw samples and status data.
 */
export const buildCassandraLocalSet = (
  samples: Samples,
  statusData: Map<string, number>,
  opts?: { preparedTtlTables?: Set<string> }
): CassandraLocalSet => {
  const preparedTtl = opts?.preparedTtlTables;
  const result: CassandraLocalSet = {
    data: { keyspaces: {} },
  };

  for (const [dcName, dcData] of Object.entries(samples)) {
    const numberOfNodes = statusData.get(dcName);
    for (const [, nodeData] of Object.entries(dcData)) {
      const tablestatsData = nodeData.tablestats_data;
      const schema = nodeData.schema;
      const infoData = nodeData.info_data;
      const rowSizeData = nodeData.row_size_data;
      const uptimeSeconds = infoData.uptime_seconds;

      for (const [keyspaceName, keyspaceData] of Object.entries(tablestatsData)) {
        if (schema?.[keyspaceName] && !schema[keyspaceName].datacenters[dcName]) {
          continue;
        }

        if (!result.data.keyspaces[keyspaceName]) {
          result.data.keyspaces[keyspaceName] = {
            type: isSystemKeyspace(keyspaceName),
            dcs: {},
          };
        }

        let replicationFactor = REPLICATION_FACTOR;
        if (schema?.[keyspaceName]) {
          replicationFactor = schema[keyspaceName].datacenters[dcName];
        }

        if (!result.data.keyspaces[keyspaceName].dcs[dcName]) {
          result.data.keyspaces[keyspaceName].dcs[dcName] = {
            number_of_nodes: numberOfNodes ?? 0,
            replication_factor: replicationFactor,
            tables: {},
          };
        }

        for (const [tableName, tableData] of Object.entries(keyspaceData)) {
          const dcTables = result.data.keyspaces[keyspaceName].dcs[dcName].tables;
          if (!dcTables[tableName]) {
            const fullyQualifiedTableName = `${keyspaceName}.${tableName}`;
            let hasTtl = false;
            let averageBytes = 1024;
            const rowEntry = rowSizeData?.[fullyQualifiedTableName];
            if (rowEntry) {
              const avgNumber = rowEntry.average ?? '1';
              const parsedBytes = parseInt(avgNumber, 10);
              averageBytes = (isNaN(parsedBytes) || parsedBytes <= 0) ? 1 : parsedBytes;
              const ttlStr = rowEntry['default-ttl'] ?? 'y';
              hasTtl = String(ttlStr).trim() === 'y';
            }
            // Prepared-statement USING TTL evidence unions into has_ttl:
            // if any prepared statement writes TTL rows to this table, 100%
            // of its writes are treated as TTL deletes (same as a table
            // with default_time_to_live set).
            if (!hasTtl && preparedTtl && preparedTtl.has(fullyQualifiedTableName.toLowerCase())) {
              hasTtl = true;
            }
            dcTables[tableName] = {
              table_name: tableName,
              total_compressed_bytes: 0,
              total_uncompressed_bytes: 0,
              avg_row_size_bytes: averageBytes,
              writes_monthly: 0,
              reads_monthly: 0,
              has_ttl: hasTtl,
              sample_count: 0,
            };
          }

          let spaceUsed = Number(tableData.space_used) || 0;
          if (isNaN(spaceUsed)) spaceUsed = 0;
          const ratio = spaceUsed > 0 ? (tableData.compression_ratio ?? 1) : 1;
          let readCount = Number(tableData.read_count) || 0;
          let writeCount = Number(tableData.write_count) || 0;
          if (isNaN(readCount)) readCount = 0;
          if (isNaN(writeCount)) writeCount = 0;

          const table = dcTables[tableName];
          table.total_compressed_bytes += spaceUsed;
          table.total_uncompressed_bytes += calculateUncompressedStoragePerNode(spaceUsed, ratio);
          table.writes_monthly += calculateWriteOperationsPerNodePerMonth(writeCount, uptimeSeconds);
          table.reads_monthly += calculateReadOperationsPerNodePerMonth(readCount, uptimeSeconds);
          table.sample_count += 1;
        }
      }
    }
  }
  return result;
};

/**
 * Aggregate table-level metrics to keyspace-level for a specific datacenter.
 */
export const getKeyspaceCassandraAggregate = (
  cassandra_set: CassandraLocalSet,
  datacenter: string
): Record<string, KeyspaceAggregate> => {
  const keyspace_aggregate: Record<string, KeyspaceAggregate> = {};

  for (const [keyspace, keyspaceData] of Object.entries(cassandra_set.data.keyspaces)) {
    if (keyspaceData.type === 'system') continue;
    const dcData = keyspaceData.dcs[datacenter];
    if (!dcData) continue;

    const number_of_nodes = dcData.number_of_nodes;
    const replication_factor = dcData.replication_factor;

    let keyspace_writes_total = 0;
    let keyspace_reads_total = 0;
    let total_live_space = 0;
    let uncompressed_single_replica = 0;
    let write_row_size_bytes = 0;
    let read_row_size_bytes = 0;
    let keyspace_ttls_total = 0;

    for (const [, tableData] of Object.entries(dcData.tables)) {
      const sc = tableData.sample_count || 1;
      keyspace_writes_total += tableData.writes_monthly / sc;
      total_live_space += tableData.total_compressed_bytes / sc;
      uncompressed_single_replica += tableData.total_uncompressed_bytes / sc;
      write_row_size_bytes += (tableData.writes_monthly * tableData.avg_row_size_bytes) / sc;
      read_row_size_bytes += (tableData.reads_monthly * tableData.avg_row_size_bytes) / sc;
      keyspace_reads_total += tableData.reads_monthly / sc;
      keyspace_ttls_total += tableData.has_ttl ? tableData.writes_monthly / sc : 0;
    }

    const total_ops = keyspace_reads_total + keyspace_writes_total;
    const combined_row_size_bytes = (read_row_size_bytes + write_row_size_bytes) / (total_ops > 0 ? total_ops : 1);
    const average_read_row_size_bytes = keyspace_reads_total > 0
      ? read_row_size_bytes / keyspace_reads_total
      : combined_row_size_bytes;
    const average_write_row_size_bytes = keyspace_writes_total > 0
      ? write_row_size_bytes / keyspace_writes_total
      : combined_row_size_bytes;
    keyspace_aggregate[keyspace] = {
      keyspace_name: keyspace,
      keyspace_type: keyspaceData.type,
      replication_factor,
      total_live_space_gb: (total_live_space * number_of_nodes) / GIGABYTE,
      uncompressed_single_replica_gb: (uncompressed_single_replica * number_of_nodes) / replication_factor / GIGABYTE,
      avg_write_row_size_bytes: average_write_row_size_bytes,
      avg_read_row_size_bytes: average_read_row_size_bytes,
      writes_per_second: (keyspace_writes_total / SECONDS_PER_MONTH) * (number_of_nodes / replication_factor),
      reads_per_second: (keyspace_reads_total / SECONDS_PER_MONTH) * (number_of_nodes / (replication_factor - 1 > 0 ? replication_factor - 1 : 1)),
      ttls_per_second: (keyspace_ttls_total / SECONDS_PER_MONTH) * (number_of_nodes / replication_factor),
    };
  }
  return keyspace_aggregate;
};

/**
 * Compute per-datacenter and total monthly costs from keyspace aggregates.
 */
export const calculatePricingEstimate = (
  datacenters: DatacenterRef[],
  regions: Record<string, string>,
  estimateResults: EstimateResults
): PricingEstimateResult | null => {
  if (!Array.isArray(datacenters) || datacenters.length === 0) return null;

  const pricingData: Record<string, DatacenterCost> = {};
  let total_monthly_provisioned_cost = 0;
  let total_monthly_on_demand_cost = 0;
  let total_monthly_provisioned_cost_savings = 0;
  let total_monthly_on_demand_cost_savings = 0;

  datacenters.forEach((dc) => {
    const region = regions[dc.name];
    const results = estimateResults[dc.name];
    if (!results || !region) return;

    const regionPricing = getRegionPricing(region);
    if (!regionPricing) return;

    const savingsPlan = savingsPlanMap[region];
    let total_datacenter_provisioned_cost = 0;
    let total_datacenter_on_demand_cost = 0;
    let total_datacenter_provisioned_cost_savings = 0;
    let total_datacenter_on_demand_cost_savings = 0;

    const keyspaceCosts: Record<string, KeyspaceCostEntry> = {};
    keyspaceCosts['totals'] = {
      name: 'region total',
      storage: 0,
      backup: 0,
      reads_provisioned: 0,
      reads_provisioned_savings: 0,
      writes_provisioned: 0,
      writes_provisioned_savings: 0,
      reads_on_demand: 0,
      reads_on_demand_savings: 0,
      writes_on_demand: 0,
      writes_on_demand_savings: 0,
      ttlDeletes: 0,
      provisioned_total: 0,
      on_demand_total: 0,
      provisioned_total_savings: 0,
      on_demand_total_savings: 0,
    };

    Object.entries(results).forEach(([keyspace, data]) => {
      const writePrice = regionPricing.writeRequestPrice;
      const readPrice = regionPricing.readRequestPrice;
      const oneDemandWriteCost = calculateOnDemandWriteUnitsPerMonthCost(data.writes_per_second, data.avg_write_row_size_bytes, writePrice);
      const oneDemandReadCost = calculateOnDemandReadUnitsPerMonthCost(data.reads_per_second, data.avg_read_row_size_bytes, readPrice);
      const ttlDeleteCost = calculateTtlUnitsPerMonthCost(data.ttls_per_second, data.avg_write_row_size_bytes, regionPricing.ttlDeletesPrice);

      const oneDemandWriteCostWithSavings = savingsPlan
        ? calculateOnDemandWriteUnitsPerMonthCost(data.writes_per_second, data.avg_write_row_size_bytes, savingsPlan['WriteRequestUnits']?.rate ?? writePrice)
        : oneDemandWriteCost;
      const oneDemandReadCostWithSavings = savingsPlan
        ? calculateOnDemandReadUnitsPerMonthCost(data.reads_per_second, data.avg_read_row_size_bytes, savingsPlan['ReadRequestUnits']?.rate ?? readPrice)
        : oneDemandReadCost;
      const provisionedWriteCostWithSavings = savingsPlan
        ? calculateProvisionedWriteCostPerMonth(data.writes_per_second, data.avg_write_row_size_bytes, savingsPlan['WriteCapacityUnitHrs']?.rate ?? regionPricing.writeRequestPricePerHour, 0.70)
        : 0;
      const provisionedReadCostWithSavings = savingsPlan
        ? calculateProvisionedReadCostPerMonth(data.reads_per_second, data.avg_read_row_size_bytes, savingsPlan['ReadCapacityUnitHrs']?.rate ?? regionPricing.readRequestPricePerHour, 0.70)
        : 0;

      const provisionedWriteCost = calculateProvisionedWriteCostPerMonth(data.writes_per_second, data.avg_write_row_size_bytes, regionPricing.writeRequestPricePerHour, 0.70);
      const provisionedReadCost = calculateProvisionedReadCostPerMonth(data.reads_per_second, data.avg_read_row_size_bytes, regionPricing.readRequestPricePerHour, 0.70);

      const storageCost = calculateStorageCostPerMonth(data.uncompressed_single_replica_gb, regionPricing.storagePricePerGB);
      const backupCost = data.use_backup === true
        ? calculateBackupCostPerMonth(data.uncompressed_single_replica_gb, regionPricing.pitrPricePerGB)
        : 0;

      const provisioned_total_savings = calculateProvisionedCapacityTotalMonthlyCostWithAggregates(provisionedReadCostWithSavings, provisionedWriteCostWithSavings, ttlDeleteCost, storageCost, backupCost);
      const on_demand_total_savings = calculateOnDemandCapcityTotalMonthlyCostWithAggregates(oneDemandReadCostWithSavings, oneDemandWriteCostWithSavings, ttlDeleteCost, storageCost, backupCost);
      const provisioned_total = calculateProvisionedCapacityTotalMonthlyCostWithAggregates(provisionedReadCost, provisionedWriteCost, ttlDeleteCost, storageCost, backupCost);
      const on_demand_total = calculateOnDemandCapcityTotalMonthlyCostWithAggregates(oneDemandReadCost, oneDemandWriteCost, ttlDeleteCost, storageCost, backupCost);

      keyspaceCosts[keyspace] = {
        name: keyspace,
        storage: storageCost,
        backup: backupCost,
        reads_provisioned: provisionedReadCost,
        writes_provisioned: provisionedWriteCost,
        reads_provisioned_savings: provisionedReadCostWithSavings,
        writes_provisioned_savings: provisionedWriteCostWithSavings,
        reads_on_demand: oneDemandReadCost,
        writes_on_demand: oneDemandWriteCost,
        reads_on_demand_savings: oneDemandReadCostWithSavings,
        writes_on_demand_savings: oneDemandWriteCostWithSavings,
        ttlDeletes: ttlDeleteCost,
        provisioned_total,
        on_demand_total,
        provisioned_total_savings,
        on_demand_total_savings,
      };

      const totals = keyspaceCosts['totals'];
      totals.storage += storageCost;
      totals.backup += backupCost;
      totals.reads_provisioned += provisionedReadCost;
      totals.writes_provisioned += provisionedWriteCost;
      totals.reads_on_demand += oneDemandReadCost;
      totals.writes_on_demand += oneDemandWriteCost;
      totals.reads_provisioned_savings += provisionedReadCostWithSavings;
      totals.writes_provisioned_savings += provisionedWriteCostWithSavings;
      totals.reads_on_demand_savings += oneDemandReadCostWithSavings;
      totals.writes_on_demand_savings += oneDemandWriteCostWithSavings;
      totals.ttlDeletes += ttlDeleteCost;
      totals.provisioned_total += provisioned_total;
      totals.on_demand_total += on_demand_total;
      totals.provisioned_total_savings += provisioned_total_savings;
      totals.on_demand_total_savings += on_demand_total_savings;

      total_datacenter_provisioned_cost += provisioned_total;
      total_datacenter_on_demand_cost += on_demand_total;
      total_datacenter_provisioned_cost_savings += provisioned_total_savings;
      total_datacenter_on_demand_cost_savings += on_demand_total_savings;
    });

    const totals = keyspaceCosts['totals'];
    delete keyspaceCosts['totals'];
    keyspaceCosts['totals'] = totals;

    pricingData[dc.name] = {
      region,
      keyspaceCosts,
      total_datacenter_provisioned_cost,
      total_datacenter_on_demand_cost,
      total_datacenter_provisioned_cost_savings,
      total_datacenter_on_demand_cost_savings,
    };

    total_monthly_provisioned_cost += total_datacenter_provisioned_cost;
    total_monthly_on_demand_cost += total_datacenter_on_demand_cost;
    total_monthly_provisioned_cost_savings += total_datacenter_provisioned_cost_savings;
    total_monthly_on_demand_cost_savings += total_datacenter_on_demand_cost_savings;
  });

  return {
    total_datacenter_cost: pricingData,
    total_monthly_provisioned_cost,
    total_monthly_on_demand_cost,
    total_monthly_provisioned_cost_savings,
    total_monthly_on_demand_cost_savings,
  };
};

/**
 * Build datacenters, regions, and estimateResults from Keyspaces form data.
 */
export const buildKeyspacesEstimateInput = (
  formData: Record<string, FormDataRegion>,
  selectedRegion: string,
  multiSelectedRegions: MultiRegionOption[] = []
): KeyspacesEstimateInput => {
  const regionNames = [selectedRegion, ...multiSelectedRegions.map((r) => r.value)];
  const datacenters: DatacenterRef[] = regionNames.map((name) => ({ name }));
  const regions: Record<string, string> = {};
  const estimateResults: EstimateResults = {};
  regionNames.forEach((regionName) => {
    regions[regionName] = regionName;
    const d = formData[regionName] ?? formData.default ?? {};
    const storageGb = Number(d.storageSizeInGb) || 0;
    estimateResults[regionName] = {
      default: {
        keyspace_name: 'default',
        keyspace_type: 'user',
        replication_factor: 3,
        total_live_space_gb: storageGb,
        uncompressed_single_replica_gb: storageGb,
        avg_read_row_size_bytes: Number(d.averageRowSizeInBytes) || 1024,
        avg_write_row_size_bytes: Number(d.averageRowSizeInBytes) || 1024,
        reads_per_second: Number(d.averageReadRequestsPerSecond) || 0,
        writes_per_second: Number(d.averageWriteRequestsPerSecond) || 0,
        ttls_per_second: Number(d.averageTtlDeletesPerSecond) || 0,
        use_backup: !!d.pointInTimeRecoveryForBackups,
      },
    };
  });
  return { datacenters, regions, estimateResults };
};

export interface PricingTotals {
  storage: number;
  backup: number;
  ttl_deletes: number;
  reads_provisioned: number;
  writes_provisioned: number;
  reads_provisioned_savings: number;
  writes_provisioned_savings: number;
  reads_on_demand: number;
  writes_on_demand: number;
  reads_on_demand_savings: number;
  writes_on_demand_savings: number;
}

/**
 * Sum the pre-computed 'totals' row across all datacenters in a PricingEstimateResult.
 */
export const aggregatePricingTotals = (pricing: PricingEstimateResult): PricingTotals => {
  const acc: PricingTotals = {
    storage: 0, backup: 0, ttl_deletes: 0,
    reads_provisioned: 0, writes_provisioned: 0,
    reads_provisioned_savings: 0, writes_provisioned_savings: 0,
    reads_on_demand: 0, writes_on_demand: 0,
    reads_on_demand_savings: 0, writes_on_demand_savings: 0,
  };
  for (const dcCost of Object.values(pricing.total_datacenter_cost)) {
    const t = dcCost.keyspaceCosts['totals'];
    if (!t) continue;
    acc.storage                   += t.storage;
    acc.backup                    += t.backup;
    acc.ttl_deletes               += t.ttlDeletes;
    acc.reads_provisioned         += t.reads_provisioned;
    acc.writes_provisioned        += t.writes_provisioned;
    acc.reads_provisioned_savings += t.reads_provisioned_savings;
    acc.writes_provisioned_savings += t.writes_provisioned_savings;
    acc.reads_on_demand           += t.reads_on_demand;
    acc.writes_on_demand          += t.writes_on_demand;
    acc.reads_on_demand_savings   += t.reads_on_demand_savings;
    acc.writes_on_demand_savings  += t.writes_on_demand_savings;
  }
  return acc;
};

/**
 * Map calculatePricingEstimate result to the Keyspaces PricingTable shape.
 */
export const mapPricingEstimateToKeyspacesTable = (
  pricingResult: PricingEstimateResult | null
): MapPricingResult => {
  if (!pricingResult?.total_datacenter_cost) {
    const zeroPricing: ProvisionedPricing = {
      strongConsistencyReads: 0,
      strongConsistencyWrites: 0,
      eventualConsistencyReads: 0,
      eventualConsistencyWrites: 0,
      strongConsistencyReadsSavings: 0,
      strongConsistencyWritesSavings: 0,
      eventualConsistencyReadsSavings: 0,
      eventualConsistencyWritesSavings: 0,
      strongConsistencyStorage: 0,
      strongConsistencyBackup: 0,
      eventualConsistencyStorage: 0,
      eventualConsistencyBackup: 0,
      strongConsistencyTtlDeletesPrice: 0,
      eventualConsistencyTtlDeletesPrice: 0,
    };
    return { provisionedPricing: zeroPricing, onDemandPricing: zeroPricing };
  }

  const t = aggregatePricingTotals(pricingResult);
  const {
    storage: totalStorage, backup: totalBackup, ttl_deletes: totalTtl,
    reads_provisioned: readsProvisioned, writes_provisioned: writesProvisioned,
    reads_provisioned_savings: readsProvisionedSavings, writes_provisioned_savings: writesProvisionedSavings,
    reads_on_demand: readsOnDemand, writes_on_demand: writesOnDemand,
    reads_on_demand_savings: readsOnDemandSavings, writes_on_demand_savings: writesOnDemandSavings,
  } = t;

  const provisionedPricing: ProvisionedPricing = {
    strongConsistencyReads: readsProvisioned,
    strongConsistencyWrites: writesProvisioned,
    eventualConsistencyReads: readsProvisioned / 2,
    eventualConsistencyWrites: writesProvisioned,
    strongConsistencyReadsSavings: readsProvisionedSavings,
    strongConsistencyWritesSavings: writesProvisionedSavings,
    eventualConsistencyReadsSavings: readsProvisionedSavings / 2,
    eventualConsistencyWritesSavings: writesProvisionedSavings,
    strongConsistencyStorage: totalStorage,
    strongConsistencyBackup: totalBackup,
    eventualConsistencyStorage: totalStorage,
    eventualConsistencyBackup: totalBackup,
    strongConsistencyTtlDeletesPrice: totalTtl,
    eventualConsistencyTtlDeletesPrice: totalTtl,
  };
  const onDemandPricing: ProvisionedPricing = {
    strongConsistencyReads: readsOnDemand,
    strongConsistencyWrites: writesOnDemand,
    eventualConsistencyReads: readsOnDemand / 2,
    eventualConsistencyWrites: writesOnDemand,
    strongConsistencyReadsSavings: readsOnDemandSavings,
    strongConsistencyWritesSavings: writesOnDemandSavings,
    eventualConsistencyReadsSavings: readsOnDemandSavings / 2,
    eventualConsistencyWritesSavings: writesOnDemandSavings,
    strongConsistencyStorage: totalStorage,
    strongConsistencyBackup: totalBackup,
    eventualConsistencyStorage: totalStorage,
    eventualConsistencyBackup: totalBackup,
    strongConsistencyTtlDeletesPrice: totalTtl,
    eventualConsistencyTtlDeletesPrice: totalTtl,
  };
  return { provisionedPricing, onDemandPricing };
};

// --- Cassandra / price formulas ---

export const isSystemKeyspace = (keyspaceName: string): 'system' | 'user' =>
  system_keyspaces.has(keyspaceName) ? 'system' : 'user';

export const calculateWriteOperationsPerNodePerMonth = (total_writes_per_node: number, node_uptime_seconds: number): number =>
  (total_writes_per_node / node_uptime_seconds) * SECONDS_PER_MONTH;

export const calculateReadOperationsPerNodePerMonth = (total_reads_per_node: number, node_uptime_seconds: number): number =>
  (total_reads_per_node / node_uptime_seconds) * SECONDS_PER_MONTH;

export const calculateUncompressedStoragePerNode = (table_live_space_gb: number, compression_ratio: number): number =>
  table_live_space_gb / compression_ratio;

export const calculateOnDemandCapcityTotalMonthlyCostWithAggregates = (
  onDemandReadCost: number,
  onDemandWriteCost: number,
  onDemandTtlDeleteCost: number,
  storageCost: number,
  backupCost: number
): number =>
  onDemandReadCost + onDemandWriteCost + onDemandTtlDeleteCost + storageCost + backupCost;

export const calculateProvisionedCapacityTotalMonthlyCostWithAggregates = (
  provisionedReadCost: number,
  provisionedWriteCost: number,
  ttlDeleteCost: number,
  storageCost: number,
  backupCost: number
): number =>
  provisionedReadCost + provisionedWriteCost + ttlDeleteCost + storageCost + backupCost;

export const calculateProvisionedReadCostPerMonth = (
  reads_per_second: number,
  avg_read_row_size_bytes: number,
  readRequestPricePerHour: number,
  target_utilization: number
): number =>
  (reads_per_second * calculateReadUnitsPerOperation(avg_read_row_size_bytes) * HOURS_PER_MONTH * readRequestPricePerHour) / target_utilization;

export const calculateProvisionedWriteCostPerMonth = (
  writes_per_second: number,
  avg_write_row_size_bytes: number,
  writeRequestPricePerHour: number,
  target_utilization = 0.7
): number =>
  (writes_per_second * calculateWriteUnitsPerOperation(avg_write_row_size_bytes) * HOURS_PER_MONTH * writeRequestPricePerHour) / target_utilization;

export const calculateStorageCostPerMonth = (uncompressed_single_replica_gb: number, storagePricePerGB: number): number =>
  uncompressed_single_replica_gb * storagePricePerGB;

export const calculateBackupCostPerMonth = (uncompressed_single_replica_gb: number, pitrPricePerGB: number): number =>
  uncompressed_single_replica_gb * pitrPricePerGB;

export const calculateOnDemandReadUnitsPerMonthCost = (
  reads_per_second: number,
  avg_read_row_size_bytes: number,
  onDemandReadPrice: number
): number =>
  calculateOnDemandReadUnitsPerMonth(reads_per_second, avg_read_row_size_bytes) * onDemandReadPrice;

export const calculateOnDemandWriteUnitsPerMonthCost = (
  writes_per_second: number,
  avg_write_row_size_bytes: number,
  onDemandWritePrice: number
): number =>
  calculateOnDemandWriteUnitsPerMonth(writes_per_second, avg_write_row_size_bytes) * onDemandWritePrice;

export const calculateTtlUnitsPerMonthCost = (
  ttls_per_second: number,
  avg_write_row_size_bytes: number,
  ttlDeletesPrice: number
): number =>
  calculateOnDemandTtlUnitsPerMonth(ttls_per_second, avg_write_row_size_bytes) * ttlDeletesPrice;

export const calculateOnDemandReadUnitsPerMonth = (reads_per_second: number, avg_read_row_size_bytes: number): number =>
  reads_per_second * calculateReadUnitsPerOperation(avg_read_row_size_bytes) * SECONDS_PER_MONTH;

/** @deprecated Use calculateOnDemandReadUnitsPerMonth instead. */
export const calcualteOnDemandReadUnitsPerMonth = calculateOnDemandReadUnitsPerMonth;

export const calculateOnDemandWriteUnitsPerMonth = (writes_per_second: number, avg_write_row_size_bytes: number): number =>
  writes_per_second * calculateWriteUnitsPerOperation(avg_write_row_size_bytes) * SECONDS_PER_MONTH;

export const calculateOnDemandTtlUnitsPerMonth = (ttls_per_second: number, avg_write_row_size_bytes: number): number =>
  ttls_per_second * calculateTtlUnitsPerOperation(avg_write_row_size_bytes) * SECONDS_PER_MONTH;

export const calculateWriteUnitsPerOperation = (avg_write_row_size_bytes: number): number =>
  Math.ceil(avg_write_row_size_bytes / 1024);

export const calculateReadUnitsPerOperation = (avg_read_row_size_bytes: number): number =>
  Math.ceil(avg_read_row_size_bytes / 4096);

export const calculateTtlUnitsPerOperation = (avg_write_row_size_bytes: number): number =>
  Math.ceil(avg_write_row_size_bytes / 1024);

export default calculatePricingEstimate;
scripts/check-compatibility.ts
#!/usr/bin/env npx ts-node
/**
 * check-compatibility.ts
 *
 * Checks Cassandra inputs against Amazon Keyspaces feature support and
 * emits a JSON compatibility report.
 *
 * Schema (CQL DDL): reports CREATE INDEX, CREATE TRIGGER, CREATE
 *   MATERIALIZED VIEW, CREATE FUNCTION, and CREATE AGGREGATE as
 *   incompatibilities.
 *
 * Prepared statements (NDJSON / cqlsh `SELECT JSON * FROM
 *   system.prepared_statements`): additionally reports
 *     - LWT inside `BEGIN UNLOGGED BATCH`
 *     - Aggregate calls (COUNT / MIN / MAX / SUM / AVG)
 *     - `USING TTL` per target table (informational; not an issue)
 *
 * Detection delegated to ParsingHelpers.ts.
 *
 * Usage:
 *   npx ts-node --require tsconfig-paths/register --project tsconfig.scripts.json \
 *     scripts/check-compatibility.ts \
 *     [--schema <path>] [--prepared <path>]
 *
 *   # CQL on stdin (only valid with no --prepared flag)
 *   cat schema.cql | npx ts-node --require tsconfig-paths/register --project tsconfig.scripts.json \
 *     scripts/check-compatibility.ts
 */

import fs from 'fs';

import {
  parse_cassandra_schema_compatibility,
  parse_prepared_statements,
  type CompatibilityInfo,
  type QueryPatternsInfo,
} from './calculator/ParsingHelpers';

interface Args {
  schema: string | null;
  prepared: string | null;
}

function parseArgs(argv: string[]): Args {
  const args: Args = { schema: null, prepared: null };
  let i = 0;
  while (i < argv.length) {
    switch (argv[i]) {
      case '--schema':   args.schema   = argv[++i]; break;
      case '--prepared': args.prepared = argv[++i]; break;
    }
    i++;
  }
  return args;
}

function readStdinSync(): string {
  try {
    return fs.readFileSync(0, 'utf8');
  } catch {
    return '';
  }
}

function summarizeSchema(details: CompatibilityInfo) {
  let total_table_issues = 0;
  let tables_affected = 0;
  const keyspaces_affected = Object.keys(details.keyspaces).length;

  for (const tables of Object.values(details.keyspaces)) {
    for (const issue of Object.values(tables)) {
      const count = issue.indexes.length + issue.triggers.length + issue.materializedViews.length;
      if (count > 0) {
        tables_affected++;
        total_table_issues += count;
      }
    }
  }

  const total_issues = total_table_issues + details.functions + details.aggregates;

  return {
    total_issues,
    keyspaces_affected,
    tables_affected,
    functions: details.functions,
    aggregates: details.aggregates,
  };
}

function summarizeQueryPatterns(p: QueryPatternsInfo) {
  return {
    lwt_in_unlogged_batch: p.lwt_in_unlogged_batch.length,
    aggregations:          p.aggregations.length,
    ttl_tables:            Object.keys(p.ttl_tables).length,
  };
}

function main() {
  const args = parseArgs(process.argv.slice(2));

  let schemaContent: string | null = null;
  if (args.schema) {
    try { schemaContent = fs.readFileSync(args.schema, 'utf8'); }
    catch (err: unknown) {
      console.error('Failed to read schema file: file not found or unreadable');
      process.exit(1);
    }
  } else if (!args.prepared && !process.stdin.isTTY) {
    // Backwards compat: stdin = schema CQL (only when no other source given)
    const stdinContent = readStdinSync();
    if (stdinContent) schemaContent = stdinContent;
  }

  let preparedContent: string | null = null;
  if (args.prepared) {
    try { preparedContent = fs.readFileSync(args.prepared, 'utf8'); }
    catch (err: unknown) {
      console.error('Failed to read prepared statements file: file not found or unreadable');
      process.exit(1);
    }
  }

  if (!schemaContent && !preparedContent) {
    console.error('Usage: check-compatibility.ts [--schema <path>] [--prepared <path>]');
    console.error('       cat schema.cql | check-compatibility.ts');
    process.exit(1);
  }

  // Schema-driven findings
  const schemaDetails: CompatibilityInfo | null = schemaContent
    ? parse_cassandra_schema_compatibility(schemaContent)
    : null;

  // Prepared-statement findings
  const queryDetails: QueryPatternsInfo | null = preparedContent
    ? parse_prepared_statements(preparedContent)
    : null;

  const schemaSummary = schemaDetails ? summarizeSchema(schemaDetails) : null;
  const querySummary = queryDetails ? summarizeQueryPatterns(queryDetails) : null;

  const total_issues =
    (schemaSummary?.total_issues ?? 0) +
    (querySummary?.lwt_in_unlogged_batch ?? 0) +
    (querySummary?.aggregations ?? 0);

  const result = {
    source: 'compatibility-check',
    input: { schema: args.schema, prepared: args.prepared },
    has_issues: total_issues > 0,
    summary: {
      total_issues,
      schema: schemaSummary,
      query_patterns: querySummary,
    },
    details: {
      schema: schemaDetails,
      query_patterns: queryDetails,
    },
  };

  console.log(JSON.stringify(result, null, 2));
}

main();
scripts/generate-pdf.ts
#!/usr/bin/env node
/**
 * generate-pdf.ts — Node.js entry point for PDF generation.
 *
 * Reads one or more pricing-estimate JSON documents (output of
 * `calculate.ts` / `parse-cassandra.ts`) and writes a PDF report to disk.
 *
 * Single estimate:
 *     # via stdin (backwards-compat)
 *     npx ts-node ... scripts/parse-cassandra.ts ... \
 *       | npx ts-node ... scripts/generate-pdf.ts
 *
 *     # via flag
 *     npx ts-node ... scripts/generate-pdf.ts --input /tmp/keyspaces-calc.json
 *
 * Multiple estimates (consolidated into a single comparison report):
 *     npx ts-node ... scripts/generate-pdf.ts \
 *         --input /tmp/optA.json --label "Option A" \
 *         --input /tmp/optB.json --label "Option B" \
 *         --input /tmp/optC.json --label "Option C" \
 *         --output /tmp/comparison.pdf
 *
 * All flags:
 *   --input <path>    Path to a calculate.ts / parse-cassandra.ts JSON file. Repeatable.
 *   --label <name>    Display label for the most recent --input. Optional.
 *   --output <path>   PDF output path (default: ./keyspaces-pricing-estimate.pdf).
 *   -h | --help       Show usage.
 *
 * Delegates all PDF rendering to src/calculator/CreatePDFReport.ts.
 */

import fs from 'fs';
import path from 'path';
import CreatePDFReport, {
    type CompatibilityData,
    type Estimate,
} from './calculator/CreatePDFReport';

// ---------------------------------------------------------------------------
// Subclass that writes the finished PDF to the filesystem instead of
// triggering a browser download.
// ---------------------------------------------------------------------------

class NodePDFReport extends CreatePDFReport {
    private readonly filePath: string;

    constructor(filePath: string) {
        super();
        this.filePath = filePath;
    }

    protected _output(): void {
        const buffer = new Uint8Array(this.doc.output('arraybuffer') as ArrayBuffer);
        fs.writeFileSync(this.filePath, buffer);
        console.error(`PDF saved: ${this.filePath}`);
    }
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------

interface CliInput {
    path: string;
    label: string | null;
}

interface CliArgs {
    inputs: CliInput[];
    output: string | null;
}

function printHelp(): void {
    console.error(`Usage:
  generate-pdf.ts                                              # read single JSON from stdin
  generate-pdf.ts --input <path> [--label <name>]              # single estimate from a file
  generate-pdf.ts --input <a.json> [--label A] \\
                  --input <b.json> [--label B] ...             # multi-estimate consolidated report
  generate-pdf.ts ... --output <path.pdf>                      # custom output path
                                                                 (default: ./keyspaces-pricing-estimate.pdf)
`);
}

function parseCli(argv: string[]): CliArgs {
    const args: CliArgs = { inputs: [], output: null };
    let i = 0;
    while (i < argv.length) {
        const a = argv[i];
        switch (a) {
            case '--input': {
                const p = argv[++i];
                if (!p) throw new Error('--input requires a path argument');
                args.inputs.push({ path: p, label: null });
                break;
            }
            case '--label': {
                const l = argv[++i];
                if (!l) throw new Error('--label requires a name argument');
                if (args.inputs.length === 0) {
                    throw new Error('--label must follow a --input flag');
                }
                args.inputs[args.inputs.length - 1].label = l;
                break;
            }
            case '--output':
            case '-o': {
                const o = argv[++i];
                if (!o) throw new Error('--output requires a path argument');
                args.output = o;
                break;
            }
            case '-h':
            case '--help':
                printHelp();
                process.exit(0);
                break;
            default:
                throw new Error(`Unknown argument: ${a}`);
        }
        i++;
    }
    return args;
}

// ---------------------------------------------------------------------------
// JSON helpers
// ---------------------------------------------------------------------------

interface ReportDataShape {
    datacenters: Estimate['datacenters'];
    regions: Estimate['regions'];
    estimateResults: Estimate['estimateResults'];
    pricing: Estimate['pricing'];
}

interface RawCompatibility {
    has_issues?: boolean;
    details?: {
        schema?: {
            functions?: number;
            aggregates?: number;
            keyspaces?: Record<string, Record<string, {
                indexes: string[];
                triggers: string[];
                materializedViews: string[];
            }>>;
        } | null;
        query_patterns?: {
            lwt_in_unlogged_batch?: Array<{ prepared_id?: string; query_string: string }>;
            aggregations?: Array<{ prepared_id?: string; function?: string; query_string: string }>;
        } | null;
    };
}

function readJsonFileSync(p: string): Record<string, unknown> {
    const raw = fs.readFileSync(p, 'utf8');
    try {
        return JSON.parse(raw);
    } catch (e: unknown) {
        throw new Error('Failed to parse JSON file: invalid JSON content');
    }
}

function readJsonStdinSync(): Record<string, unknown> {
    const raw = fs.readFileSync(0, 'utf8');
    if (!raw.trim()) {
        throw new Error('No JSON received on stdin');
    }
    try {
        return JSON.parse(raw);
    } catch (e: unknown) {
        throw new Error('Failed to parse JSON from stdin: invalid JSON content');
    }
}

function extractEstimate(json: Record<string, unknown>, label: string): Estimate {
    const reportData = json.report_data as ReportDataShape | undefined;
    if (!reportData) {
        throw new Error(`Input JSON for "${label}" is missing report_data — was it produced by calculate.ts or parse-cassandra.ts?`);
    }
    const compat = json.compatibility as RawCompatibility | undefined;
    return {
        label,
        datacenters: reportData.datacenters,
        regions: reportData.regions,
        estimateResults: reportData.estimateResults,
        pricing: reportData.pricing,
        tcoData: null,
        compatibilityData: compatToReport(compat ?? null),
    };
}

function compatToReport(compat: RawCompatibility | null): CompatibilityData | null {
    if (!compat) return null;
    const schema = compat.details?.schema ?? null;
    const qp = compat.details?.query_patterns ?? null;
    if (!schema && !qp) return null;
    return {
        functions: schema?.functions ?? 0,
        aggregates: schema?.aggregates ?? 0,
        keyspaces: schema?.keyspaces ?? {},
        queryPatterns: qp ? {
            lwtInUnloggedBatch: (qp.lwt_in_unlogged_batch ?? []).map(item => ({
                prepared_id: item.prepared_id,
                query_string: item.query_string,
            })),
            aggregations: (qp.aggregations ?? []).map(item => ({
                prepared_id: item.prepared_id,
                query_string: item.query_string,
            })),
        } : undefined,
    };
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

function resolveOutputPath(output: string | null): string {
    if (!output) return path.join(process.cwd(), 'keyspaces-pricing-estimate.pdf');
    return path.isAbsolute(output) ? output : path.join(process.cwd(), output);
}

function main(): void {
    let cli: CliArgs;
    try {
        cli = parseCli(process.argv.slice(2));
    } catch (e: unknown) {
        console.error((e as Error).message);
        printHelp();
        process.exit(1);
        return;
    }

    const outputPath = resolveOutputPath(cli.output);
    const estimates: Estimate[] = [];

    if (cli.inputs.length === 0) {
        if (process.stdin.isTTY) {
            console.error('No --input flags and stdin is a TTY. Provide JSON via stdin or --input.');
            printHelp();
            process.exit(1);
        }
        try {
            const json = readJsonStdinSync();
            estimates.push(extractEstimate(json, 'Estimate'));
        } catch (e: unknown) {
            console.error((e as Error).message);
            process.exit(1);
        }
    } else {
        for (const [idx, inp] of cli.inputs.entries()) {
            try {
                const json = readJsonFileSync(inp.path);
                const label = inp.label ?? `Estimate ${idx + 1}`;
                estimates.push(extractEstimate(json, label));
            } catch (e: unknown) {
                console.error((e as Error).message);
                process.exit(1);
            }
        }
    }

    const report = new NodePDFReport(outputPath);

    if (estimates.length === 1) {
        const e = estimates[0];
        report.createReport(
            e.datacenters,
            e.regions,
            e.estimateResults,
            e.pricing,
            e.tcoData ?? null,
            e.compatibilityData ?? null,
        );
    } else {
        report.createMultiReport(estimates);
    }
}

main();
scripts/package.json
{
  "name": "amazon-keyspaces-skill-scripts",
  "version": "1.0.0",
  "description": "Bundled scripts for the Amazon Keyspaces AWS MCP skill: pricing, Cassandra diagnostic parsing, compatibility checks, SQL modeling, and PDF reporting.",
  "private": true,
  "scripts": {
    "calculate": "ts-node --project tsconfig.scripts.json calculate.ts",
    "parse-cassandra": "ts-node --project tsconfig.scripts.json parse-cassandra.ts",
    "check-compatibility": "ts-node --project tsconfig.scripts.json check-compatibility.ts",
    "generate-pdf": "ts-node --project tsconfig.scripts.json generate-pdf.ts"
  },
  "dependencies": {
    "jspdf": "4.2.1",
    "jspdf-autotable": "5.0.8",
    "ts-node": "10.9.2",
    "typescript": "5.8.3"
  },
  "devDependencies": {
    "@types/node": "20.17.0"
  }
}
scripts/parse-cassandra.ts
#!/usr/bin/env npx ts-node
/**
 * parse-cassandra.ts
 *
 * Parses Cassandra diagnostic files and outputs a Keyspaces pricing estimate
 * as JSON (same shape as calculate.js), suitable for piping to generate-pdf.js.
 *
 * Imports directly from ParsingHelpers.ts and PricingFormulas.ts — no duplication.
 *
 * Usage:
 *   npx ts-node --require tsconfig-paths/register --project tsconfig.scripts.json \
 *     scripts/parse-cassandra.ts \
 *     --region    us-east-1 \
 *     --tablestats tablestats.txt \
 *     [--status   status.txt] \
 *     [--info     node1-info.txt] \
 *     [--rowsize  rowsize.txt] \
 *     [--schema   schema.cql] \
 *     [--pitr]
 *
 * Multiple --info flags accepted (one per node).
 */

import fs from 'fs';
import path from 'path';

import {
  parseNodetoolStatus,
  parseNodetoolInfo,
  parse_nodetool_tablestats,
  parse_cassandra_schema,
  parse_cassandra_schema_compatibility,
  parse_prepared_statements,
  parseRowSizeInfo,
  scanCassandraFiles,
  type CompatibilityInfo,
  type QueryPatternsInfo,
} from './calculator/ParsingHelpers';

import {
  buildCassandraLocalSet,
  getKeyspaceCassandraAggregate,
  calculatePricingEstimate,
  aggregatePricingTotals,
  type Samples,
  type DatacenterRef,
  type EstimateResults,
  type KeyspaceAggregate,
  type PricingEstimateResult,
} from './calculator/PricingFormulas';

const regionsMap: Record<string, string> = require('../assets/data/regions.json');

// ─── CLI arg parsing ──────────────────────────────────────────────────────────

interface Args {
  region: string;
  info: string[];
  status: string | null;
  tablestats: string | null;
  rowsize: string | null;
  schema: string | null;
  prepared: string | null;
  pitr: boolean;
  dir: string | null;
}

function parseArgs(argv: string[]): Args {
  const args: Args = { region: 'us-east-1', info: [], status: null, tablestats: null, rowsize: null, schema: null, prepared: null, pitr: false, dir: null };
  let i = 0;
  while (i < argv.length) {
    switch (argv[i]) {
      case '--region':     args.region = argv[++i]; break;
      case '--info':       args.info.push(argv[++i]); break;
      case '--status':     args.status = argv[++i]; break;
      case '--tablestats': args.tablestats = argv[++i]; break;
      case '--rowsize':    args.rowsize = argv[++i]; break;
      case '--schema':     args.schema = argv[++i]; break;
      case '--prepared':   args.prepared = argv[++i]; break;
      case '--dir':        args.dir = argv[++i]; break;
      case '--pitr':       args.pitr = true; break;
    }
    i++;
  }
  return args;
}

// Read all files in a directory and classify them using ParsingHelpers detectors.
function resolveArgsFromDir(dir: string, args: Args): void {
  const entries = fs.readdirSync(dir).filter(f => fs.statSync(path.join(dir, f)).isFile());
  const fileMap: Record<string, string> = {};
  for (const entry of entries) {
    try { fileMap[path.join(dir, entry)] = fs.readFileSync(path.join(dir, entry), 'utf8'); }
    catch { /* skip unreadable */ }
  }
  const scan = scanCassandraFiles(fileMap);
  if (!args.tablestats && scan.tablestats.length > 0) args.tablestats = scan.tablestats[0];
  if (scan.tablestats.length > 1) {
    console.warn(`Warning: ${scan.tablestats.length} tablestats files found in --dir. Only the first is used. Multi-node tablestats aggregation is not currently supported.`);
  }
  if (!args.status && scan.status)   args.status  = scan.status;
  if (args.info.length === 0)        args.info.push(...scan.info);
  if (!args.rowsize && scan.rowsize) args.rowsize = scan.rowsize;
  if (!args.schema  && scan.schema)  args.schema  = scan.schema;
  if (!args.prepared && scan.prepared) args.prepared = scan.prepared;
}

// ─── Main ─────────────────────────────────────────────────────────────────────

function main() {
  const args = parseArgs(process.argv.slice(2));

  if (args.dir) resolveArgsFromDir(args.dir, args);

  if (!args.tablestats) {
    console.error('Usage: parse-cassandra.ts --tablestats <file> [--status <file>] [--info <file>] [--rowsize <file>] [--schema <file>] [--region us-east-1] [--pitr]');
    console.error('       parse-cassandra.ts --dir <directory> [--region us-east-1] [--pitr]');
    console.error('');
    console.error('Note: --dir reads all files at the specified path using the invoking user\'s permissions.');
    process.exit(1);
  }

  // ── Parse files using ParsingHelpers.ts ──────────────────────────────────

  const tablestats = parse_nodetool_tablestats(fs.readFileSync(args.tablestats, 'utf8'));
  
  const statusData = args.status
    ? parseNodetoolStatus(fs.readFileSync(args.status, 'utf8'))
    : new Map<string, number>();
  const rowSizeData = args.rowsize
    ? parseRowSizeInfo(fs.readFileSync(args.rowsize, 'utf8'))
    : {};

  // Group info files by DC
  const nodesByDc = new Map<string, Array<{ id: string; uptime_seconds: number }>>();
  for (const infoFile of args.info) {
    const { dc, id, uptime_seconds } = parseNodetoolInfo(fs.readFileSync(infoFile, 'utf8'));
    if (!nodesByDc.has(dc)) nodesByDc.set(dc, []);
    nodesByDc.get(dc)!.push({ id, uptime_seconds });
  }

  // Fall back to status DCs or placeholder if no info files
  if (nodesByDc.size === 0) {
    const dcNames = statusData.size > 0 ? [...statusData.keys()] : ['datacenter1'];
    const SECONDS_PER_MONTH = (365 / 12) * 86400;
    for (const dc of dcNames) {
      nodesByDc.set(dc, [{ id: 'node0', uptime_seconds: SECONDS_PER_MONTH }]);
    }
  }
  if (statusData.size === 0) {
    for (const [dc, nodes] of nodesByDc.entries()) statusData.set(dc, nodes.length);
  }

  const dcNames = [...nodesByDc.keys()];
  const schemaContent = args.schema ? fs.readFileSync(args.schema, 'utf8') : null;
  const schema = schemaContent ? parse_cassandra_schema(schemaContent, dcNames[0]) : null;
  const compatibility: CompatibilityInfo | null = schemaContent
    ? parse_cassandra_schema_compatibility(schemaContent)
    : null;
  const preparedContent = args.prepared ? fs.readFileSync(args.prepared, 'utf8') : null;
  const queryPatterns: QueryPatternsInfo | null = preparedContent
    ? parse_prepared_statements(preparedContent)
    : null;
  const preparedTtlTables = queryPatterns
    ? new Set(Object.keys(queryPatterns.ttl_tables))
    : undefined;

  // Convert SchemaInfo → NodePayload schema shape (drop 'class' and 'tables', keep 'datacenters')
  const nodeSchema = schema
    ? Object.fromEntries(Object.entries(schema).map(([ks, v]) => [ks, { datacenters: v.datacenters }]))
    : undefined;

  // ── Build Samples structure for PricingFormulas.ts ────────────────────────

  const samples: Samples = {};
  for (const [dc, nodes] of nodesByDc.entries()) {
    samples[dc] = {};
    for (const node of nodes) {
      samples[dc][node.id] = {
        tablestats_data: tablestats,
        schema: nodeSchema,
        info_data: { uptime_seconds: node.uptime_seconds },
        row_size_data: rowSizeData,
      };
    }
  }

  // ── Aggregate using PricingFormulas.ts ────────────────────────────────────

  const cassandraSet = buildCassandraLocalSet(samples, statusData, { preparedTtlTables });

  const estimateResults: EstimateResults = {};
  for (const dc of dcNames) {
    const aggregates = getKeyspaceCassandraAggregate(cassandraSet, dc);
    // Apply use_backup flag per keyspace
    for (const agg of Object.values(aggregates)) {
      (agg as KeyspaceAggregate & { use_backup: boolean }).use_backup = args.pitr;
    }
    estimateResults[dc] = aggregates;
  }

  // ── Price using calculatePricingEstimate from PricingFormulas.ts ──────────

  const longRegion = regionsMap[args.region] ?? args.region;
  const datacenters: DatacenterRef[] = dcNames.map(name => ({ name, nodeCount: statusData.get(name) ?? 0 }));
  const regions: Record<string, string> = Object.fromEntries(dcNames.map(dc => [dc, longRegion]));

  const pricing: PricingEstimateResult | null = calculatePricingEstimate(datacenters, regions, estimateResults);
  if (!pricing) { console.error('Failed to calculate pricing — check region and input files.'); process.exit(1); }

  // ── Build summary output (same shape as calculate.js) ────────────────────

  const {
    reads_on_demand: sumOdRead, writes_on_demand: sumOdWrite,
    ttl_deletes: sumTtl, storage: sumStorage, backup: sumBackup,
    reads_provisioned: sumProvRead, writes_provisioned: sumProvWrite,
  } = aggregatePricingTotals(pricing);

  let compatibilityReport: {
    has_issues: boolean;
    summary: {
      total_issues: number;
      schema: {
        total_issues: number;
        keyspaces_affected: number;
        tables_affected: number;
        functions: number;
        aggregates: number;
      } | null;
      query_patterns: {
        lwt_in_unlogged_batch: number;
        aggregations: number;
        ttl_tables: number;
      } | null;
    };
    details: {
      schema: CompatibilityInfo | null;
      query_patterns: QueryPatternsInfo | null;
    };
  } | null = null;
  if (compatibility || queryPatterns) {
    let schemaSummary: {
      total_issues: number;
      keyspaces_affected: number;
      tables_affected: number;
      functions: number;
      aggregates: number;
    } | null = null;
    if (compatibility) {
      let total_table_issues = 0;
      let tables_affected = 0;
      for (const tables of Object.values(compatibility.keyspaces)) {
        for (const issue of Object.values(tables)) {
          const count = issue.indexes.length + issue.triggers.length + issue.materializedViews.length;
          if (count > 0) { tables_affected++; total_table_issues += count; }
        }
      }
      schemaSummary = {
        total_issues: total_table_issues + compatibility.functions + compatibility.aggregates,
        keyspaces_affected: Object.keys(compatibility.keyspaces).length,
        tables_affected,
        functions: compatibility.functions,
        aggregates: compatibility.aggregates,
      };
    }
    const querySummary = queryPatterns
      ? {
          lwt_in_unlogged_batch: queryPatterns.lwt_in_unlogged_batch.length,
          aggregations:          queryPatterns.aggregations.length,
          ttl_tables:            Object.keys(queryPatterns.ttl_tables).length,
        }
      : null;
    const total_issues =
      (schemaSummary?.total_issues ?? 0) +
      (querySummary?.lwt_in_unlogged_batch ?? 0) +
      (querySummary?.aggregations ?? 0);
    compatibilityReport = {
      has_issues: total_issues > 0,
      summary: { total_issues, schema: schemaSummary, query_patterns: querySummary },
      details: { schema: compatibility, query_patterns: queryPatterns },
    };
  }

  const result = {
    region: { short: args.region, long: longRegion },
    source: 'cassandra-diagnostic-files',
    datacenters,
    on_demand: {
      reads_strong: sumOdRead, reads_eventual: sumOdRead / 2,
      writes: sumOdWrite, ttl_deletes: sumTtl,
      storage: sumStorage, backup: sumBackup,
      total: pricing.total_monthly_on_demand_cost,
    },
    provisioned: {
      reads_strong: sumProvRead, reads_eventual: sumProvRead / 2,
      writes: sumProvWrite, ttl_deletes: sumTtl,
      storage: sumStorage, backup: sumBackup,
      total: pricing.total_monthly_provisioned_cost,
    },
    savings_plan_available: pricing.total_monthly_provisioned_cost_savings !== pricing.total_monthly_provisioned_cost,
    on_demand_savings_plan: {
      total: pricing.total_monthly_on_demand_cost_savings,
    },
    provisioned_savings_plan: {
      total: pricing.total_monthly_provisioned_cost_savings,
    },
    per_datacenter: pricing.total_datacenter_cost,
    compatibility: compatibilityReport,
    report_data: {
      datacenters,
      regions,
      estimateResults,
      pricing,
    },
  };

  console.log(JSON.stringify(result, null, 2));
}

main();
scripts/prepared-statements-sampler.sh
#!/bin/bash
shopt -s expand_aliases
# The following script exports the cluster's prepared statements as
#   newline-delimited JSON (NDJSON) for Amazon Keyspaces compatibility
#   analysis.
#
# It reads `system.prepared_statements` (which every coordinator maintains
#   in-memory for every CQL statement clients have prepared) and emits one
#   JSON object per statement on stdout.
#
# The prepared statements are used by the Keyspaces calculator skill to
#   detect Cassandra features that are not supported by Amazon Keyspaces:
#     - Lightweight transactions inside `BEGIN UNLOGGED BATCH`
#     - Aggregations (COUNT / MIN / MAX / SUM / AVG)
#     - User-defined function calls (when a schema is also supplied)
#     - Per-table `USING TTL` usage (informational — used to set has_ttl
#       when no default TTL is declared on the table)
#
# The script takes the same parameters as cqlsh to connect to cassandra.
#
# For Amazon Keyspaces, prefer SigV4 authentication (no password needed):
#   ./scripts/prepared-statements-sampler.sh cassandra.us-east-1.amazonaws.com 9142 --ssl
#   (requires the SigV4 auth plugin configured in cqlshrc or the cqlsh-expansion tool)
#
# For source Cassandra clusters with username/password auth, use a cqlshrc credentials file:
#   1. Create ~/.cassandra/cqlshrc with [authentication] section (username + password)
#   2. chmod 600 ~/.cassandra/cqlshrc
#   3. ./scripts/prepared-statements-sampler.sh <host> <port> --ssl > prepared_statements.ndjson
#
# Alternatively, set CQLSH_PASSWORD environment variable (less secure than cqlshrc but avoids CLI args):
#   export CQLSH_PASSWORD="$PASSWORD"
#   ./scripts/prepared-statements-sampler.sh <host> <port> -u "serviceuser" --ssl > prepared_statements.ndjson
#
# NEVER pass passwords via -p on the command line — they are visible in process listings.

# check if the cqlsh-expansion is installed, then if cqlsh installed, then check local file
if [ -x "$(command -v cqlsh-expansion)" ]; then
  echo 'using installed cqlsh-expansion' 1>&2
  alias kqlsh='cqlsh-expansion'
elif [ -x "$(command -v cqlsh)" ]; then
  echo 'using installed cqlsh' 1>&2
  alias kqlsh='cqlsh'
elif [ -e cqlsh ]; then
  echo 'using local cqlsh' 1>&2
  alias kqlsh='./cqlsh'
else
  echo 'cqlsh not found' 1>&2
  exit 1
fi

echo 'starting...' 1>&2

# Filter statements that reference only system keyspaces (driver/cqlsh chatter).
SYSTEMKEYSPACEFILTER='system\.\|system_schema\.\|system_traces\.\|system_auth\.\|system_distributed\.\|dse_\|OpsCenter\.'

# cqlsh prints a header line, a divider of dashes, the rows, a blank line, and
# "(N rows)". We keep only lines that look like a JSON object.
kqlsh "$@" -e "CONSISTENCY LOCAL_ONE; PAGING OFF; SELECT JSON * FROM system.prepared_statements;" \
  | awk '/^[[:space:]]*\{.*\}[[:space:]]*$/' \
  | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
  | grep -v "$SYSTEMKEYSPACEFILTER"

echo 'fin!' 1>&2
scripts/tsconfig.scripts.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true
  },
  "include": [
    "calculator/**/*",
    "*.ts",
    "../assets/data/*.json"
  ]
}