From cf20d70300a1a5663de1809c909aeda6bea7ef3d Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Mon, 16 Mar 2026 19:01:55 -0700 Subject: [PATCH 1/8] Add batch DELETE/UPDATE samples for datasets exceeding 3k row limit Demonstrates sequential and parallel batch processing patterns for Aurora DSQL with OCC retry logic and hashtext() partitioning. Includes Python (psycopg2), Java (pgJDBC), and Node.js (node-postgres) implementations. --- .gitignore | 2 + batch-operations/GITHUB_ISSUE.md | 69 ++++++++ batch-operations/README.md | 52 ++++++ batch-operations/java/pgjdbc/README.md | 90 ++++++++++ batch-operations/java/pgjdbc/build.gradle | 27 +++ .../java/com/example/dsql/BatchDelete.java | 115 +++++++++++++ .../java/com/example/dsql/BatchUpdate.java | 117 +++++++++++++ .../src/main/java/com/example/dsql/Main.java | 123 ++++++++++++++ .../main/java/com/example/dsql/OccRetry.java | 83 ++++++++++ .../java/com/example/dsql/Repopulate.java | 59 +++++++ .../javascript/node-postgres/README.md | 97 +++++++++++ .../javascript/node-postgres/package.json | 14 ++ .../node-postgres/src/batchDelete.js | 121 ++++++++++++++ .../node-postgres/src/batchUpdate.js | 133 +++++++++++++++ .../javascript/node-postgres/src/main.js | 107 ++++++++++++ .../javascript/node-postgres/src/occRetry.js | 56 +++++++ .../node-postgres/src/repopulate.js | 64 ++++++++ batch-operations/python/psycopg2/README.md | 119 ++++++++++++++ .../python/psycopg2/src/batch_delete.py | 67 ++++++++ .../python/psycopg2/src/batch_update.py | 68 ++++++++ batch-operations/python/psycopg2/src/main.py | 155 ++++++++++++++++++ .../python/psycopg2/src/occ_retry.py | 60 +++++++ .../psycopg2/src/parallel_batch_delete.py | 94 +++++++++++ .../psycopg2/src/parallel_batch_update.py | 95 +++++++++++ .../python/psycopg2/src/repopulate.py | 66 ++++++++ batch-operations/sql/batch_test_setup.sql | 73 +++++++++ 26 files changed, 2126 insertions(+) create mode 100644 batch-operations/GITHUB_ISSUE.md create mode 100644 batch-operations/README.md create mode 100644 batch-operations/java/pgjdbc/README.md create mode 100644 batch-operations/java/pgjdbc/build.gradle create mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java create mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java create mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java create mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java create mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java create mode 100644 batch-operations/javascript/node-postgres/README.md create mode 100644 batch-operations/javascript/node-postgres/package.json create mode 100644 batch-operations/javascript/node-postgres/src/batchDelete.js create mode 100644 batch-operations/javascript/node-postgres/src/batchUpdate.js create mode 100644 batch-operations/javascript/node-postgres/src/main.js create mode 100644 batch-operations/javascript/node-postgres/src/occRetry.js create mode 100644 batch-operations/javascript/node-postgres/src/repopulate.js create mode 100644 batch-operations/python/psycopg2/README.md create mode 100644 batch-operations/python/psycopg2/src/batch_delete.py create mode 100644 batch-operations/python/psycopg2/src/batch_update.py create mode 100644 batch-operations/python/psycopg2/src/main.py create mode 100644 batch-operations/python/psycopg2/src/occ_retry.py create mode 100644 batch-operations/python/psycopg2/src/parallel_batch_delete.py create mode 100644 batch-operations/python/psycopg2/src/parallel_batch_update.py create mode 100644 batch-operations/python/psycopg2/src/repopulate.py create mode 100644 batch-operations/sql/batch_test_setup.sql diff --git a/.gitignore b/.gitignore index a9c4a2b9..ddd8ac8e 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ Icon? # TypeScript build outputs dist/ +.venv/ +batch-operations/python/psycopg2/.venv diff --git a/batch-operations/GITHUB_ISSUE.md b/batch-operations/GITHUB_ISSUE.md new file mode 100644 index 00000000..741e3696 --- /dev/null +++ b/batch-operations/GITHUB_ISSUE.md @@ -0,0 +1,69 @@ +**Title:** Add batch DELETE/UPDATE code samples for datasets exceeding 3,000-row transaction limit + +**Description:** + +Aurora DSQL limits each transaction to 3,000 row mutations. This is a common stumbling block for users who need to DELETE or UPDATE large datasets. I'd like to contribute code samples that demonstrate how to handle this across Python, Java, and JavaScript. + +**What the samples cover:** + +Two patterns, each implemented in all three languages: + +1. **Sequential batch processing** — single-threaded loop that processes rows in batches of N (default 1,000), committing each batch as a separate transaction +2. **Parallel batch processing** — multiple worker threads partition rows using `abs(hashtext(id::text)) % num_workers` and process their partitions concurrently, each running its own batch loop + +Both patterns include: +- OCC retry logic with exponential backoff (SQLSTATE 40001 handling) +- Connection pooling using the DSQL connectors (auto IAM token refresh) +- Subquery-based DELETE (`DELETE WHERE id IN (SELECT id ... LIMIT N)`) since PostgreSQL doesn't support `DELETE ... LIMIT` +- Subquery-based UPDATE with `updated_at` tracking to prevent reprocessing +- Table repopulation between DELETE and UPDATE demos +- CLI entry point with configurable batch size and worker count + +**Languages and drivers:** + +| Language | Driver | Connector | +|----------|--------|-----------| +| Python | psycopg2 | `aurora-dsql-python-connector` (`AuroraDSQLThreadedConnectionPool`) | +| Java | pgJDBC | AWS JDBC Wrapper (`AwsWrapperDataSource`) | +| JavaScript | node-postgres | `@aws/aurora-dsql-node-postgres-connector` | + +**Files per language:** +- OCC retry module (reusable) +- Sequential batch DELETE +- Sequential batch UPDATE +- Parallel batch DELETE (hashtext partitioning) +- Parallel batch UPDATE (hashtext partitioning) +- Repopulate helper (restores test data between demos) +- Main entry point with CLI args +- README with prerequisites, setup, and usage instructions + +**Shared:** +- SQL setup script (creates `batch_test` table, inserts 5,000 rows in batches of 1,000) + +**Proposed structure:** + +I have this organized as a `batch-operations/` top-level directory following the existing repo conventions (`language/driver/src/`). Happy to restructure if you'd prefer these integrated differently — for example, as additional files within the existing `python/psycopg2/`, `java/pgjdbc/`, and `javascript/node-postgres/` directories. + +``` +batch-operations/ +├── README.md +├── sql/batch_test_setup.sql +├── python/psycopg2/ +│ ├── README.md +│ ├── requirements.txt +│ └── src/ +├── javascript/node-postgres/ +│ ├── README.md +│ ├── package.json +│ └── src/ +└── java/pgjdbc/ + ├── README.md + ├── build.gradle + └── src/main/java/... +``` + +**Tested against:** Aurora DSQL cluster in us-east-1 with the Python implementation. All seven operations (sequential delete, repopulate, sequential update, repopulate, parallel delete, repopulate, parallel update) complete successfully. + +The parallel pattern is inspired by the [re:Invent DAT401 parallel worker approach](https://github.com/marcbowes/riv25-codetalk/). + +Happy to adjust structure, naming, or scope based on your feedback. diff --git a/batch-operations/README.md b/batch-operations/README.md new file mode 100644 index 00000000..0e06fe3a --- /dev/null +++ b/batch-operations/README.md @@ -0,0 +1,52 @@ +# Aurora DSQL Batch Operations + +Code examples demonstrating how to perform batch DELETE and UPDATE operations in +[Amazon Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) when working with +datasets exceeding the 3,000-row transaction mutation limit. + +## How this section is organized + +| Language | Client | Directory | +|----------|--------|-----------| +| Java | pgJDBC + AWS JDBC Wrapper | [java/pgjdbc](java/pgjdbc) | +| JavaScript | node-postgres | [javascript/node-postgres](javascript/node-postgres) | +| Python | psycopg2 | [python/psycopg2](python/psycopg2) | + +Shared resources: + +| Resource | Path | Description | +|----------|------|-------------| +| SQL setup | [sql/batch_test_setup.sql](sql/batch_test_setup.sql) | Creates and populates the test table | + +## Patterns + +Each language example includes two patterns: + +1. **Sequential batch processing** — A single-threaded loop that processes rows in batches of 1,000 + (configurable), committing each batch as a separate transaction. + +2. **Parallel batch processing** — Multiple worker threads partition rows using + `abs(hashtext(id::text)) % num_workers` and process their partitions concurrently. Each worker + runs its own sequential batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff +for handling serialization conflicts (SQLSTATE 40001). + +## Key concepts + +- **3,000-row transaction limit**: Aurora DSQL limits each transaction to 3,000 row mutations, + regardless of how many indexes are defined on the table. +- **Batch size**: Default 1,000 rows per transaction, providing a safe margin below the limit. +- **hashtext() partitioning**: Parallel workers use `abs(hashtext(id::text)) % num_workers` to + ensure each worker operates on a disjoint set of rows, avoiding OCC conflicts between workers. +- **Connection pooling**: All examples use DSQL connectors that automatically refresh IAM auth + tokens, important for long-running batch jobs. + +## Security + +See [CONTRIBUTING](https://github.com/aws-samples/aurora-dsql-samples/blob/main/CONTRIBUTING.md) +for more information. + +## License + +This project is licensed under the MIT-0 License. diff --git a/batch-operations/java/pgjdbc/README.md b/batch-operations/java/pgjdbc/README.md new file mode 100644 index 00000000..268ca336 --- /dev/null +++ b/batch-operations/java/pgjdbc/README.md @@ -0,0 +1,90 @@ +# Batch Operations with pgJDBC + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[pgJDBC](https://jdbc.postgresql.org/) with the +[Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) for automatic +IAM authentication via the AWS JDBC Wrapper. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across worker threads using +`abs(hashtext(CAST(id AS text))) % num_workers = worker_id`, ensuring workers operate on disjoint sets +of rows and avoid OCC conflicts with each other. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Java Development Kit (JDK) 17 or later. +- Gradle (the wrapper is included in this project). +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up the test table + +Before running the examples, create and populate the test table: + +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Build and run: + +```bash +./gradlew run --args="--endpoint $CLUSTER_ENDPOINT --user $CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) +- [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/java/pgjdbc/build.gradle b/batch-operations/java/pgjdbc/build.gradle new file mode 100644 index 00000000..6b4bf248 --- /dev/null +++ b/batch-operations/java/pgjdbc/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'application' +} + +group = 'com.example.dsql' +version = '1.0.0' + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.postgresql:postgresql:42.7.4' + implementation 'software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.5.4' + implementation 'software.amazon.awssdk:dsql:2.31.9' + implementation 'software.amazon.awssdk:auth:2.31.9' +} + +application { + mainClass = 'com.example.dsql.Main' +} diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java new file mode 100644 index 00000000..4d41b72b --- /dev/null +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.sql.DataSource; + +/** + * Sequential and parallel batch DELETE for Aurora DSQL. + */ +public class BatchDelete { + + public static int batchDelete( + DataSource pool, String table, String condition, + int batchSize, int maxRetries, double baseDelay) + throws SQLException, OccRetry.MaxRetriesExceededException { + + int totalDeleted = 0; + while (true) { + try (Connection conn = pool.getConnection()) { + conn.setAutoCommit(false); + String sql = "DELETE FROM " + table + " WHERE id IN (SELECT id FROM " + table + + " WHERE " + condition + " LIMIT " + batchSize + ")"; + + int deleted = OccRetry.executeWithRetry(conn, (c) -> { + try (Statement stmt = c.createStatement()) { + return stmt.executeUpdate(sql); + } + }, maxRetries, baseDelay); + + conn.commit(); + totalDeleted += deleted; + System.out.println("Deleted " + deleted + " rows (total: " + totalDeleted + ")"); + if (deleted == 0) break; + } + } + return totalDeleted; + } + + public static int batchDelete(DataSource pool, String table, String condition) + throws SQLException, OccRetry.MaxRetriesExceededException { + return batchDelete(pool, table, condition, 1000, 3, 0.1); + } + + public static int parallelBatchDelete( + DataSource pool, String table, String condition, + int numWorkers, int batchSize, int maxRetries, double baseDelay) + throws SQLException, OccRetry.MaxRetriesExceededException { + + ExecutorService executor = Executors.newFixedThreadPool(numWorkers); + List> futures = new ArrayList<>(); + + for (int i = 0; i < numWorkers; i++) { + final int workerId = i; + futures.add(executor.submit(() -> { + int totalDeleted = 0; + String partitionCondition = condition + + " AND abs(hashtext(CAST(id AS text))) % " + numWorkers + " = " + workerId; + + while (true) { + try (Connection conn = pool.getConnection()) { + conn.setAutoCommit(false); + String sql = "DELETE FROM " + table + " WHERE id IN (SELECT id FROM " + table + + " WHERE " + partitionCondition + " LIMIT " + batchSize + ")"; + + int deleted = OccRetry.executeWithRetry(conn, (c) -> { + try (Statement stmt = c.createStatement()) { + return stmt.executeUpdate(sql); + } + }, maxRetries, baseDelay); + + conn.commit(); + totalDeleted += deleted; + System.out.println("Worker " + workerId + ": Deleted " + deleted + " rows (total: " + totalDeleted + ")"); + if (deleted == 0) break; + } + } + return totalDeleted; + })); + } + + executor.shutdown(); + int total = 0; + for (Future future : futures) { + try { + total += future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof SQLException) throw (SQLException) cause; + if (cause instanceof OccRetry.MaxRetriesExceededException) throw (OccRetry.MaxRetriesExceededException) cause; + throw new SQLException("Worker thread failed", cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SQLException("Parallel delete interrupted", e); + } + } + + System.out.println("Parallel delete complete: " + total + " rows deleted by " + numWorkers + " workers"); + return total; + } + + public static int parallelBatchDelete(DataSource pool, String table, String condition) + throws SQLException, OccRetry.MaxRetriesExceededException { + return parallelBatchDelete(pool, table, condition, 4, 1000, 3, 0.1); + } +} diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java new file mode 100644 index 00000000..c1d44ea5 --- /dev/null +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java @@ -0,0 +1,117 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.sql.DataSource; + +/** + * Sequential and parallel batch UPDATE for Aurora DSQL. + */ +public class BatchUpdate { + + public static int batchUpdate( + DataSource pool, String table, String setClause, String condition, + int batchSize, int maxRetries, double baseDelay) + throws SQLException, OccRetry.MaxRetriesExceededException { + + int totalUpdated = 0; + while (true) { + try (Connection conn = pool.getConnection()) { + conn.setAutoCommit(false); + String sql = "UPDATE " + table + " SET " + setClause + ", updated_at = NOW()" + + " WHERE id IN (SELECT id FROM " + table + + " WHERE " + condition + " LIMIT " + batchSize + ")"; + + int updated = OccRetry.executeWithRetry(conn, (c) -> { + try (Statement stmt = c.createStatement()) { + return stmt.executeUpdate(sql); + } + }, maxRetries, baseDelay); + + conn.commit(); + totalUpdated += updated; + System.out.println("Updated " + updated + " rows (total: " + totalUpdated + ")"); + if (updated == 0) break; + } + } + return totalUpdated; + } + + public static int batchUpdate(DataSource pool, String table, String setClause, String condition) + throws SQLException, OccRetry.MaxRetriesExceededException { + return batchUpdate(pool, table, setClause, condition, 1000, 3, 0.1); + } + + public static int parallelBatchUpdate( + DataSource pool, String table, String setClause, String condition, + int numWorkers, int batchSize, int maxRetries, double baseDelay) + throws SQLException, OccRetry.MaxRetriesExceededException { + + ExecutorService executor = Executors.newFixedThreadPool(numWorkers); + List> futures = new ArrayList<>(); + + for (int i = 0; i < numWorkers; i++) { + final int workerId = i; + futures.add(executor.submit(() -> { + int totalUpdated = 0; + String partitionCondition = condition + + " AND abs(hashtext(CAST(id AS text))) % " + numWorkers + " = " + workerId; + + while (true) { + try (Connection conn = pool.getConnection()) { + conn.setAutoCommit(false); + String sql = "UPDATE " + table + " SET " + setClause + ", updated_at = NOW()" + + " WHERE id IN (SELECT id FROM " + table + + " WHERE " + partitionCondition + " LIMIT " + batchSize + ")"; + + int updated = OccRetry.executeWithRetry(conn, (c) -> { + try (Statement stmt = c.createStatement()) { + return stmt.executeUpdate(sql); + } + }, maxRetries, baseDelay); + + conn.commit(); + totalUpdated += updated; + System.out.println("Worker " + workerId + ": Updated " + updated + " rows (total: " + totalUpdated + ")"); + if (updated == 0) break; + } + } + return totalUpdated; + })); + } + + executor.shutdown(); + int total = 0; + for (Future future : futures) { + try { + total += future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof SQLException) throw (SQLException) cause; + if (cause instanceof OccRetry.MaxRetriesExceededException) throw (OccRetry.MaxRetriesExceededException) cause; + throw new SQLException("Worker thread failed", cause); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SQLException("Parallel update interrupted", e); + } + } + + System.out.println("Parallel update complete: " + total + " rows updated by " + numWorkers + " workers"); + return total; + } + + public static int parallelBatchUpdate(DataSource pool, String table, String setClause, String condition) + throws SQLException, OccRetry.MaxRetriesExceededException { + return parallelBatchUpdate(pool, table, setClause, condition, 4, 1000, 3, 0.1); + } +} diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java new file mode 100644 index 00000000..6f780cd9 --- /dev/null +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java @@ -0,0 +1,123 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import software.amazon.jdbc.ds.AwsWrapperDataSource; + +/** + * Main entry point for Aurora DSQL batch operations demo (Java). + * + * Usage: + * java Main --endpoint [--user admin] + * [--batch-size 1000] [--num-workers 4] + */ +public class Main { + + @FunctionalInterface + interface BatchOperation { + int run() throws SQLException, OccRetry.MaxRetriesExceededException; + } + + private static int runOperation(String label, BatchOperation op) + throws SQLException, OccRetry.MaxRetriesExceededException { + System.out.println(); + System.out.println("=".repeat(60)); + System.out.println(" " + label); + System.out.println("=".repeat(60)); + long start = System.currentTimeMillis(); + int total = op.run(); + double elapsed = (System.currentTimeMillis() - start) / 1000.0; + System.out.printf("%n Summary: %d rows affected in %.2fs%n", total, elapsed); + System.out.println("=".repeat(60)); + return total; + } + + private static DataSource createDataSource(String endpoint, String user) { + AwsWrapperDataSource ds = new AwsWrapperDataSource(); + ds.setJdbcProtocol("jdbc:postgresql:"); + ds.setServerName(endpoint); + ds.setServerPort("5432"); + ds.setDatabase("postgres"); + ds.setUser(user); + ds.setSslMode("verify-full"); + ds.setTargetDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); + Map props = new HashMap<>(); + props.put("sslmode", "verify-full"); + ds.setTargetDataSourceProperties(props); + return ds; + } + + private static Map parseArgs(String[] args) { + Map config = new HashMap<>(); + config.put("user", "admin"); + config.put("batch-size", "1000"); + config.put("num-workers", "4"); + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--endpoint": config.put("endpoint", args[++i]); break; + case "--user": config.put("user", args[++i]); break; + case "--batch-size": config.put("batch-size", args[++i]); break; + case "--num-workers": config.put("num-workers", args[++i]); break; + default: + System.err.println("Unknown argument: " + args[i]); + System.exit(1); + } + } + if (!config.containsKey("endpoint")) { + System.err.println("Usage: java Main --endpoint " + + "[--user admin] [--batch-size 1000] [--num-workers 4]"); + System.exit(1); + } + return config; + } + + public static void main(String[] args) { + Map config = parseArgs(args); + String endpoint = config.get("endpoint"); + String user = config.get("user"); + int batchSize = Integer.parseInt(config.get("batch-size")); + int numWorkers = Integer.parseInt(config.get("num-workers")); + DataSource pool = createDataSource(endpoint, user); + String table = "batch_test"; + + try { + runOperation("Sequential Batch DELETE (category = 'electronics')", + () -> BatchDelete.batchDelete(pool, table, "category = 'electronics'", batchSize, 3, 0.1)); + + runOperation("Repopulate test data", + () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + + runOperation("Sequential Batch UPDATE (clothing -> processed)", + () -> BatchUpdate.batchUpdate(pool, table, "status = 'processed'", + "category = 'clothing' AND status != 'processed'", batchSize, 3, 0.1)); + + runOperation("Repopulate test data", + () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + + runOperation("Parallel Batch DELETE (category = 'food') [" + numWorkers + " workers]", + () -> BatchDelete.parallelBatchDelete(pool, table, "category = 'food'", numWorkers, batchSize, 3, 0.1)); + + runOperation("Repopulate test data", + () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + + runOperation("Parallel Batch UPDATE (books -> archived) [" + numWorkers + " workers]", + () -> BatchUpdate.parallelBatchUpdate(pool, table, "status = 'archived'", + "category = 'books' AND status != 'archived'", numWorkers, batchSize, 3, 0.1)); + + } catch (SQLException e) { + System.err.println("Database error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } catch (OccRetry.MaxRetriesExceededException e) { + System.err.println("Max retries exceeded: " + e.getMessage()); + System.exit(1); + } + System.out.println("\nDemo complete."); + } +} diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java new file mode 100644 index 00000000..503eb02f --- /dev/null +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; + +/** + * OCC retry logic with exponential backoff for Aurora DSQL batch operations. + */ +public class OccRetry { + + private static final Logger logger = Logger.getLogger(OccRetry.class.getName()); + private static final String SERIALIZATION_FAILURE = "40001"; + + @FunctionalInterface + public interface DatabaseOperation { + T execute(Connection connection) throws SQLException; + } + + public static class MaxRetriesExceededException extends Exception { + private final int maxRetries; + + public MaxRetriesExceededException(int maxRetries) { + super("Max retries exceeded: failed after " + maxRetries + " retries"); + this.maxRetries = maxRetries; + } + + public int getMaxRetries() { + return maxRetries; + } + } + + /** + * Execute a database operation with OCC conflict retry and exponential backoff. + * + * @param connection a JDBC connection (autoCommit should be false) + * @param operation the database operation to execute + * @param maxRetries maximum retry attempts (default 3) + * @param baseDelay base delay in seconds for backoff (default 0.1) + * @return the return value of the operation + */ + public static T executeWithRetry( + Connection connection, + DatabaseOperation operation, + int maxRetries, + double baseDelay) throws MaxRetriesExceededException, SQLException { + + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + return operation.execute(connection); + } catch (SQLException e) { + if (SERIALIZATION_FAILURE.equals(e.getSQLState())) { + connection.rollback(); + if (attempt >= maxRetries) { + throw new MaxRetriesExceededException(maxRetries); + } + double delay = baseDelay * Math.pow(2, attempt); + logger.warning(String.format( + "OCC conflict, retry %d/%d after %.1fs", + attempt + 1, maxRetries, delay)); + try { + Thread.sleep((long) (delay * 1000)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new SQLException("Retry interrupted", ie); + } + } else { + throw e; + } + } + } + throw new MaxRetriesExceededException(maxRetries); + } + + public static T executeWithRetry( + Connection connection, + DatabaseOperation operation) throws MaxRetriesExceededException, SQLException { + return executeWithRetry(connection, operation, 3, 0.1); + } +} diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java new file mode 100644 index 00000000..0155403f --- /dev/null +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import javax.sql.DataSource; + +/** + * Repopulate test data for Aurora DSQL batch operation samples. + */ +public class Repopulate { + + private static final String INSERT_SQL = + "INSERT INTO batch_test (category, status, value) " + + "SELECT " + + " (ARRAY['electronics','clothing','food','books','toys'])" + + "[floor(random() * 5 + 1)], " + + " 'active', " + + " round((random() * 1000)::numeric, 2) " + + "FROM generate_series(1, ?)"; + + public static int repopulateTestData( + DataSource pool, int rowCount, int batchSize, + int maxRetries, double baseDelay) + throws SQLException, OccRetry.MaxRetriesExceededException { + + int totalInserted = 0; + int remaining = rowCount; + + while (remaining > 0) { + int currentBatch = Math.min(batchSize, remaining); + try (Connection conn = pool.getConnection()) { + conn.setAutoCommit(false); + final int size = currentBatch; + int inserted = OccRetry.executeWithRetry(conn, (c) -> { + try (PreparedStatement pstmt = c.prepareStatement(INSERT_SQL)) { + pstmt.setInt(1, size); + return pstmt.executeUpdate(); + } + }, maxRetries, baseDelay); + + conn.commit(); + totalInserted += inserted; + remaining -= inserted; + System.out.println("Inserted " + inserted + " rows (total: " + totalInserted + ")"); + } + } + System.out.println("Repopulation complete: " + totalInserted + " rows inserted"); + return totalInserted; + } + + public static int repopulateTestData(DataSource pool) + throws SQLException, OccRetry.MaxRetriesExceededException { + return repopulateTestData(pool, 5000, 1000, 3, 0.1); + } +} diff --git a/batch-operations/javascript/node-postgres/README.md b/batch-operations/javascript/node-postgres/README.md new file mode 100644 index 00000000..ef7b6295 --- /dev/null +++ b/batch-operations/javascript/node-postgres/README.md @@ -0,0 +1,97 @@ +# Batch Operations with node-postgres + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[node-postgres](https://node-postgres.com/) with the +[Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) for automatic +IAM authentication. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple concurrent async workers each process a disjoint partition of the dataset using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across workers using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Node.js 18 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up + +Install the required packages: + +```bash +npm install +``` + +## Set up the test table + +Before running the examples, create and populate the test table: + +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Run the demo: + +```bash +node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel async workers | + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) +- [node-postgres Documentation](https://node-postgres.com/) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/javascript/node-postgres/package.json b/batch-operations/javascript/node-postgres/package.json new file mode 100644 index 00000000..20ebf375 --- /dev/null +++ b/batch-operations/javascript/node-postgres/package.json @@ -0,0 +1,14 @@ +{ + "name": "dsql-batch-operations", + "version": "1.0.0", + "description": "Batch DELETE and UPDATE operations for Aurora DSQL", + "main": "src/main.js", + "scripts": { + "start": "node src/main.js" + }, + "dependencies": { + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "pg": "^8.13.0" + }, + "license": "MIT-0" +} diff --git a/batch-operations/javascript/node-postgres/src/batchDelete.js b/batch-operations/javascript/node-postgres/src/batchDelete.js new file mode 100644 index 00000000..98396ff0 --- /dev/null +++ b/batch-operations/javascript/node-postgres/src/batchDelete.js @@ -0,0 +1,121 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Sequential and parallel batch DELETE for Aurora DSQL. + * + * Deletes rows matching a condition in configurable-size batches, committing + * each batch as a separate transaction to stay within the 3,000-row mutation + * limit. + */ + +const { executeWithRetry } = require("./occRetry"); + +/** + * Delete rows in batches, committing each batch as a separate transaction. + * + * @param {import('pg').Pool} pool + * @param {string} table + * @param {string} condition - SQL WHERE clause (without `WHERE`). + * @param {number} [batchSize=1000] + * @param {number} [maxRetries=3] + * @param {number} [baseDelay=0.1] + * @returns {Promise} Total rows deleted. + */ +async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { + let totalDeleted = 0; + + while (true) { + const client = await pool.connect(); + try { + const sql = `DELETE FROM ${table} WHERE id IN (SELECT id FROM ${table} WHERE ${condition} LIMIT ${batchSize})`; + + const deleted = await executeWithRetry( + client, + async (c) => { + await c.query("BEGIN"); + const result = await c.query(sql); + return result.rowCount; + }, + maxRetries, + baseDelay + ); + + await client.query("COMMIT"); + totalDeleted += deleted; + console.log(`Deleted ${deleted} rows (total: ${totalDeleted})`); + + if (deleted === 0) break; + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + } + + return totalDeleted; +} + +/** + * Delete rows in parallel using multiple concurrent async workers. + * + * Partitions rows using `abs(hashtext(id::text)) % num_workers = worker_id`. + * + * @param {import('pg').Pool} pool + * @param {string} table + * @param {string} condition + * @param {number} [numWorkers=4] + * @param {number} [batchSize=1000] + * @param {number} [maxRetries=3] + * @param {number} [baseDelay=0.1] + * @returns {Promise} Total rows deleted. + */ +async function parallelBatchDelete( + pool, table, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelay = 0.1 +) { + async function worker(workerId) { + let totalDeleted = 0; + const partitionCondition = + `${condition} AND abs(hashtext(id::text)) % ${numWorkers} = ${workerId}`; + + while (true) { + const client = await pool.connect(); + try { + const sql = `DELETE FROM ${table} WHERE id IN (SELECT id FROM ${table} WHERE ${partitionCondition} LIMIT ${batchSize})`; + + const deleted = await executeWithRetry( + client, + async (c) => { + await c.query("BEGIN"); + const result = await c.query(sql); + return result.rowCount; + }, + maxRetries, + baseDelay + ); + + await client.query("COMMIT"); + totalDeleted += deleted; + console.log(`Worker ${workerId}: Deleted ${deleted} rows (total: ${totalDeleted})`); + + if (deleted === 0) break; + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + } + + return totalDeleted; + } + + const workers = Array.from({ length: numWorkers }, (_, i) => worker(i)); + const results = await Promise.all(workers); + const total = results.reduce((sum, n) => sum + n, 0); + console.log(`Parallel delete complete: ${total} rows deleted by ${numWorkers} workers`); + return total; +} + +module.exports = { batchDelete, parallelBatchDelete }; diff --git a/batch-operations/javascript/node-postgres/src/batchUpdate.js b/batch-operations/javascript/node-postgres/src/batchUpdate.js new file mode 100644 index 00000000..e7e53c3b --- /dev/null +++ b/batch-operations/javascript/node-postgres/src/batchUpdate.js @@ -0,0 +1,133 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Sequential and parallel batch UPDATE for Aurora DSQL. + * + * Updates rows matching a condition in configurable-size batches, committing + * each batch as a separate transaction to stay within the 3,000-row mutation + * limit. Uses a subquery to avoid reprocessing rows. + */ + +const { executeWithRetry } = require("./occRetry"); + +/** + * Update rows in batches, committing each batch as a separate transaction. + * + * @param {import('pg').Pool} pool + * @param {string} table + * @param {string} setClause - SQL SET expressions (without `SET`). + * @param {string} condition - SQL WHERE clause (without `WHERE`). + * @param {number} [batchSize=1000] + * @param {number} [maxRetries=3] + * @param {number} [baseDelay=0.1] + * @returns {Promise} Total rows updated. + */ +async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { + let totalUpdated = 0; + + while (true) { + const client = await pool.connect(); + try { + const sql = `UPDATE ${table} SET ${setClause}, updated_at = NOW() + WHERE id IN ( + SELECT id FROM ${table} + WHERE ${condition} + LIMIT ${batchSize} + )`; + + const updated = await executeWithRetry( + client, + async (c) => { + await c.query("BEGIN"); + const result = await c.query(sql); + return result.rowCount; + }, + maxRetries, + baseDelay + ); + + await client.query("COMMIT"); + totalUpdated += updated; + console.log(`Updated ${updated} rows (total: ${totalUpdated})`); + + if (updated === 0) break; + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + } + + return totalUpdated; +} + +/** + * Update rows in parallel using multiple concurrent async workers. + * + * Partitions rows using `abs(hashtext(id::text)) % num_workers = worker_id`. + * + * @param {import('pg').Pool} pool + * @param {string} table + * @param {string} setClause + * @param {string} condition + * @param {number} [numWorkers=4] + * @param {number} [batchSize=1000] + * @param {number} [maxRetries=3] + * @param {number} [baseDelay=0.1] + * @returns {Promise} Total rows updated. + */ +async function parallelBatchUpdate( + pool, table, setClause, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelay = 0.1 +) { + async function worker(workerId) { + let totalUpdated = 0; + const partitionCondition = + `${condition} AND abs(hashtext(id::text)) % ${numWorkers} = ${workerId}`; + + while (true) { + const client = await pool.connect(); + try { + const sql = `UPDATE ${table} SET ${setClause}, updated_at = NOW() + WHERE id IN ( + SELECT id FROM ${table} + WHERE ${partitionCondition} + LIMIT ${batchSize} + )`; + + const updated = await executeWithRetry( + client, + async (c) => { + await c.query("BEGIN"); + const result = await c.query(sql); + return result.rowCount; + }, + maxRetries, + baseDelay + ); + + await client.query("COMMIT"); + totalUpdated += updated; + console.log(`Worker ${workerId}: Updated ${updated} rows (total: ${totalUpdated})`); + + if (updated === 0) break; + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + } + + return totalUpdated; + } + + const workers = Array.from({ length: numWorkers }, (_, i) => worker(i)); + const results = await Promise.all(workers); + const total = results.reduce((sum, n) => sum + n, 0); + console.log(`Parallel update complete: ${total} rows updated by ${numWorkers} workers`); + return total; +} + +module.exports = { batchUpdate, parallelBatchUpdate }; diff --git a/batch-operations/javascript/node-postgres/src/main.js b/batch-operations/javascript/node-postgres/src/main.js new file mode 100644 index 00000000..f21bdf9f --- /dev/null +++ b/batch-operations/javascript/node-postgres/src/main.js @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Main entry point for Aurora DSQL batch operations demo (Node.js). + * + * Usage: + * node main.js --endpoint [--user admin] \ + * [--batch-size 1000] [--num-workers 4] + */ + +const { Pool } = require("pg"); +const { getNodePostgresConnectionConfig } = require( + "@aws/aurora-dsql-node-postgres-connector" +); +const { batchDelete, parallelBatchDelete } = require("./batchDelete"); +const { batchUpdate, parallelBatchUpdate } = require("./batchUpdate"); +const { repopulateTestData } = require("./repopulate"); + +function parseArgs() { + const args = process.argv.slice(2); + const config = { endpoint: null, user: "admin", batchSize: 1000, numWorkers: 4 }; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case "--endpoint": config.endpoint = args[++i]; break; + case "--user": config.user = args[++i]; break; + case "--batch-size": config.batchSize = parseInt(args[++i], 10); break; + case "--num-workers": config.numWorkers = parseInt(args[++i], 10); break; + default: + console.error(`Unknown argument: ${args[i]}`); + process.exit(1); + } + } + + if (!config.endpoint) { + console.error( + "Usage: node main.js --endpoint " + + "[--user admin] [--batch-size 1000] [--num-workers 4]" + ); + process.exit(1); + } + return config; +} + +function createPool(endpoint, user, numWorkers) { + const connectionConfig = getNodePostgresConnectionConfig({ + host: endpoint, + user: user, + database: "postgres", + }); + return new Pool({ ...connectionConfig, ssl: { rejectUnauthorized: true }, max: numWorkers }); +} + +async function runOperation(label, fn) { + console.log(); + console.log("=".repeat(60)); + console.log(` ${label}`); + console.log("=".repeat(60)); + const start = Date.now(); + const total = await fn(); + const elapsed = ((Date.now() - start) / 1000).toFixed(2); + console.log(); + console.log(` Summary: ${total} rows affected in ${elapsed}s`); + console.log("=".repeat(60)); + return total; +} + +async function main() { + const config = parseArgs(); + const pool = createPool(config.endpoint, config.user, config.numWorkers); + const table = "batch_test"; + const { batchSize, numWorkers } = config; + + try { + await runOperation("Sequential Batch DELETE (category = 'electronics')", + () => batchDelete(pool, table, "category = 'electronics'", batchSize)); + + await runOperation("Repopulate test data", + () => repopulateTestData(pool, 5000, batchSize)); + + await runOperation("Sequential Batch UPDATE (clothing -> processed)", + () => batchUpdate(pool, table, "status = 'processed'", + "category = 'clothing' AND status != 'processed'", batchSize)); + + await runOperation("Repopulate test data", + () => repopulateTestData(pool, 5000, batchSize)); + + await runOperation(`Parallel Batch DELETE (category = 'food') [${numWorkers} workers]`, + () => parallelBatchDelete(pool, table, "category = 'food'", numWorkers, batchSize)); + + await runOperation("Repopulate test data", + () => repopulateTestData(pool, 5000, batchSize)); + + await runOperation(`Parallel Batch UPDATE (books -> archived) [${numWorkers} workers]`, + () => parallelBatchUpdate(pool, table, "status = 'archived'", + "category = 'books' AND status != 'archived'", numWorkers, batchSize)); + } finally { + await pool.end(); + console.log("\nConnection pool closed. Demo complete."); + } +} + +main().catch((err) => { + console.error("Fatal error:", err.message); + process.exit(1); +}); diff --git a/batch-operations/javascript/node-postgres/src/occRetry.js b/batch-operations/javascript/node-postgres/src/occRetry.js new file mode 100644 index 00000000..26442e34 --- /dev/null +++ b/batch-operations/javascript/node-postgres/src/occRetry.js @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * OCC retry logic with exponential backoff for Aurora DSQL batch operations. + */ + +class MaxRetriesExceededError extends Error { + constructor(maxRetries) { + super(`Max retries exceeded: failed after ${maxRetries} retries`); + this.name = "MaxRetriesExceededError"; + this.maxRetries = maxRetries; + } +} + +function sleep(seconds) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +} + +/** + * Execute a database operation with OCC conflict retry and exponential backoff. + * + * Retries on SQLSTATE 40001 (serialization failure) with exponential backoff. + * Each retry rolls back the current transaction before waiting. + * + * @param {import('pg').PoolClient} client - A node-postgres pool client. + * @param {(client: import('pg').PoolClient) => Promise<*>} operation - Async + * function that performs database work. Should NOT commit. + * @param {number} [maxRetries=3] - Maximum retry attempts. + * @param {number} [baseDelay=0.1] - Base delay in seconds for backoff. + * @returns {Promise<*>} The return value of `operation(client)`. + */ +async function executeWithRetry(client, operation, maxRetries = 3, baseDelay = 0.1) { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation(client); + } catch (e) { + if (e.code === "40001") { + await client.query("ROLLBACK"); + if (attempt >= maxRetries) { + throw new MaxRetriesExceededError(maxRetries); + } + const delay = baseDelay * Math.pow(2, attempt); + console.warn( + `OCC conflict, retry ${attempt + 1}/${maxRetries} after ${delay.toFixed(1)}s` + ); + await sleep(delay); + } else { + throw e; + } + } + } + throw new MaxRetriesExceededError(maxRetries); +} + +module.exports = { MaxRetriesExceededError, executeWithRetry, sleep }; diff --git a/batch-operations/javascript/node-postgres/src/repopulate.js b/batch-operations/javascript/node-postgres/src/repopulate.js new file mode 100644 index 00000000..bf851ace --- /dev/null +++ b/batch-operations/javascript/node-postgres/src/repopulate.js @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** + * Repopulate test data for Aurora DSQL batch operation samples. + */ + +const { executeWithRetry } = require("./occRetry"); + +const INSERT_SQL = + "INSERT INTO batch_test (category, status, value) " + + "SELECT " + + " (ARRAY['electronics','clothing','food','books','toys'])" + + "[floor(random() * 5 + 1)], " + + " 'active', " + + " round((random() * 1000)::numeric, 2) " + + "FROM generate_series(1, $1)"; + +/** + * Insert test rows into batch_test in batches. + * + * @param {import('pg').Pool} pool + * @param {number} [rowCount=5000] + * @param {number} [batchSize=1000] + * @param {number} [maxRetries=3] + * @param {number} [baseDelay=0.1] + * @returns {Promise} Total rows inserted. + */ +async function repopulateTestData(pool, rowCount = 5000, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { + let totalInserted = 0; + let remaining = rowCount; + + while (remaining > 0) { + const currentBatch = Math.min(batchSize, remaining); + const client = await pool.connect(); + try { + const inserted = await executeWithRetry( + client, + async (c) => { + await c.query("BEGIN"); + const result = await c.query(INSERT_SQL, [currentBatch]); + return result.rowCount; + }, + maxRetries, + baseDelay + ); + + await client.query("COMMIT"); + totalInserted += inserted; + remaining -= inserted; + console.log(`Inserted ${inserted} rows (total: ${totalInserted})`); + } catch (err) { + await client.query("ROLLBACK"); + throw err; + } finally { + client.release(); + } + } + + console.log(`Repopulation complete: ${totalInserted} rows inserted`); + return totalInserted; +} + +module.exports = { repopulateTestData }; diff --git a/batch-operations/python/psycopg2/README.md b/batch-operations/python/psycopg2/README.md new file mode 100644 index 00000000..f395e794 --- /dev/null +++ b/batch-operations/python/psycopg2/README.md @@ -0,0 +1,119 @@ +# Batch Operations with psycopg2 + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[psycopg2](https://www.psycopg.org/docs/) with the +[Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic +IAM authentication and connection pooling. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across worker threads using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. + +Connection pooling uses `AuroraDSQLThreadedConnectionPool` from the Aurora DSQL Python Connector, which +automatically handles IAM token refresh for long-running batch jobs. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Python 3.8 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up + +Create and activate a Python virtual environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Install the required packages: + +```bash +pip install -r requirements.txt +``` + +## Set up the test table + +Before running the examples, create and populate the test table. Run each INSERT as a separate +transaction (each block of 1,000 rows): + +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Run the demo: + +```bash +python src/main.py --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | + +The demo runs the following sequence: + +1. Sequential batch DELETE (all `electronics` rows) +2. Repopulate test data +3. Sequential batch UPDATE (`clothing` → `processed`) +4. Repopulate test data +5. Parallel batch DELETE (all `food` rows) with N workers +6. Repopulate test data +7. Parallel batch UPDATE (`books` → `archived`) with N workers + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) +- [Psycopg2 Documentation](https://www.psycopg.org/docs/) +- [Batch operations in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/batch-operations.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/python/psycopg2/src/batch_delete.py b/batch-operations/python/psycopg2/src/batch_delete.py new file mode 100644 index 00000000..baa968fe --- /dev/null +++ b/batch-operations/python/psycopg2/src/batch_delete.py @@ -0,0 +1,67 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Sequential and parallel batch DELETE for Aurora DSQL. + +Deletes rows matching a condition in configurable-size batches, committing each +batch as a separate transaction to stay within the 3,000-row mutation limit. +Uses OCC retry logic with exponential backoff for serialization conflicts. + +The parallel variant partitions rows across worker threads using +``abs(hashtext(id::text)) % num_workers`` so that each worker operates on a +disjoint subset, avoiding OCC conflicts between workers. +""" + +import threading + +from occ_retry import execute_with_retry + + +def batch_delete(pool, table, condition, batch_size=1000, max_retries=3, base_delay=0.1): + """Delete rows in batches, committing each batch as a separate transaction. + + Args: + pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. + table: Name of the table to delete from. + condition: SQL WHERE clause (without the ``WHERE`` keyword). + batch_size: Number of rows to delete per transaction (default 1,000). + max_retries: Maximum OCC retry attempts per batch (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + Total number of rows deleted across all batches. + """ + total_deleted = 0 + + while True: + conn = pool.getconn() + try: + + def delete_batch(conn): + with conn.cursor() as cur: + cur.execute( + f"""DELETE FROM {table} + WHERE id IN ( + SELECT id FROM {table} + WHERE {condition} + LIMIT {batch_size} + )""" + ) + return cur.rowcount + + deleted = execute_with_retry( + conn, delete_batch, max_retries=max_retries, base_delay=base_delay + ) + conn.commit() + total_deleted += deleted + print(f"Deleted {deleted} rows (total: {total_deleted})") + + if deleted == 0: + break + except Exception: + conn.rollback() + raise + finally: + pool.putconn(conn) + + return total_deleted diff --git a/batch-operations/python/psycopg2/src/batch_update.py b/batch-operations/python/psycopg2/src/batch_update.py new file mode 100644 index 00000000..7a6ad44d --- /dev/null +++ b/batch-operations/python/psycopg2/src/batch_update.py @@ -0,0 +1,68 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Sequential batch UPDATE for Aurora DSQL. + +Updates rows matching a condition in configurable-size batches, committing each +batch as a separate transaction to stay within the 3,000-row mutation limit. +Uses a subquery pattern to select rows not yet updated, and sets updated_at = +NOW() to track processed rows. +""" + +from occ_retry import execute_with_retry + + +def batch_update(pool, table, set_clause, condition, batch_size=1000, max_retries=3, base_delay=0.1): + """Update rows in batches, committing each batch as a separate transaction. + + Uses a subquery to select rows not yet updated and sets ``updated_at = NOW()`` + on each batch to track which rows have been processed. + + Args: + pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. + table: Name of the table to update. + set_clause: SQL SET expressions (without the ``SET`` keyword), + e.g. ``"status = 'processed'"``. + condition: SQL WHERE clause (without the ``WHERE`` keyword) that + identifies rows needing update. + batch_size: Number of rows to update per transaction (default 1,000). + max_retries: Maximum OCC retry attempts per batch (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + Total number of rows updated across all batches. + """ + total_updated = 0 + + while True: + conn = pool.getconn() + try: + + def update_batch(conn): + with conn.cursor() as cur: + cur.execute( + f"""UPDATE {table} SET {set_clause}, updated_at = NOW() + WHERE id IN ( + SELECT id FROM {table} + WHERE {condition} + LIMIT {batch_size} + )""" + ) + return cur.rowcount + + updated = execute_with_retry( + conn, update_batch, max_retries=max_retries, base_delay=base_delay + ) + conn.commit() + total_updated += updated + print(f"Updated {updated} rows (total: {total_updated})") + + if updated == 0: + break + except Exception: + conn.rollback() + raise + finally: + pool.putconn(conn) + + return total_updated diff --git a/batch-operations/python/psycopg2/src/main.py b/batch-operations/python/psycopg2/src/main.py new file mode 100644 index 00000000..379dd0f0 --- /dev/null +++ b/batch-operations/python/psycopg2/src/main.py @@ -0,0 +1,155 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Main entry point for Aurora DSQL batch operations demo. + +Demonstrates sequential and parallel batch DELETE and UPDATE operations +against an Aurora DSQL cluster, with OCC retry logic and connection pooling. + +Usage: + python main.py --endpoint [--user admin] \ + [--batch-size 1000] [--num-workers 4] +""" + +import argparse +import time + +import aurora_dsql_psycopg2 as dsql + +from batch_delete import batch_delete +from batch_update import batch_update +from parallel_batch_delete import parallel_batch_delete +from parallel_batch_update import parallel_batch_update +from repopulate import repopulate_test_data + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Aurora DSQL batch operations demo" + ) + parser.add_argument( + "--endpoint", required=True, help="Aurora DSQL cluster endpoint" + ) + parser.add_argument( + "--user", default="admin", help="Database user (default: admin)" + ) + parser.add_argument( + "--batch-size", + type=int, + default=1000, + help="Rows per batch transaction (default: 1000)", + ) + parser.add_argument( + "--num-workers", + type=int, + default=4, + help="Number of parallel workers (default: 4)", + ) + return parser.parse_args() + + +def create_pool(endpoint, user, num_workers): + """Create a threaded connection pool sized for parallel workers.""" + conn_params = { + "user": user, + "host": endpoint, + "sslmode": "verify-full", + "sslrootcert": "./root.pem", + } + return dsql.AuroraDSQLThreadedConnectionPool( + minconn=num_workers, + maxconn=num_workers, + **conn_params, + ) + + +def run_operation(label, func, *args, **kwargs): + """Run a batch operation, print a summary with elapsed time.""" + print(f"\n{'=' * 60}") + print(f" {label}") + print(f"{'=' * 60}") + start = time.time() + total = func(*args, **kwargs) + elapsed = time.time() - start + print(f"\n Summary: {total} rows affected in {elapsed:.2f}s") + print(f"{'=' * 60}") + return total + + +def main(): + args = parse_args() + pool = create_pool(args.endpoint, args.user, args.num_workers) + + table = "batch_test" + batch_size = args.batch_size + num_workers = args.num_workers + + try: + # 1. Sequential batch DELETE — delete all 'electronics' rows + run_operation( + "Sequential Batch DELETE (category = 'electronics')", + batch_delete, + pool, table, "category = 'electronics'", + batch_size=batch_size, + ) + + # 2. Repopulate test data + run_operation( + "Repopulate test data", + repopulate_test_data, + pool, + batch_size=batch_size, + ) + + # 3. Sequential batch UPDATE — update 'clothing' status to 'processed' + run_operation( + "Sequential Batch UPDATE (clothing -> processed)", + batch_update, + pool, table, "status = 'processed'", + "category = 'clothing' AND status != 'processed'", + batch_size=batch_size, + ) + + # 4. Repopulate test data + run_operation( + "Repopulate test data", + repopulate_test_data, + pool, + batch_size=batch_size, + ) + + # 5. Parallel batch DELETE — delete all 'food' rows + run_operation( + f"Parallel Batch DELETE (category = 'food') [{num_workers} workers]", + parallel_batch_delete, + pool, table, "category = 'food'", + num_workers=num_workers, + batch_size=batch_size, + ) + + # 6. Repopulate test data + run_operation( + "Repopulate test data", + repopulate_test_data, + pool, + batch_size=batch_size, + ) + + # 7. Parallel batch UPDATE — update 'books' status to 'archived' + run_operation( + f"Parallel Batch UPDATE (books -> archived) [{num_workers} workers]", + parallel_batch_update, + pool, table, "status = 'archived'", + "category = 'books' AND status != 'archived'", + num_workers=num_workers, + batch_size=batch_size, + ) + + finally: + pool.closeall() + print("\nConnection pool closed. Demo complete.") + + +if __name__ == "__main__": + main() diff --git a/batch-operations/python/psycopg2/src/occ_retry.py b/batch-operations/python/psycopg2/src/occ_retry.py new file mode 100644 index 00000000..1a150f6b --- /dev/null +++ b/batch-operations/python/psycopg2/src/occ_retry.py @@ -0,0 +1,60 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""OCC retry logic with exponential backoff for Aurora DSQL batch operations.""" + +import logging +import time + +import psycopg2 +import psycopg2.errors + +logger = logging.getLogger(__name__) + + +class MaxRetriesExceeded(Exception): + """Raised when a batch operation exceeds the maximum number of OCC retries.""" + + def __init__(self, max_retries): + super().__init__( + f"Max retries exceeded: failed after {max_retries} retries" + ) + self.max_retries = max_retries + + +def execute_with_retry(connection, operation, max_retries=3, base_delay=0.1): + """Execute a database operation with OCC conflict retry and exponential backoff. + + Runs ``operation(connection)`` and retries on serialization failures + (SQLSTATE 40001) using exponential backoff. Each retry rolls back the + current transaction before waiting. + + Args: + connection: A psycopg2 connection object. + operation: A callable that accepts a connection and performs database + work. It should NOT commit — the caller is responsible for commits. + max_retries: Maximum number of retry attempts (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + The return value of ``operation(connection)``. + + Raises: + MaxRetriesExceeded: If the operation fails with OCC conflicts after + ``max_retries`` retry attempts. + """ + for attempt in range(max_retries + 1): + try: + return operation(connection) + except psycopg2.errors.SerializationFailure: + connection.rollback() + if attempt >= max_retries: + raise MaxRetriesExceeded(max_retries) + delay = base_delay * (2 ** attempt) + logger.warning( + "OCC conflict, retry %d/%d after %.1fs", + attempt + 1, + max_retries, + delay, + ) + time.sleep(delay) diff --git a/batch-operations/python/psycopg2/src/parallel_batch_delete.py b/batch-operations/python/psycopg2/src/parallel_batch_delete.py new file mode 100644 index 00000000..9347af8a --- /dev/null +++ b/batch-operations/python/psycopg2/src/parallel_batch_delete.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Parallel batch DELETE for Aurora DSQL. + +Partitions rows across worker threads using ``abs(hashtext(id::text)) % num_workers`` +so that each worker operates on a disjoint subset, avoiding OCC conflicts between +workers. Each worker runs a sequential batch-delete loop on its partition. +""" + +import threading + +from occ_retry import execute_with_retry + + +def parallel_batch_delete( + pool, table, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay=0.1 +): + """Delete rows in parallel using multiple worker threads. + + Args: + pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance sized to at + least *num_workers* connections. + table: Name of the table to delete from. + condition: SQL WHERE clause (without the ``WHERE`` keyword). + num_workers: Number of parallel worker threads (default 4). + batch_size: Number of rows to delete per transaction (default 1,000). + max_retries: Maximum OCC retry attempts per batch (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + Total number of rows deleted across all workers. + """ + results = [0] * num_workers + errors = [None] * num_workers + + def worker(worker_id): + total_deleted = 0 + partition_condition = ( + f"{condition} AND abs(hashtext(id::text)) % {num_workers} = {worker_id}" + ) + + while True: + conn = pool.getconn() + try: + + def delete_batch(conn): + with conn.cursor() as cur: + cur.execute( + f"""DELETE FROM {table} + WHERE id IN ( + SELECT id FROM {table} + WHERE {partition_condition} + LIMIT {batch_size} + )""" + ) + return cur.rowcount + + deleted = execute_with_retry( + conn, delete_batch, max_retries=max_retries, base_delay=base_delay + ) + conn.commit() + total_deleted += deleted + print( + f"Worker {worker_id}: Deleted {deleted} rows " + f"(total: {total_deleted})" + ) + + if deleted == 0: + break + except Exception as exc: + conn.rollback() + errors[worker_id] = exc + break + finally: + pool.putconn(conn) + + results[worker_id] = total_deleted + + threads = [ + threading.Thread(target=worker, args=(i,)) for i in range(num_workers) + ] + for t in threads: + t.start() + for t in threads: + t.join() + + for err in errors: + if err is not None: + raise err + + total = sum(results) + print(f"Parallel delete complete: {total} rows deleted by {num_workers} workers") + return total diff --git a/batch-operations/python/psycopg2/src/parallel_batch_update.py b/batch-operations/python/psycopg2/src/parallel_batch_update.py new file mode 100644 index 00000000..4bb73405 --- /dev/null +++ b/batch-operations/python/psycopg2/src/parallel_batch_update.py @@ -0,0 +1,95 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Parallel batch UPDATE for Aurora DSQL. + +Partitions rows across worker threads using ``abs(hashtext(id::text)) % num_workers`` +so that each worker operates on a disjoint subset, avoiding OCC conflicts between +workers. Each worker runs a sequential batch-update loop on its partition. +""" + +import threading + +from occ_retry import execute_with_retry + + +def parallel_batch_update( + pool, table, set_clause, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay=0.1 +): + """Update rows in parallel using multiple worker threads. + + Args: + pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance sized to at + least *num_workers* connections. + table: Name of the table to update. + set_clause: SQL SET expressions (without the ``SET`` keyword). + condition: SQL WHERE clause (without the ``WHERE`` keyword). + num_workers: Number of parallel worker threads (default 4). + batch_size: Number of rows to update per transaction (default 1,000). + max_retries: Maximum OCC retry attempts per batch (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + Total number of rows updated across all workers. + """ + results = [0] * num_workers + errors = [None] * num_workers + + def worker(worker_id): + total_updated = 0 + partition_condition = ( + f"{condition} AND abs(hashtext(id::text)) % {num_workers} = {worker_id}" + ) + + while True: + conn = pool.getconn() + try: + + def update_batch(conn): + with conn.cursor() as cur: + cur.execute( + f"""UPDATE {table} SET {set_clause}, updated_at = NOW() + WHERE id IN ( + SELECT id FROM {table} + WHERE {partition_condition} + LIMIT {batch_size} + )""" + ) + return cur.rowcount + + updated = execute_with_retry( + conn, update_batch, max_retries=max_retries, base_delay=base_delay + ) + conn.commit() + total_updated += updated + print( + f"Worker {worker_id}: Updated {updated} rows " + f"(total: {total_updated})" + ) + + if updated == 0: + break + except Exception as exc: + conn.rollback() + errors[worker_id] = exc + break + finally: + pool.putconn(conn) + + results[worker_id] = total_updated + + threads = [ + threading.Thread(target=worker, args=(i,)) for i in range(num_workers) + ] + for t in threads: + t.start() + for t in threads: + t.join() + + for err in errors: + if err is not None: + raise err + + total = sum(results) + print(f"Parallel update complete: {total} rows updated by {num_workers} workers") + return total diff --git a/batch-operations/python/psycopg2/src/repopulate.py b/batch-operations/python/psycopg2/src/repopulate.py new file mode 100644 index 00000000..4297f419 --- /dev/null +++ b/batch-operations/python/psycopg2/src/repopulate.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +"""Repopulate test data for Aurora DSQL batch operation samples. + +Inserts rows into the batch_test table in batches, respecting the 3,000-row +transaction limit. Called between DELETE and UPDATE demonstrations to restore +the test dataset. +""" + +from occ_retry import execute_with_retry + + +def repopulate_test_data(pool, row_count=5000, batch_size=1000, max_retries=3, base_delay=0.1): + """Insert test rows into batch_test in batches. + + Each batch is committed as a separate transaction to stay within the + 3,000-row mutation limit. + + Args: + pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. + row_count: Total number of rows to insert (default 5,000). + batch_size: Number of rows to insert per transaction (default 1,000). + max_retries: Maximum OCC retry attempts per batch (default 3). + base_delay: Base delay in seconds for exponential backoff (default 0.1). + + Returns: + Total number of rows inserted across all batches. + """ + total_inserted = 0 + remaining = row_count + + while remaining > 0: + current_batch = min(batch_size, remaining) + conn = pool.getconn() + try: + + def insert_batch(conn, size=current_batch): + with conn.cursor() as cur: + cur.execute( + "INSERT INTO batch_test (category, status, value) " + "SELECT " + " (ARRAY['electronics','clothing','food','books','toys'])" + "[floor(random() * 5 + 1)], " + " 'active', " + " round((random() * 1000)::numeric, 2) " + "FROM generate_series(1, %s)", + (size,), + ) + return cur.rowcount + + inserted = execute_with_retry( + conn, insert_batch, max_retries=max_retries, base_delay=base_delay + ) + conn.commit() + total_inserted += inserted + remaining -= inserted + print(f"Inserted {inserted} rows (total: {total_inserted})") + except Exception: + conn.rollback() + raise + finally: + pool.putconn(conn) + + print(f"Repopulation complete: {total_inserted} rows inserted") + return total_inserted diff --git a/batch-operations/sql/batch_test_setup.sql b/batch-operations/sql/batch_test_setup.sql new file mode 100644 index 00000000..d6428ee0 --- /dev/null +++ b/batch-operations/sql/batch_test_setup.sql @@ -0,0 +1,73 @@ +-- ============================================================================= +-- Sample table setup for Aurora DSQL batch operations +-- +-- This script creates a test table and populates it with 5,000 rows of sample +-- data. Use this table to test the batch DELETE and UPDATE code samples. +-- +-- Aurora DSQL limits each transaction to 3,000 row mutations, so we insert +-- in batches of 1,000 rows. Run each INSERT as a separate transaction. +-- ============================================================================= + +-- Drop the table if it already exists +DROP TABLE IF EXISTS batch_test; + +-- Create the sample table. +-- Uses UUID primary key with gen_random_uuid() to minimize OCC contention, +-- following Aurora DSQL best practice of random keys. +CREATE TABLE batch_test ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + value NUMERIC(10,2), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create an asynchronous index on the category column. +-- Aurora DSQL requires CREATE INDEX ASYNC for tables with existing rows. +CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); + +-- ============================================================================= +-- Populate the table with 5,000 rows of test data. +-- Each INSERT is 1,000 rows — run each as a separate transaction. +-- ============================================================================= + +-- Batch 1: rows 1–1,000 +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +-- Batch 2: rows 1,001–2,000 +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +-- Batch 3: rows 2,001–3,000 +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +-- Batch 4: rows 3,001–4,000 +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +-- Batch 5: rows 4,001–5,000 +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); From cffd30517390011d931d9bf0d92290f7d7ffd985 Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Mon, 16 Mar 2026 19:35:30 -0700 Subject: [PATCH 2/8] Added cleanup instructions to READMEs. --- batch-operations/GITHUB_ISSUE.md | 69 ------------------- batch-operations/java/pgjdbc/README.md | 9 +++ .../javascript/node-postgres/README.md | 9 +++ batch-operations/python/psycopg2/README.md | 9 +++ 4 files changed, 27 insertions(+), 69 deletions(-) delete mode 100644 batch-operations/GITHUB_ISSUE.md diff --git a/batch-operations/GITHUB_ISSUE.md b/batch-operations/GITHUB_ISSUE.md deleted file mode 100644 index 741e3696..00000000 --- a/batch-operations/GITHUB_ISSUE.md +++ /dev/null @@ -1,69 +0,0 @@ -**Title:** Add batch DELETE/UPDATE code samples for datasets exceeding 3,000-row transaction limit - -**Description:** - -Aurora DSQL limits each transaction to 3,000 row mutations. This is a common stumbling block for users who need to DELETE or UPDATE large datasets. I'd like to contribute code samples that demonstrate how to handle this across Python, Java, and JavaScript. - -**What the samples cover:** - -Two patterns, each implemented in all three languages: - -1. **Sequential batch processing** — single-threaded loop that processes rows in batches of N (default 1,000), committing each batch as a separate transaction -2. **Parallel batch processing** — multiple worker threads partition rows using `abs(hashtext(id::text)) % num_workers` and process their partitions concurrently, each running its own batch loop - -Both patterns include: -- OCC retry logic with exponential backoff (SQLSTATE 40001 handling) -- Connection pooling using the DSQL connectors (auto IAM token refresh) -- Subquery-based DELETE (`DELETE WHERE id IN (SELECT id ... LIMIT N)`) since PostgreSQL doesn't support `DELETE ... LIMIT` -- Subquery-based UPDATE with `updated_at` tracking to prevent reprocessing -- Table repopulation between DELETE and UPDATE demos -- CLI entry point with configurable batch size and worker count - -**Languages and drivers:** - -| Language | Driver | Connector | -|----------|--------|-----------| -| Python | psycopg2 | `aurora-dsql-python-connector` (`AuroraDSQLThreadedConnectionPool`) | -| Java | pgJDBC | AWS JDBC Wrapper (`AwsWrapperDataSource`) | -| JavaScript | node-postgres | `@aws/aurora-dsql-node-postgres-connector` | - -**Files per language:** -- OCC retry module (reusable) -- Sequential batch DELETE -- Sequential batch UPDATE -- Parallel batch DELETE (hashtext partitioning) -- Parallel batch UPDATE (hashtext partitioning) -- Repopulate helper (restores test data between demos) -- Main entry point with CLI args -- README with prerequisites, setup, and usage instructions - -**Shared:** -- SQL setup script (creates `batch_test` table, inserts 5,000 rows in batches of 1,000) - -**Proposed structure:** - -I have this organized as a `batch-operations/` top-level directory following the existing repo conventions (`language/driver/src/`). Happy to restructure if you'd prefer these integrated differently — for example, as additional files within the existing `python/psycopg2/`, `java/pgjdbc/`, and `javascript/node-postgres/` directories. - -``` -batch-operations/ -├── README.md -├── sql/batch_test_setup.sql -├── python/psycopg2/ -│ ├── README.md -│ ├── requirements.txt -│ └── src/ -├── javascript/node-postgres/ -│ ├── README.md -│ ├── package.json -│ └── src/ -└── java/pgjdbc/ - ├── README.md - ├── build.gradle - └── src/main/java/... -``` - -**Tested against:** Aurora DSQL cluster in us-east-1 with the Python implementation. All seven operations (sequential delete, repopulate, sequential update, repopulate, parallel delete, repopulate, parallel update) complete successfully. - -The parallel pattern is inspired by the [re:Invent DAT401 parallel worker approach](https://github.com/marcbowes/riv25-codetalk/). - -Happy to adjust structure, naming, or scope based on your feedback. diff --git a/batch-operations/java/pgjdbc/README.md b/batch-operations/java/pgjdbc/README.md index 268ca336..a158fe20 100644 --- a/batch-operations/java/pgjdbc/README.md +++ b/batch-operations/java/pgjdbc/README.md @@ -78,6 +78,15 @@ Build and run: | `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | | `--num-workers` | `4` | Number of parallel worker threads | +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + ## Additional resources - [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) diff --git a/batch-operations/javascript/node-postgres/README.md b/batch-operations/javascript/node-postgres/README.md index ef7b6295..76daf217 100644 --- a/batch-operations/javascript/node-postgres/README.md +++ b/batch-operations/javascript/node-postgres/README.md @@ -85,6 +85,15 @@ node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" | `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | | `--num-workers` | `4` | Number of parallel async workers | +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + ## Additional resources - [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) diff --git a/batch-operations/python/psycopg2/README.md b/batch-operations/python/psycopg2/README.md index f395e794..e53d61dd 100644 --- a/batch-operations/python/psycopg2/README.md +++ b/batch-operations/python/psycopg2/README.md @@ -106,6 +106,15 @@ The demo runs the following sequence: 6. Repopulate test data 7. Parallel batch UPDATE (`books` → `archived`) with N workers +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + ## Additional resources - [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) From dd05b994c01bc4e71ee246e9485cfe6ab4823948 Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Tue, 17 Mar 2026 11:52:12 -0700 Subject: [PATCH 3/8] switch Java to DSQL JDBC connector --- batch-operations/java/pgjdbc/build.gradle | 8 +-- .../src/main/java/com/example/dsql/Main.java | 54 ++++++++++++------ .../__pycache__/batch_delete.cpython-310.pyc | Bin 0 -> 2389 bytes .../__pycache__/batch_update.cpython-310.pyc | Bin 0 -> 2539 bytes .../src/__pycache__/occ_retry.cpython-310.pyc | Bin 0 -> 2265 bytes .../parallel_batch_delete.cpython-310.pyc | Bin 0 -> 3262 bytes .../parallel_batch_update.cpython-310.pyc | Bin 0 -> 3387 bytes .../__pycache__/repopulate.cpython-310.pyc | Bin 0 -> 2237 bytes 8 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc create mode 100644 batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc create mode 100644 batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc create mode 100644 batch-operations/python/psycopg2/src/__pycache__/parallel_batch_delete.cpython-310.pyc create mode 100644 batch-operations/python/psycopg2/src/__pycache__/parallel_batch_update.cpython-310.pyc create mode 100644 batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc diff --git a/batch-operations/java/pgjdbc/build.gradle b/batch-operations/java/pgjdbc/build.gradle index 6b4bf248..86dabf6e 100644 --- a/batch-operations/java/pgjdbc/build.gradle +++ b/batch-operations/java/pgjdbc/build.gradle @@ -16,10 +16,10 @@ repositories { } dependencies { - implementation 'org.postgresql:postgresql:42.7.4' - implementation 'software.amazon.jdbc:aws-advanced-jdbc-wrapper:2.5.4' - implementation 'software.amazon.awssdk:dsql:2.31.9' - implementation 'software.amazon.awssdk:auth:2.31.9' + implementation 'org.postgresql:postgresql:42.7.10' + implementation 'com.zaxxer:HikariCP:7.0.2' + implementation 'software.amazon.dsql:aurora-dsql-jdbc-connector:1.4.0' + implementation 'software.amazon.awssdk:dsql:2.42.14' } application { diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java index 6f780cd9..7ac5c8f6 100644 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java +++ b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java @@ -3,18 +3,23 @@ package com.example.dsql; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import javax.sql.DataSource; -import software.amazon.jdbc.ds.AwsWrapperDataSource; /** * Main entry point for Aurora DSQL batch operations demo (Java). * + * Uses HikariCP with the Aurora DSQL JDBC Connector for automatic + * IAM authentication and connection pooling. + * * Usage: - * java Main --endpoint [--user admin] - * [--batch-size 1000] [--num-workers 4] + * gradle run --args="--endpoint <cluster-endpoint> [--user admin] + * [--batch-size 1000] [--num-workers 4]" */ public class Main { @@ -37,19 +42,27 @@ private static int runOperation(String label, BatchOperation op) return total; } - private static DataSource createDataSource(String endpoint, String user) { - AwsWrapperDataSource ds = new AwsWrapperDataSource(); - ds.setJdbcProtocol("jdbc:postgresql:"); - ds.setServerName(endpoint); - ds.setServerPort("5432"); - ds.setDatabase("postgres"); - ds.setUser(user); - ds.setSslMode("verify-full"); - ds.setTargetDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); - Map props = new HashMap<>(); - props.put("sslmode", "verify-full"); - ds.setTargetDataSourceProperties(props); - return ds; + /** + * Create a HikariCP DataSource configured for Aurora DSQL with automatic + * IAM authentication via the Aurora DSQL JDBC Connector. + */ + private static HikariDataSource createDataSource(String endpoint, String user, int numWorkers) { + String jdbcUrl = "jdbc:aws-dsql:postgresql://" + endpoint; + + HikariConfig config = new HikariConfig(); + config.setJdbcUrl(jdbcUrl); + config.setUsername(user); + + // Pool sized for parallel workers + config.setPoolName("BatchOpsPool"); + config.setMaximumPoolSize(numWorkers); + config.setMinimumIdle(1); + config.setConnectionTimeout(30000); + config.setMaxLifetime(3300000); // 55 min (connector handles token refresh) + config.setConnectionTestQuery("SELECT 1"); + config.setAutoCommit(true); + + return new HikariDataSource(config); } private static Map parseArgs(String[] args) { @@ -70,8 +83,8 @@ private static Map parseArgs(String[] args) { } } if (!config.containsKey("endpoint")) { - System.err.println("Usage: java Main --endpoint " - + "[--user admin] [--batch-size 1000] [--num-workers 4]"); + System.err.println("Usage: gradle run --args=\"--endpoint " + + "[--user admin] [--batch-size 1000] [--num-workers 4]\""); System.exit(1); } return config; @@ -83,7 +96,8 @@ public static void main(String[] args) { String user = config.get("user"); int batchSize = Integer.parseInt(config.get("batch-size")); int numWorkers = Integer.parseInt(config.get("num-workers")); - DataSource pool = createDataSource(endpoint, user); + + HikariDataSource pool = createDataSource(endpoint, user, numWorkers); String table = "batch_test"; try { @@ -117,6 +131,8 @@ public static void main(String[] args) { } catch (OccRetry.MaxRetriesExceededException e) { System.err.println("Max retries exceeded: " + e.getMessage()); System.exit(1); + } finally { + pool.close(); } System.out.println("\nDemo complete."); } diff --git a/batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c5f06f2bf1407e81318f0ea4f6351df89fb2ac6 GIT binary patch literal 2389 zcmaJ?UvC>l5Z}E!+h^AaZ9%BwkFRt8m0~uUwsjtvC*~Hb$HVo0a@Hs=$Tf<`S9{ zEoq`?V7+NUl%_+n9cnm<=uJ*8otvARI}aBiajL1+ib8POsC=#9X15j=h~!!hNhCJH zz*-W%GZ4un+#U_KM89u?Q(Qs@!cp$V+&c;btsu}2-QzTUoUZvZmJyqMJYE$zaT?<*OWhV7H`G_QG+{ukpw@Z`&Q>tx34t$9L zH@Q#*GD<5fRJTP47f@+Waea=^yCP&Ji6+{^Ym*+=+ngs8ClJOnfCXBv|h%UtH&_ji4b$EC49_QnNP_}<$sk4t;bV}#MTI4*yJc8l6&F#8#?(zpoHe^j`K zVYkfimLor1LuT8{KgM<5wMo zadw^Ux+!FfeT!7y(9@ZG|(=^iL zf??m3x0vo&IE7rb$W6Kv#%VmoL8>*62U-#1Fem$=t4mXEdQ|Za#7u`R@;S^+{6Kqn zHN{QT%F^{^G(L1U*SG)eVNo=sbwtMP}CPz z38>~iKYXor?sxQhr5o38USB1ncV6kKUU#!%kV++FR+`kH%m=UqB29GN$vk-7By&OK ziJRr^z+y8iAA&tASXMki&vg?@yi`r-Rj{jc(-hG`L<8QOUu)0ru->8wR8z^|*mqVaCrFNt#$cFL^J&Mf*UZbG0a3vo_Oau5 z`;P1V=Xk|`y%(N;_V3XL$LudhBA}7F(wKGvai5qXj*0sK{v&_UGM|yrY);CWHsEcM z=eWLHFJ+|-u1(IQ!6M|=Hq@*zkkB8p(jc{)S$SzE-~-bfO}3-RG`)Hy^Gw;2vtTJ_ zj4)W1r;RX(UZ1f?86HBDXVO%EdCF{*4wl|9ODBvt2_mab$NCnms0Ijc)^U7D&q>Fr z`ndSQcYXXe_8kvb3ZCQrQ-xgzJNwT5nWFPlz6U0CSDHQ}JtGQcp^%k29VUWKCoAWj q75d$JSp-4ezHI!~R@zG)341gDax7RH@hgVD$_L@O{)wtH>;4a`eyfuJ literal 0 HcmV?d00001 diff --git a/batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f02b63f8a40e7bbb252064de1e8e8d477caacfcb GIT binary patch literal 2539 zcmai0QEwYX5Z=8z`_7INpj4>h0j&yBb5d%j6bUJ63n2}qQk#}KQAC7fZ*JGgHRrqQ z?jDIP=Y>{EUJ%cKDy2`T5US-X98adnw=5(w<0k%6IX6OJQ#B&AAm0lUBmk9eSpu;WCL z9#0hf|9W29bv3du!2&QI|@9%PEQ zbcij%mFPYF14{4|9`yTCxNTghhqxV0Rp5Gv|S~Ur~7A4Jw1upv+3&Is4UtcVs<67!BM(^<%W}Dw~xD)t?Y^2QEnO zVd*}G)hfd~mRMLv>)O6E@E++|w+Y{I%|WWvyN1CE%y~y;ZSPyOJE0qw$NP5CJsTj? zg}@6S6bXgGm^%JPfrof{Wq|=w1jfsidws>m1#7p5MKmQNrey85F$qxA2pa}4D&y{U z2$KfB+w>biCg<_Ew!?51#g<$!R&;P_l0*w+k+j=P-izu*L+E~3-4>iO&X$rm26O@X zZ-Z*P4Lty$jRRgE*-*L{7RU+f4-X=SD7d=)AsFhq%u|I4YCmGC4Pb@wv0@_SomBe+!rE zUo78QCPff0`!j1NJVF>$O?VBNCX-Hh%o8!^On3at2Q{E+X@Y>vWZg|JbZ$&-Mrk1_ z%${U~S;jjwixjz_dB7hZp-cBn`+?g9a+B_b-Kc%0zquOsk0k zmW?M*I|MPIfk-4!STy8eE{o#{;ApfOXn8y=c$tCcZFuC@APV;Qp6ufRKEezi+Q0(V zp#{=5@6I8H_tGH(S$;TWcmUtE){zC5gad!~ZH$`e0+Q!;alr{+0Ut1!^WBxcJG>M6 zB?HKQXKsQni{6`nfd=65C;$~Qa(#u&zEVqTHygKgN zykwgDF?yyNKy5-c1dTvbss=RaP8!jGHx}2Mi+gPIY7)qX6v4!Iq%T?3$hH~=spk^_ zt-0!CBke)+$Bk6>f+XGg&}=OlY1m$AFRvA^Ei>sVJ+Q|cJ+p-SAIc-@U<}iDu`uy?m%0hdB7Q}8xGaNk?3y=Y{|m!x)H478 literal 0 HcmV?d00001 diff --git a/batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3928283d93c429ff4693ea59033cf44c6d95e656 GIT binary patch literal 2265 zcmZ`)TW=dh6rR~#+v{zUhSCb5N@xT`swJ>fE-wKAp$$+}Qjr8zSVEie&aAWPde@zq zC9&m7h*aty-~|v;^4Na>e`O!x0p$nqfk$Uqel2S-9{A`;iS_*c!FEhrURt<3RoMvWUvK~K5dPR?AMXw_j z%VRdR5Ck?OX8{_nE6@?4egy_Yj>!plbwqzA!uiELrId{5h=h(HfX&?+z6Ri|yFl1P ztrY7l=1B_hcx*uB(4U@YxMf8b;GxnF!z4>g7?$S%V2BO}2@R+8v`k}#C?3(W9)?lM zwGP8a=-kL7-O?(0;ydL=!r`LZvH^N? z9-vl#VD|H@)z^b4?|0v_SMw;cXa>#xKw+X?L}x%2k}3_T_g~-+uRI%&=G2@<6-~={ zPl9Pd81`~eq_{pGhW84dPX4Kdp~#~!v>M8#iJ6g1Q`oD6vhh&E71n~n&|HA7t1$S) zqn8}JcZRY0^QRxN{q|vmfTs;l;@#`e^#}$-Mv!MOkz;y;eDm&oZ&<@v9@9I-Ar#{2 z5kn8019$Kqg>Q&7Iq^<$_D0U#ihA8tM$R$)o{n7MjJy}gC33%FswXw!jy#k*az>R? zj}qgb)L{j=@-?|vJ%n0=T&WZ{0qW+Fj0z(eXM!8v;abiMW?X5QoDkkPO(HX?L!OD} z6<{+67_{vo(`;{V7S^k9JChNXU}JBO;pItou{>jtK~TfV(3Yr8(?UsY1zm+A-r3#Q z-DK~qt*u>autFzU_qm$cCWlfVvxLgiR9jNOa0rJV8AB(mZt@zWBmWgG!=r}yCCCPI#_2nW9zF~fNZMi zXHy&kgdF)q4EFXG{01rfq?^XBIipa}XUGb})y0x|V{&>`v{tA5yV|@K>#asmRv;g{ zvh=5^RaT{f_e+iqgl_#@Eg=O<9HaunW{(JLD>Z94@Q$hYW!TrR!mx7Lq04lI`u2C8 zdeos+=nnP%S*|*NdH*Ur)^OAcOzH((pHs^)l{G{UsMQtx=Q0fK3sXNdCvK>BrMYE| vROj#%>MiT|==PCLtQ)L?1M4x%%=7%7 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 literal 0 HcmV?d00001 diff --git a/batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b284f9348b41c3c43580e52546fff7d448a6906 GIT binary patch literal 2237 zcmZ`)&u<$=6rNeH?X~lRQjqu|f#v|jTT(k|Re?-ZYLXyDDrrelMNL&XW**Lt54!4vjQAi+ZINw&Z^9Lgttqqwt?KK(Vy!)7I6|# z&57np6Gk;9eIZFFkwQ|kyz%)B(xckjB_igMYF|V|(J&6UYS!zkk>XM-BE`NUexwD_ zyS(u4=J_=31srCIcU;XA$yLlf?MH*$_wxM0!oo#}uWLyoMPUY32>j63O|nFTfHTtL zdY|)%EU(;H*<2wsV&wL%-L4K#+^G^ZbB& ziRRsXU+;D$*K&CL^(zP+{{h$?8~lF%{yQ^%{@5#i|eKeX&t9@XAl^)@7Opm!{U3Kk2vPMOSEFIYU~4>%_^ zCqkf015;EHN)(SNFar3WVHBuQnhuEp9nt|kd7=sZBusRnu7dkwosJx+cJ63ajfDu> zq(gRgnEE1U77{fI-Q1O&GR~Gn6d7%a=oYx{?11oK(a7V?Ne=kFt|yX6x5=i^G$2tD z_P8XXUx*m?2|*xg(qOz#lR%RzMxyR%iUJtrk9eD`{a-W`%X8m9$+q~PY#}|!Md)+Y zCZEv*KTJY$b7={b49X-Fa1CQ9F_W-Be3I(&bDL7U%XmPCZSo0K97+T3P30->iHNDZ zs`)`IA|7cUKDg2cR_rh zg~sAMc~`k^DXVyiR6=IeXMHPkK*PC&VbTU}qs%s9%u3++Ba$J!d5Ln&~>5lQEfFZQqbOLwkX_&Hs#zVPp~_BZE~!c!vxwB99J5u^~Co?xNEMHl|nzt zsspYKlG11u5YHQ0R*of{X<2RMz~iy$DhB!>Foly<<0Ox8r!w30rS00JvzU!S5p)eH z@*E_TFN4{h%{SORSv#vq9#TI7nQhMia}vt3fKHkdUcO{voD=Vy)xsE1vsYgPqnS}n zF!nhcJ60V#fc>;(%{sX9)G0ae{3f0TYhmjj>*?!d=ht(@#_}x)S#xb^ic{Lgm~&57 xz_@l+?RJ^)y4|cMJg+z}bC~k5Gy5{SllkVHtD#^?z^|F|S0;^Jny${4{sk6Hmw5mH literal 0 HcmV?d00001 From d92baff674672bc10f069d812fc6f5766cf3f22e Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Tue, 17 Mar 2026 12:01:41 -0700 Subject: [PATCH 4/8] Use AuroraDSQLPool for Node.js, HikariCP for Java --- .../node-postgres/package-lock.json | 1423 +++++++++++++++++ .../javascript/node-postgres/src/main.js | 12 +- 2 files changed, 1428 insertions(+), 7 deletions(-) create mode 100644 batch-operations/javascript/node-postgres/package-lock.json diff --git a/batch-operations/javascript/node-postgres/package-lock.json b/batch-operations/javascript/node-postgres/package-lock.json new file mode 100644 index 00000000..a30ca6e1 --- /dev/null +++ b/batch-operations/javascript/node-postgres/package-lock.json @@ -0,0 +1,1423 @@ +{ + "name": "dsql-batch-operations", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsql-batch-operations", + "version": "1.0.0", + "license": "MIT-0", + "dependencies": { + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "pg": "^8.13.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.1010.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1010.0.tgz", + "integrity": "sha512-VEh7D8MPQbb2XAc2Liq9n4t9CdTvpcx3YIWL+QL6HlD1tUnNqlITSWxyeVMr05uBJOYnA5afowyxr1pbe7V8SA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.13.tgz", + "integrity": "sha512-WZnIK8NPX+4OXkpVoNmUS+Ya1osqjszUsDqFEz97+a/LD5K012np9iR/eWEC43btx8zQjyRIK8kyiwbh8SiHzg==", + "dependencies": { + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.1010.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1010.0.tgz", + "integrity": "sha512-6TTaCoOOyFSBIDHPBPG8jLYa9xr2+hirtf5yu4ERvk9/luoRfrfE+frHVlpq7Fb7uV619/COMg5xmN39LrXvaA==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1010.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.13", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dsql-signer": { + "version": "3.1010.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/dsql-signer/-/dsql-signer-3.1010.0.tgz", + "integrity": "sha512-cYGrSpub75mEAjntsCpt/2498kQi6co9eyvziN0y868sonpb7eE2jBAg24hyrqiSsXIxk5zjMFR+y4TNRCyL/w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/util-format-url": "^3.972.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", + "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/aurora-dsql-node-postgres-connector": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@aws/aurora-dsql-node-postgres-connector/-/aurora-dsql-node-postgres-connector-0.1.8.tgz", + "integrity": "sha512-R+S7tbIAZysbv1GLitYCNKvy96pcz95VSTwKBtxHrAvlDtozaUeBfJMPw1ZMhev2jGFRNqI25+eHqJ/ASVmAvA==", + "dependencies": { + "@aws-sdk/credential-providers": "^3.894.0", + "@aws-sdk/dsql-signer": "^3.940.0", + "@smithy/types": "^4.11.0" + }, + "peerDependencies": { + "pg": "^8.16.3", + "pg-connection-string": "^2.7.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "dependencies": { + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "dependencies": { + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "dependencies": { + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "dependencies": { + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/batch-operations/javascript/node-postgres/src/main.js b/batch-operations/javascript/node-postgres/src/main.js index f21bdf9f..247e50b8 100644 --- a/batch-operations/javascript/node-postgres/src/main.js +++ b/batch-operations/javascript/node-postgres/src/main.js @@ -9,10 +9,7 @@ * [--batch-size 1000] [--num-workers 4] */ -const { Pool } = require("pg"); -const { getNodePostgresConnectionConfig } = require( - "@aws/aurora-dsql-node-postgres-connector" -); +const { AuroraDSQLPool } = require("@aws/aurora-dsql-node-postgres-connector"); const { batchDelete, parallelBatchDelete } = require("./batchDelete"); const { batchUpdate, parallelBatchUpdate } = require("./batchUpdate"); const { repopulateTestData } = require("./repopulate"); @@ -44,12 +41,13 @@ function parseArgs() { } function createPool(endpoint, user, numWorkers) { - const connectionConfig = getNodePostgresConnectionConfig({ + return new AuroraDSQLPool({ host: endpoint, user: user, - database: "postgres", + max: numWorkers, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 10000, }); - return new Pool({ ...connectionConfig, ssl: { rejectUnauthorized: true }, max: numWorkers }); } async function runOperation(label, fn) { From 4a674ae3f73f47b8d23381fd5e6b14d33c278787 Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Tue, 17 Mar 2026 17:55:39 -0700 Subject: [PATCH 5/8] Reorganized batch operations into batch_operations/ subdirectories. Added tests. --- batch-operations/README.md | 52 - batch-operations/java/pgjdbc/README.md | 99 -- .../java/com/example/dsql/Repopulate.java | 59 - .../javascript/node-postgres/README.md | 106 -- .../node-postgres/package-lock.json | 1423 ----------------- .../javascript/node-postgres/package.json | 14 - .../node-postgres/src/repopulate.js | 64 - batch-operations/python/psycopg2/README.md | 128 -- .../__pycache__/batch_delete.cpython-310.pyc | Bin 2389 -> 0 bytes .../__pycache__/batch_update.cpython-310.pyc | Bin 2539 -> 0 bytes .../src/__pycache__/occ_retry.cpython-310.pyc | Bin 2265 -> 0 bytes .../parallel_batch_delete.cpython-310.pyc | Bin 3262 -> 0 bytes .../parallel_batch_update.cpython-310.pyc | Bin 3387 -> 0 bytes .../__pycache__/repopulate.cpython-310.pyc | Bin 2237 -> 0 bytes .../python/psycopg2/src/repopulate.py | 66 - batch-operations/sql/batch_test_setup.sql | 73 - java/pgjdbc/README.md | 143 +- .../java => java}/pgjdbc/build.gradle | 15 +- .../dsql/batch_operations}/BatchDelete.java | 32 +- .../dsql/batch_operations}/BatchUpdate.java | 32 +- .../example/dsql/batch_operations}/Main.java | 20 +- .../dsql/batch_operations}/OccRetry.java | 16 +- .../dsql/batch_operations/MainTest.java | 18 + javascript/node-postgres/README.md | 130 +- javascript/node-postgres/package.json | 18 +- .../src/batch_operations}/batchDelete.js | 51 +- .../src/batch_operations}/batchUpdate.js | 51 +- .../src/batch_operations}/main.js | 22 +- .../src/batch_operations}/occRetry.js | 14 +- .../batch_operations/batch_operations.test.js | 8 + python/psycopg2/README.md | 138 +- python/psycopg2/requirements.txt | 2 +- .../src/batch_operations}/batch_delete.py | 27 +- .../src/batch_operations}/batch_update.py | 26 +- .../psycopg2/src/batch_operations}/main.py | 31 +- .../src/batch_operations}/occ_retry.py | 13 +- .../parallel_batch_delete.py | 27 +- .../parallel_batch_update.py | 27 +- .../batch_operations/test_batch_operations.py | 18 + sql/batch_operations/batch_test_setup.sql | 214 +++ 40 files changed, 744 insertions(+), 2433 deletions(-) delete mode 100644 batch-operations/README.md delete mode 100644 batch-operations/java/pgjdbc/README.md delete mode 100644 batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java delete mode 100644 batch-operations/javascript/node-postgres/README.md delete mode 100644 batch-operations/javascript/node-postgres/package-lock.json delete mode 100644 batch-operations/javascript/node-postgres/package.json delete mode 100644 batch-operations/javascript/node-postgres/src/repopulate.js delete mode 100644 batch-operations/python/psycopg2/README.md delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/parallel_batch_delete.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/parallel_batch_update.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc delete mode 100644 batch-operations/python/psycopg2/src/repopulate.py delete mode 100644 batch-operations/sql/batch_test_setup.sql rename {batch-operations/java => java}/pgjdbc/build.gradle (54%) rename {batch-operations/java/pgjdbc/src/main/java/com/example/dsql => java/pgjdbc/src/main/java/com/example/dsql/batch_operations}/BatchDelete.java (74%) rename {batch-operations/java/pgjdbc/src/main/java/com/example/dsql => java/pgjdbc/src/main/java/com/example/dsql/batch_operations}/BatchUpdate.java (76%) rename {batch-operations/java/pgjdbc/src/main/java/com/example/dsql => java/pgjdbc/src/main/java/com/example/dsql/batch_operations}/Main.java (89%) rename {batch-operations/java/pgjdbc/src/main/java/com/example/dsql => java/pgjdbc/src/main/java/com/example/dsql/batch_operations}/OccRetry.java (83%) create mode 100644 java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java rename {batch-operations/javascript/node-postgres/src => javascript/node-postgres/src/batch_operations}/batchDelete.js (67%) rename {batch-operations/javascript/node-postgres/src => javascript/node-postgres/src/batch_operations}/batchUpdate.js (69%) rename {batch-operations/javascript/node-postgres/src => javascript/node-postgres/src/batch_operations}/main.js (86%) rename {batch-operations/javascript/node-postgres/src => javascript/node-postgres/src/batch_operations}/occRetry.js (84%) create mode 100644 javascript/node-postgres/test/batch_operations/batch_operations.test.js rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/batch_delete.py (67%) rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/batch_update.py (70%) rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/main.py (82%) rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/occ_retry.py (85%) rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/parallel_batch_delete.py (73%) rename {batch-operations/python/psycopg2/src => python/psycopg2/src/batch_operations}/parallel_batch_update.py (73%) create mode 100644 python/psycopg2/test/batch_operations/test_batch_operations.py create mode 100644 sql/batch_operations/batch_test_setup.sql diff --git a/batch-operations/README.md b/batch-operations/README.md deleted file mode 100644 index 0e06fe3a..00000000 --- a/batch-operations/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Aurora DSQL Batch Operations - -Code examples demonstrating how to perform batch DELETE and UPDATE operations in -[Amazon Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) when working with -datasets exceeding the 3,000-row transaction mutation limit. - -## How this section is organized - -| Language | Client | Directory | -|----------|--------|-----------| -| Java | pgJDBC + AWS JDBC Wrapper | [java/pgjdbc](java/pgjdbc) | -| JavaScript | node-postgres | [javascript/node-postgres](javascript/node-postgres) | -| Python | psycopg2 | [python/psycopg2](python/psycopg2) | - -Shared resources: - -| Resource | Path | Description | -|----------|------|-------------| -| SQL setup | [sql/batch_test_setup.sql](sql/batch_test_setup.sql) | Creates and populates the test table | - -## Patterns - -Each language example includes two patterns: - -1. **Sequential batch processing** — A single-threaded loop that processes rows in batches of 1,000 - (configurable), committing each batch as a separate transaction. - -2. **Parallel batch processing** — Multiple worker threads partition rows using - `abs(hashtext(id::text)) % num_workers` and process their partitions concurrently. Each worker - runs its own sequential batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff -for handling serialization conflicts (SQLSTATE 40001). - -## Key concepts - -- **3,000-row transaction limit**: Aurora DSQL limits each transaction to 3,000 row mutations, - regardless of how many indexes are defined on the table. -- **Batch size**: Default 1,000 rows per transaction, providing a safe margin below the limit. -- **hashtext() partitioning**: Parallel workers use `abs(hashtext(id::text)) % num_workers` to - ensure each worker operates on a disjoint set of rows, avoiding OCC conflicts between workers. -- **Connection pooling**: All examples use DSQL connectors that automatically refresh IAM auth - tokens, important for long-running batch jobs. - -## Security - -See [CONTRIBUTING](https://github.com/aws-samples/aurora-dsql-samples/blob/main/CONTRIBUTING.md) -for more information. - -## License - -This project is licensed under the MIT-0 License. diff --git a/batch-operations/java/pgjdbc/README.md b/batch-operations/java/pgjdbc/README.md deleted file mode 100644 index a158fe20..00000000 --- a/batch-operations/java/pgjdbc/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Batch Operations with pgJDBC - -## Overview - -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[pgJDBC](https://jdbc.postgresql.org/) with the -[Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) for automatic -IAM authentication via the AWS JDBC Wrapper. - -Two patterns are provided: - -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using - `hashtext()` partitioning, with each worker running its own batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. - -## About the code example - -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. - -The parallel pattern partitions rows across worker threads using -`abs(hashtext(CAST(id AS text))) % num_workers = worker_id`, ensuring workers operate on disjoint sets -of rows and avoid OCC conflicts with each other. - -⚠️ **Important** - -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. - -## Prerequisites - -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Java Development Kit (JDK) 17 or later. -- Gradle (the wrapper is included in this project). -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. - -## Set up the test table - -Before running the examples, create and populate the test table: - -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` - -## Run the example - -Set environment variables for your cluster: - -```bash -# e.g. "admin" -export CLUSTER_USER="admin" - -# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" -``` - -Build and run: - -```bash -./gradlew run --args="--endpoint $CLUSTER_ENDPOINT --user $CLUSTER_USER" -``` - -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel worker threads | - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: - -```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" -``` - -## Additional resources - -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) -- [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) - ---- - -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java b/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java deleted file mode 100644 index 0155403f..00000000 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Repopulate.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package com.example.dsql; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import javax.sql.DataSource; - -/** - * Repopulate test data for Aurora DSQL batch operation samples. - */ -public class Repopulate { - - private static final String INSERT_SQL = - "INSERT INTO batch_test (category, status, value) " - + "SELECT " - + " (ARRAY['electronics','clothing','food','books','toys'])" - + "[floor(random() * 5 + 1)], " - + " 'active', " - + " round((random() * 1000)::numeric, 2) " - + "FROM generate_series(1, ?)"; - - public static int repopulateTestData( - DataSource pool, int rowCount, int batchSize, - int maxRetries, double baseDelay) - throws SQLException, OccRetry.MaxRetriesExceededException { - - int totalInserted = 0; - int remaining = rowCount; - - while (remaining > 0) { - int currentBatch = Math.min(batchSize, remaining); - try (Connection conn = pool.getConnection()) { - conn.setAutoCommit(false); - final int size = currentBatch; - int inserted = OccRetry.executeWithRetry(conn, (c) -> { - try (PreparedStatement pstmt = c.prepareStatement(INSERT_SQL)) { - pstmt.setInt(1, size); - return pstmt.executeUpdate(); - } - }, maxRetries, baseDelay); - - conn.commit(); - totalInserted += inserted; - remaining -= inserted; - System.out.println("Inserted " + inserted + " rows (total: " + totalInserted + ")"); - } - } - System.out.println("Repopulation complete: " + totalInserted + " rows inserted"); - return totalInserted; - } - - public static int repopulateTestData(DataSource pool) - throws SQLException, OccRetry.MaxRetriesExceededException { - return repopulateTestData(pool, 5000, 1000, 3, 0.1); - } -} diff --git a/batch-operations/javascript/node-postgres/README.md b/batch-operations/javascript/node-postgres/README.md deleted file mode 100644 index 76daf217..00000000 --- a/batch-operations/javascript/node-postgres/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# Batch Operations with node-postgres - -## Overview - -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[node-postgres](https://node-postgres.com/) with the -[Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) for automatic -IAM authentication. - -Two patterns are provided: - -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple concurrent async workers each process a disjoint partition of the dataset using - `hashtext()` partitioning, with each worker running its own batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. - -## About the code example - -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. - -The parallel pattern partitions rows across workers using -`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows -and avoid OCC conflicts with each other. - -⚠️ **Important** - -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. - -## Prerequisites - -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Node.js 18 or later. -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. - -## Set up - -Install the required packages: - -```bash -npm install -``` - -## Set up the test table - -Before running the examples, create and populate the test table: - -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` - -## Run the example - -Set environment variables for your cluster: - -```bash -# e.g. "admin" -export CLUSTER_USER="admin" - -# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" -``` - -Run the demo: - -```bash -node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" -``` - -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel async workers | - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: - -```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" -``` - -## Additional resources - -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) -- [node-postgres Documentation](https://node-postgres.com/) - ---- - -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/javascript/node-postgres/package-lock.json b/batch-operations/javascript/node-postgres/package-lock.json deleted file mode 100644 index a30ca6e1..00000000 --- a/batch-operations/javascript/node-postgres/package-lock.json +++ /dev/null @@ -1,1423 +0,0 @@ -{ - "name": "dsql-batch-operations", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "dsql-batch-operations", - "version": "1.0.0", - "license": "MIT-0", - "dependencies": { - "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", - "pg": "^8.13.0" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1010.0.tgz", - "integrity": "sha512-VEh7D8MPQbb2XAc2Liq9n4t9CdTvpcx3YIWL+QL6HlD1tUnNqlITSWxyeVMr05uBJOYnA5afowyxr1pbe7V8SA==", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.13.tgz", - "integrity": "sha512-WZnIK8NPX+4OXkpVoNmUS+Ya1osqjszUsDqFEz97+a/LD5K012np9iR/eWEC43btx8zQjyRIK8kyiwbh8SiHzg==", - "dependencies": { - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", - "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", - "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", - "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", - "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", - "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", - "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/token-providers": "3.1009.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", - "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-providers": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1010.0.tgz", - "integrity": "sha512-6TTaCoOOyFSBIDHPBPG8jLYa9xr2+hirtf5yu4ERvk9/luoRfrfE+frHVlpq7Fb7uV619/COMg5xmN39LrXvaA==", - "dependencies": { - "@aws-sdk/client-cognito-identity": "3.1010.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.13", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/dsql-signer": { - "version": "3.1010.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/dsql-signer/-/dsql-signer-3.1010.0.tgz", - "integrity": "sha512-cYGrSpub75mEAjntsCpt/2498kQi6co9eyvziN0y868sonpb7eE2jBAg24hyrqiSsXIxk5zjMFR+y4TNRCyL/w==", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/util-format-url": "^3.972.8", - "@smithy/config-resolver": "^4.4.11", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", - "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", - "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", - "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", - "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", - "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", - "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", - "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.4.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/aurora-dsql-node-postgres-connector": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@aws/aurora-dsql-node-postgres-connector/-/aurora-dsql-node-postgres-connector-0.1.8.tgz", - "integrity": "sha512-R+S7tbIAZysbv1GLitYCNKvy96pcz95VSTwKBtxHrAvlDtozaUeBfJMPw1ZMhev2jGFRNqI25+eHqJ/ASVmAvA==", - "dependencies": { - "@aws-sdk/credential-providers": "^3.894.0", - "@aws-sdk/dsql-signer": "^3.940.0", - "@smithy/types": "^4.11.0" - }, - "peerDependencies": { - "pg": "^8.16.3", - "pg-connection-string": "^2.7.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", - "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.23.12", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", - "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", - "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-serde": "^4.2.15", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.43", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", - "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", - "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", - "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", - "dependencies": { - "@smithy/abort-controller": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", - "dependencies": { - "@smithy/types": "^4.13.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", - "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.26", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", - "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", - "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", - "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", - "dependencies": { - "@smithy/config-resolver": "^4.4.11", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", - "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", - "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.20", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", - "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" - }, - "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", - "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.3.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", - "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ] - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - } - } -} diff --git a/batch-operations/javascript/node-postgres/package.json b/batch-operations/javascript/node-postgres/package.json deleted file mode 100644 index 20ebf375..00000000 --- a/batch-operations/javascript/node-postgres/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "dsql-batch-operations", - "version": "1.0.0", - "description": "Batch DELETE and UPDATE operations for Aurora DSQL", - "main": "src/main.js", - "scripts": { - "start": "node src/main.js" - }, - "dependencies": { - "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", - "pg": "^8.13.0" - }, - "license": "MIT-0" -} diff --git a/batch-operations/javascript/node-postgres/src/repopulate.js b/batch-operations/javascript/node-postgres/src/repopulate.js deleted file mode 100644 index bf851ace..00000000 --- a/batch-operations/javascript/node-postgres/src/repopulate.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -/** - * Repopulate test data for Aurora DSQL batch operation samples. - */ - -const { executeWithRetry } = require("./occRetry"); - -const INSERT_SQL = - "INSERT INTO batch_test (category, status, value) " + - "SELECT " + - " (ARRAY['electronics','clothing','food','books','toys'])" + - "[floor(random() * 5 + 1)], " + - " 'active', " + - " round((random() * 1000)::numeric, 2) " + - "FROM generate_series(1, $1)"; - -/** - * Insert test rows into batch_test in batches. - * - * @param {import('pg').Pool} pool - * @param {number} [rowCount=5000] - * @param {number} [batchSize=1000] - * @param {number} [maxRetries=3] - * @param {number} [baseDelay=0.1] - * @returns {Promise} Total rows inserted. - */ -async function repopulateTestData(pool, rowCount = 5000, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { - let totalInserted = 0; - let remaining = rowCount; - - while (remaining > 0) { - const currentBatch = Math.min(batchSize, remaining); - const client = await pool.connect(); - try { - const inserted = await executeWithRetry( - client, - async (c) => { - await c.query("BEGIN"); - const result = await c.query(INSERT_SQL, [currentBatch]); - return result.rowCount; - }, - maxRetries, - baseDelay - ); - - await client.query("COMMIT"); - totalInserted += inserted; - remaining -= inserted; - console.log(`Inserted ${inserted} rows (total: ${totalInserted})`); - } catch (err) { - await client.query("ROLLBACK"); - throw err; - } finally { - client.release(); - } - } - - console.log(`Repopulation complete: ${totalInserted} rows inserted`); - return totalInserted; -} - -module.exports = { repopulateTestData }; diff --git a/batch-operations/python/psycopg2/README.md b/batch-operations/python/psycopg2/README.md deleted file mode 100644 index e53d61dd..00000000 --- a/batch-operations/python/psycopg2/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# Batch Operations with psycopg2 - -## Overview - -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[psycopg2](https://www.psycopg.org/docs/) with the -[Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic -IAM authentication and connection pooling. - -Two patterns are provided: - -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using - `hashtext()` partitioning, with each worker running its own batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. - -## About the code example - -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. - -The parallel pattern partitions rows across worker threads using -`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows -and avoid OCC conflicts with each other. - -Connection pooling uses `AuroraDSQLThreadedConnectionPool` from the Aurora DSQL Python Connector, which -automatically handles IAM token refresh for long-running batch jobs. - -⚠️ **Important** - -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. - -## Prerequisites - -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Python 3.8 or later. -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. - -## Set up - -Create and activate a Python virtual environment: - -```bash -python3 -m venv .venv -source .venv/bin/activate -``` - -Install the required packages: - -```bash -pip install -r requirements.txt -``` - -## Set up the test table - -Before running the examples, create and populate the test table. Run each INSERT as a separate -transaction (each block of 1,000 rows): - -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` - -## Run the example - -Set environment variables for your cluster: - -```bash -# e.g. "admin" -export CLUSTER_USER="admin" - -# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" -``` - -Run the demo: - -```bash -python src/main.py --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" -``` - -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel worker threads | - -The demo runs the following sequence: - -1. Sequential batch DELETE (all `electronics` rows) -2. Repopulate test data -3. Sequential batch UPDATE (`clothing` → `processed`) -4. Repopulate test data -5. Parallel batch DELETE (all `food` rows) with N workers -6. Repopulate test data -7. Parallel batch UPDATE (`books` → `archived`) with N workers - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: - -```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" -``` - -## Additional resources - -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) -- [Psycopg2 Documentation](https://www.psycopg.org/docs/) -- [Batch operations in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/batch-operations.html) - ---- - -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/batch_delete.cpython-310.pyc deleted file mode 100644 index 5c5f06f2bf1407e81318f0ea4f6351df89fb2ac6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2389 zcmaJ?UvC>l5Z}E!+h^AaZ9%BwkFRt8m0~uUwsjtvC*~Hb$HVo0a@Hs=$Tf<`S9{ zEoq`?V7+NUl%_+n9cnm<=uJ*8otvARI}aBiajL1+ib8POsC=#9X15j=h~!!hNhCJH zz*-W%GZ4un+#U_KM89u?Q(Qs@!cp$V+&c;btsu}2-QzTUoUZvZmJyqMJYE$zaT?<*OWhV7H`G_QG+{ukpw@Z`&Q>tx34t$9L zH@Q#*GD<5fRJTP47f@+Waea=^yCP&Ji6+{^Ym*+=+ngs8ClJOnfCXBv|h%UtH&_ji4b$EC49_QnNP_}<$sk4t;bV}#MTI4*yJc8l6&F#8#?(zpoHe^j`K zVYkfimLor1LuT8{KgM<5wMo zadw^Ux+!FfeT!7y(9@ZG|(=^iL zf??m3x0vo&IE7rb$W6Kv#%VmoL8>*62U-#1Fem$=t4mXEdQ|Za#7u`R@;S^+{6Kqn zHN{QT%F^{^G(L1U*SG)eVNo=sbwtMP}CPz z38>~iKYXor?sxQhr5o38USB1ncV6kKUU#!%kV++FR+`kH%m=UqB29GN$vk-7By&OK ziJRr^z+y8iAA&tASXMki&vg?@yi`r-Rj{jc(-hG`L<8QOUu)0ru->8wR8z^|*mqVaCrFNt#$cFL^J&Mf*UZbG0a3vo_Oau5 z`;P1V=Xk|`y%(N;_V3XL$LudhBA}7F(wKGvai5qXj*0sK{v&_UGM|yrY);CWHsEcM z=eWLHFJ+|-u1(IQ!6M|=Hq@*zkkB8p(jc{)S$SzE-~-bfO}3-RG`)Hy^Gw;2vtTJ_ zj4)W1r;RX(UZ1f?86HBDXVO%EdCF{*4wl|9ODBvt2_mab$NCnms0Ijc)^U7D&q>Fr z`ndSQcYXXe_8kvb3ZCQrQ-xgzJNwT5nWFPlz6U0CSDHQ}JtGQcp^%k29VUWKCoAWj q75d$JSp-4ezHI!~R@zG)341gDax7RH@hgVD$_L@O{)wtH>;4a`eyfuJ diff --git a/batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/batch_update.cpython-310.pyc deleted file mode 100644 index f02b63f8a40e7bbb252064de1e8e8d477caacfcb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2539 zcmai0QEwYX5Z=8z`_7INpj4>h0j&yBb5d%j6bUJ63n2}qQk#}KQAC7fZ*JGgHRrqQ z?jDIP=Y>{EUJ%cKDy2`T5US-X98adnw=5(w<0k%6IX6OJQ#B&AAm0lUBmk9eSpu;WCL z9#0hf|9W29bv3du!2&QI|@9%PEQ zbcij%mFPYF14{4|9`yTCxNTghhqxV0Rp5Gv|S~Ur~7A4Jw1upv+3&Is4UtcVs<67!BM(^<%W}Dw~xD)t?Y^2QEnO zVd*}G)hfd~mRMLv>)O6E@E++|w+Y{I%|WWvyN1CE%y~y;ZSPyOJE0qw$NP5CJsTj? zg}@6S6bXgGm^%JPfrof{Wq|=w1jfsidws>m1#7p5MKmQNrey85F$qxA2pa}4D&y{U z2$KfB+w>biCg<_Ew!?51#g<$!R&;P_l0*w+k+j=P-izu*L+E~3-4>iO&X$rm26O@X zZ-Z*P4Lty$jRRgE*-*L{7RU+f4-X=SD7d=)AsFhq%u|I4YCmGC4Pb@wv0@_SomBe+!rE zUo78QCPff0`!j1NJVF>$O?VBNCX-Hh%o8!^On3at2Q{E+X@Y>vWZg|JbZ$&-Mrk1_ z%${U~S;jjwixjz_dB7hZp-cBn`+?g9a+B_b-Kc%0zquOsk0k zmW?M*I|MPIfk-4!STy8eE{o#{;ApfOXn8y=c$tCcZFuC@APV;Qp6ufRKEezi+Q0(V zp#{=5@6I8H_tGH(S$;TWcmUtE){zC5gad!~ZH$`e0+Q!;alr{+0Ut1!^WBxcJG>M6 zB?HKQXKsQni{6`nfd=65C;$~Qa(#u&zEVqTHygKgN zykwgDF?yyNKy5-c1dTvbss=RaP8!jGHx}2Mi+gPIY7)qX6v4!Iq%T?3$hH~=spk^_ zt-0!CBke)+$Bk6>f+XGg&}=OlY1m$AFRvA^Ei>sVJ+Q|cJ+p-SAIc-@U<}iDu`uy?m%0hdB7Q}8xGaNk?3y=Y{|m!x)H478 diff --git a/batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/occ_retry.cpython-310.pyc deleted file mode 100644 index 3928283d93c429ff4693ea59033cf44c6d95e656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2265 zcmZ`)TW=dh6rR~#+v{zUhSCb5N@xT`swJ>fE-wKAp$$+}Qjr8zSVEie&aAWPde@zq zC9&m7h*aty-~|v;^4Na>e`O!x0p$nqfk$Uqel2S-9{A`;iS_*c!FEhrURt<3RoMvWUvK~K5dPR?AMXw_j z%VRdR5Ck?OX8{_nE6@?4egy_Yj>!plbwqzA!uiELrId{5h=h(HfX&?+z6Ri|yFl1P ztrY7l=1B_hcx*uB(4U@YxMf8b;GxnF!z4>g7?$S%V2BO}2@R+8v`k}#C?3(W9)?lM zwGP8a=-kL7-O?(0;ydL=!r`LZvH^N? z9-vl#VD|H@)z^b4?|0v_SMw;cXa>#xKw+X?L}x%2k}3_T_g~-+uRI%&=G2@<6-~={ zPl9Pd81`~eq_{pGhW84dPX4Kdp~#~!v>M8#iJ6g1Q`oD6vhh&E71n~n&|HA7t1$S) zqn8}JcZRY0^QRxN{q|vmfTs;l;@#`e^#}$-Mv!MOkz;y;eDm&oZ&<@v9@9I-Ar#{2 z5kn8019$Kqg>Q&7Iq^<$_D0U#ihA8tM$R$)o{n7MjJy}gC33%FswXw!jy#k*az>R? zj}qgb)L{j=@-?|vJ%n0=T&WZ{0qW+Fj0z(eXM!8v;abiMW?X5QoDkkPO(HX?L!OD} z6<{+67_{vo(`;{V7S^k9JChNXU}JBO;pItou{>jtK~TfV(3Yr8(?UsY1zm+A-r3#Q z-DK~qt*u>autFzU_qm$cCWlfVvxLgiR9jNOa0rJV8AB(mZt@zWBmWgG!=r}yCCCPI#_2nW9zF~fNZMi zXHy&kgdF)q4EFXG{01rfq?^XBIipa}XUGb})y0x|V{&>`v{tA5yV|@K>#asmRv;g{ zvh=5^RaT{f_e+iqgl_#@Eg=O<9HaunW{(JLD>Z94@Q$hYW!TrR!mx7Lq04lI`u2C8 zdeos+=nnP%S*|*NdH*Ur)^OAcOzH((pHs^)l{G{UsMQtx=Q0fK3sXNdCvK>BrMYE| vROj#%>MiT|==PCLtQ)L?1M4x%%=7%7 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 diff --git a/batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc b/batch-operations/python/psycopg2/src/__pycache__/repopulate.cpython-310.pyc deleted file mode 100644 index 3b284f9348b41c3c43580e52546fff7d448a6906..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2237 zcmZ`)&u<$=6rNeH?X~lRQjqu|f#v|jTT(k|Re?-ZYLXyDDrrelMNL&XW**Lt54!4vjQAi+ZINw&Z^9Lgttqqwt?KK(Vy!)7I6|# z&57np6Gk;9eIZFFkwQ|kyz%)B(xckjB_igMYF|V|(J&6UYS!zkk>XM-BE`NUexwD_ zyS(u4=J_=31srCIcU;XA$yLlf?MH*$_wxM0!oo#}uWLyoMPUY32>j63O|nFTfHTtL zdY|)%EU(;H*<2wsV&wL%-L4K#+^G^ZbB& ziRRsXU+;D$*K&CL^(zP+{{h$?8~lF%{yQ^%{@5#i|eKeX&t9@XAl^)@7Opm!{U3Kk2vPMOSEFIYU~4>%_^ zCqkf015;EHN)(SNFar3WVHBuQnhuEp9nt|kd7=sZBusRnu7dkwosJx+cJ63ajfDu> zq(gRgnEE1U77{fI-Q1O&GR~Gn6d7%a=oYx{?11oK(a7V?Ne=kFt|yX6x5=i^G$2tD z_P8XXUx*m?2|*xg(qOz#lR%RzMxyR%iUJtrk9eD`{a-W`%X8m9$+q~PY#}|!Md)+Y zCZEv*KTJY$b7={b49X-Fa1CQ9F_W-Be3I(&bDL7U%XmPCZSo0K97+T3P30->iHNDZ zs`)`IA|7cUKDg2cR_rh zg~sAMc~`k^DXVyiR6=IeXMHPkK*PC&VbTU}qs%s9%u3++Ba$J!d5Ln&~>5lQEfFZQqbOLwkX_&Hs#zVPp~_BZE~!c!vxwB99J5u^~Co?xNEMHl|nzt zsspYKlG11u5YHQ0R*of{X<2RMz~iy$DhB!>Foly<<0Ox8r!w30rS00JvzU!S5p)eH z@*E_TFN4{h%{SORSv#vq9#TI7nQhMia}vt3fKHkdUcO{voD=Vy)xsE1vsYgPqnS}n zF!nhcJ60V#fc>;(%{sX9)G0ae{3f0TYhmjj>*?!d=ht(@#_}x)S#xb^ic{Lgm~&57 xz_@l+?RJ^)y4|cMJg+z}bC~k5Gy5{SllkVHtD#^?z^|F|S0;^Jny${4{sk6Hmw5mH diff --git a/batch-operations/python/psycopg2/src/repopulate.py b/batch-operations/python/psycopg2/src/repopulate.py deleted file mode 100644 index 4297f419..00000000 --- a/batch-operations/python/psycopg2/src/repopulate.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: MIT-0 - -"""Repopulate test data for Aurora DSQL batch operation samples. - -Inserts rows into the batch_test table in batches, respecting the 3,000-row -transaction limit. Called between DELETE and UPDATE demonstrations to restore -the test dataset. -""" - -from occ_retry import execute_with_retry - - -def repopulate_test_data(pool, row_count=5000, batch_size=1000, max_retries=3, base_delay=0.1): - """Insert test rows into batch_test in batches. - - Each batch is committed as a separate transaction to stay within the - 3,000-row mutation limit. - - Args: - pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. - row_count: Total number of rows to insert (default 5,000). - batch_size: Number of rows to insert per transaction (default 1,000). - max_retries: Maximum OCC retry attempts per batch (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). - - Returns: - Total number of rows inserted across all batches. - """ - total_inserted = 0 - remaining = row_count - - while remaining > 0: - current_batch = min(batch_size, remaining) - conn = pool.getconn() - try: - - def insert_batch(conn, size=current_batch): - with conn.cursor() as cur: - cur.execute( - "INSERT INTO batch_test (category, status, value) " - "SELECT " - " (ARRAY['electronics','clothing','food','books','toys'])" - "[floor(random() * 5 + 1)], " - " 'active', " - " round((random() * 1000)::numeric, 2) " - "FROM generate_series(1, %s)", - (size,), - ) - return cur.rowcount - - inserted = execute_with_retry( - conn, insert_batch, max_retries=max_retries, base_delay=base_delay - ) - conn.commit() - total_inserted += inserted - remaining -= inserted - print(f"Inserted {inserted} rows (total: {total_inserted})") - except Exception: - conn.rollback() - raise - finally: - pool.putconn(conn) - - print(f"Repopulation complete: {total_inserted} rows inserted") - return total_inserted diff --git a/batch-operations/sql/batch_test_setup.sql b/batch-operations/sql/batch_test_setup.sql deleted file mode 100644 index d6428ee0..00000000 --- a/batch-operations/sql/batch_test_setup.sql +++ /dev/null @@ -1,73 +0,0 @@ --- ============================================================================= --- Sample table setup for Aurora DSQL batch operations --- --- This script creates a test table and populates it with 5,000 rows of sample --- data. Use this table to test the batch DELETE and UPDATE code samples. --- --- Aurora DSQL limits each transaction to 3,000 row mutations, so we insert --- in batches of 1,000 rows. Run each INSERT as a separate transaction. --- ============================================================================= - --- Drop the table if it already exists -DROP TABLE IF EXISTS batch_test; - --- Create the sample table. --- Uses UUID primary key with gen_random_uuid() to minimize OCC contention, --- following Aurora DSQL best practice of random keys. -CREATE TABLE batch_test ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - category VARCHAR(50) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'active', - value NUMERIC(10,2), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Create an asynchronous index on the category column. --- Aurora DSQL requires CREATE INDEX ASYNC for tables with existing rows. -CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); - --- ============================================================================= --- Populate the table with 5,000 rows of test data. --- Each INSERT is 1,000 rows — run each as a separate transaction. --- ============================================================================= - --- Batch 1: rows 1–1,000 -INSERT INTO batch_test (category, status, value) -SELECT - (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], - 'active', - round((random() * 1000)::numeric, 2) -FROM generate_series(1, 1000); - --- Batch 2: rows 1,001–2,000 -INSERT INTO batch_test (category, status, value) -SELECT - (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], - 'active', - round((random() * 1000)::numeric, 2) -FROM generate_series(1, 1000); - --- Batch 3: rows 2,001–3,000 -INSERT INTO batch_test (category, status, value) -SELECT - (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], - 'active', - round((random() * 1000)::numeric, 2) -FROM generate_series(1, 1000); - --- Batch 4: rows 3,001–4,000 -INSERT INTO batch_test (category, status, value) -SELECT - (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], - 'active', - round((random() * 1000)::numeric, 2) -FROM generate_series(1, 1000); - --- Batch 5: rows 4,001–5,000 -INSERT INTO batch_test (category, status, value) -SELECT - (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], - 'active', - round((random() * 1000)::numeric, 2) -FROM generate_series(1, 1000); diff --git a/java/pgjdbc/README.md b/java/pgjdbc/README.md index ad730e69..a158fe20 100644 --- a/java/pgjdbc/README.md +++ b/java/pgjdbc/README.md @@ -1,120 +1,99 @@ -# Aurora DSQL with pgJDBC +# Batch Operations with pgJDBC ## Overview -This code example demonstrates how to use `pgJDBC` with Amazon Aurora DSQL. -The example shows you how to connect to an Aurora DSQL cluster and perform basic database operations. +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[pgJDBC](https://jdbc.postgresql.org/) with the +[Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) for automatic +IAM authentication via the AWS JDBC Wrapper. -Aurora DSQL is a distributed SQL database service that provides high availability and scalability for -your PostgreSQL-compatible applications. `pgJDBC` is a popular PostgreSQL adapter for Java that allows -you to interact with PostgreSQL databases using Java code. +Two patterns are provided: -This example uses the Aurora DSQL JDBC Connector to handle IAM authentication automatically. +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. -## About the code example - -The example demonstrates a flexible connection approach that works for both admin and non-admin users: - -* When connecting as an **admin user**, the example uses the `public` schema. -* When connecting as a **non-admin user**, the example uses a custom `myschema` schema. - -The code automatically detects the user type and adjusts its behavior accordingly. - -## ⚠️ Important - -* Running this code might result in charges to your AWS account. -* We recommend that you grant your code least privilege. At most, grant only the - minimum permissions required to perform the task. For more information, see - [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -* This code is not tested in every AWS Region. For more information, see - [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. -## TLS connection configuration - -This example uses direct TLS connections where supported, and verifies the server certificate is trusted. Verified SSL -connections should be used where possible to ensure data security during transmission. - -* Driver versions following the release of PostgreSQL 17 support direct TLS connections, bypassing the traditional - PostgreSQL connection preamble -* Direct TLS connections provide improved connection performance and enhanced security -* Not all PostgreSQL drivers support direct TLS connections yet, or only in recent versions following PostgreSQL 17 -* Ensure your installed driver version supports direct TLS negotiation, or use a version that is at least as recent as - the one used in this sample -* If your driver doesn't support direct TLS connections, you may need to use the traditional preamble connection instead - -## Run the example +## About the code example -### Prerequisites +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. -* You must have an AWS account, and have your default credentials and AWS Region - configured as described in the - [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) - guide. -* Java Development Kit (JDK): Ensure you have JDK 17+ installed. +The parallel pattern partitions rows across worker threads using +`abs(hashtext(CAST(id AS text))) % num_workers = worker_id`, ensuring workers operate on disjoint sets +of rows and avoid OCC conflicts with each other. - _To verify the java is installed, you can run_ - ```bash - java -version +⚠️ **Important** -* Gradle: This example uses the Gradle wrapper included in the repository, so no separate installation is required. -* AWS SDK: Ensure that you setup the latest version of the AWS Java SDK [official website](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/setup.html) -* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) - guide. -* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` - schema. See the - [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) - guide. +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. -### Run the code +## Prerequisites -The example demonstrates the following operations: +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Java Development Kit (JDK) 17 or later. +- Gradle (the wrapper is included in this project). +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. -- Opening a connection to an Aurora DSQL cluster -- Creating a table -- Inserting and querying data +## Set up the test table -The example is designed to work with both admin and non-admin users: +Before running the examples, create and populate the test table: -- When run as an admin user, it uses the `public` schema -- When run as a non-admin user, it uses the `myschema` schema +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` -**Note:** running the example will use actual resources in your AWS account and may incur charges. +## Run the example -Set environment variables for your cluster details: +Set environment variables for your cluster: ```bash # e.g. "admin" -export CLUSTER_USER="" - +export CLUSTER_USER="admin" + # e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" +export CLUSTER_ENDPOINT="" ``` -Run the example: +Build and run: ```bash -./gradlew run +./gradlew run --args="--endpoint $CLUSTER_ENDPOINT --user $CLUSTER_USER" ``` -Run the tests: +### Command-line options -```bash -./gradlew test -``` +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | +## Clean up -The example contains comments explaining the code and the operations being performed. +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` ## Additional resources -* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) -* [Amazon Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/java/jdbc) -* [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) -* [AWS SDK for Java Documentation](https://docs.aws.amazon.com/sdk-for-java/) +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) +- [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 +SPDX-License-Identifier: MIT-0 diff --git a/batch-operations/java/pgjdbc/build.gradle b/java/pgjdbc/build.gradle similarity index 54% rename from batch-operations/java/pgjdbc/build.gradle rename to java/pgjdbc/build.gradle index 86dabf6e..11e2d424 100644 --- a/batch-operations/java/pgjdbc/build.gradle +++ b/java/pgjdbc/build.gradle @@ -20,8 +20,21 @@ dependencies { implementation 'com.zaxxer:HikariCP:7.0.2' implementation 'software.amazon.dsql:aurora-dsql-jdbc-connector:1.4.0' implementation 'software.amazon.awssdk:dsql:2.42.14' + + testImplementation platform('org.junit:junit-bom:5.11.4') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } application { - mainClass = 'com.example.dsql.Main' + mainClass = 'com.example.dsql.batch_operations.Main' +} + +tasks.named('test') { + useJUnitPlatform() + + testLogging { + events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' + exceptionFormat = 'FULL' + } } diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchDelete.java similarity index 74% rename from batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java rename to java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchDelete.java index 4d41b72b..d2b2cef4 100644 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchDelete.java +++ b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchDelete.java @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -package com.example.dsql; +package com.example.dsql.batch_operations; import java.sql.Connection; import java.sql.SQLException; @@ -19,12 +19,16 @@ */ public class BatchDelete { + /** Stop retrying a batch after this many consecutive OCC exhaustions. */ + private static final int MAX_CONSECUTIVE_FAILURES = 10; + public static int batchDelete( DataSource pool, String table, String condition, - int batchSize, int maxRetries, double baseDelay) + int batchSize, int maxRetries, long baseDelayMs) throws SQLException, OccRetry.MaxRetriesExceededException { int totalDeleted = 0; + int consecutiveFailures = 0; while (true) { try (Connection conn = pool.getConnection()) { conn.setAutoCommit(false); @@ -35,12 +39,18 @@ public static int batchDelete( try (Statement stmt = c.createStatement()) { return stmt.executeUpdate(sql); } - }, maxRetries, baseDelay); + }, maxRetries, baseDelayMs); conn.commit(); totalDeleted += deleted; + consecutiveFailures = 0; System.out.println("Deleted " + deleted + " rows (total: " + totalDeleted + ")"); if (deleted == 0) break; + } catch (OccRetry.MaxRetriesExceededException e) { + consecutiveFailures++; + System.out.println("Batch OCC retries exhausted (" + consecutiveFailures + + "/" + MAX_CONSECUTIVE_FAILURES + "), retrying batch with fresh connection"); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } return totalDeleted; @@ -48,12 +58,12 @@ public static int batchDelete( public static int batchDelete(DataSource pool, String table, String condition) throws SQLException, OccRetry.MaxRetriesExceededException { - return batchDelete(pool, table, condition, 1000, 3, 0.1); + return batchDelete(pool, table, condition, 1000, 3, 100); } public static int parallelBatchDelete( DataSource pool, String table, String condition, - int numWorkers, int batchSize, int maxRetries, double baseDelay) + int numWorkers, int batchSize, int maxRetries, long baseDelayMs) throws SQLException, OccRetry.MaxRetriesExceededException { ExecutorService executor = Executors.newFixedThreadPool(numWorkers); @@ -63,6 +73,7 @@ public static int parallelBatchDelete( final int workerId = i; futures.add(executor.submit(() -> { int totalDeleted = 0; + int consecutiveFailures = 0; String partitionCondition = condition + " AND abs(hashtext(CAST(id AS text))) % " + numWorkers + " = " + workerId; @@ -76,12 +87,19 @@ public static int parallelBatchDelete( try (Statement stmt = c.createStatement()) { return stmt.executeUpdate(sql); } - }, maxRetries, baseDelay); + }, maxRetries, baseDelayMs); conn.commit(); totalDeleted += deleted; + consecutiveFailures = 0; System.out.println("Worker " + workerId + ": Deleted " + deleted + " rows (total: " + totalDeleted + ")"); if (deleted == 0) break; + } catch (OccRetry.MaxRetriesExceededException e) { + consecutiveFailures++; + System.out.println("Worker " + workerId + ": Batch OCC retries exhausted (" + + consecutiveFailures + "/" + MAX_CONSECUTIVE_FAILURES + + "), retrying batch with fresh connection"); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } return totalDeleted; @@ -110,6 +128,6 @@ public static int parallelBatchDelete( public static int parallelBatchDelete(DataSource pool, String table, String condition) throws SQLException, OccRetry.MaxRetriesExceededException { - return parallelBatchDelete(pool, table, condition, 4, 1000, 3, 0.1); + return parallelBatchDelete(pool, table, condition, 4, 1000, 3, 100); } } diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java similarity index 76% rename from batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java rename to java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java index c1d44ea5..df906e26 100644 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/BatchUpdate.java +++ b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -package com.example.dsql; +package com.example.dsql.batch_operations; import java.sql.Connection; import java.sql.SQLException; @@ -19,12 +19,16 @@ */ public class BatchUpdate { + /** Stop retrying a batch after this many consecutive OCC exhaustions. */ + private static final int MAX_CONSECUTIVE_FAILURES = 10; + public static int batchUpdate( DataSource pool, String table, String setClause, String condition, - int batchSize, int maxRetries, double baseDelay) + int batchSize, int maxRetries, long baseDelayMs) throws SQLException, OccRetry.MaxRetriesExceededException { int totalUpdated = 0; + int consecutiveFailures = 0; while (true) { try (Connection conn = pool.getConnection()) { conn.setAutoCommit(false); @@ -36,12 +40,18 @@ public static int batchUpdate( try (Statement stmt = c.createStatement()) { return stmt.executeUpdate(sql); } - }, maxRetries, baseDelay); + }, maxRetries, baseDelayMs); conn.commit(); totalUpdated += updated; + consecutiveFailures = 0; System.out.println("Updated " + updated + " rows (total: " + totalUpdated + ")"); if (updated == 0) break; + } catch (OccRetry.MaxRetriesExceededException e) { + consecutiveFailures++; + System.out.println("Batch OCC retries exhausted (" + consecutiveFailures + + "/" + MAX_CONSECUTIVE_FAILURES + "), retrying batch with fresh connection"); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } return totalUpdated; @@ -49,12 +59,12 @@ public static int batchUpdate( public static int batchUpdate(DataSource pool, String table, String setClause, String condition) throws SQLException, OccRetry.MaxRetriesExceededException { - return batchUpdate(pool, table, setClause, condition, 1000, 3, 0.1); + return batchUpdate(pool, table, setClause, condition, 1000, 3, 100); } public static int parallelBatchUpdate( DataSource pool, String table, String setClause, String condition, - int numWorkers, int batchSize, int maxRetries, double baseDelay) + int numWorkers, int batchSize, int maxRetries, long baseDelayMs) throws SQLException, OccRetry.MaxRetriesExceededException { ExecutorService executor = Executors.newFixedThreadPool(numWorkers); @@ -64,6 +74,7 @@ public static int parallelBatchUpdate( final int workerId = i; futures.add(executor.submit(() -> { int totalUpdated = 0; + int consecutiveFailures = 0; String partitionCondition = condition + " AND abs(hashtext(CAST(id AS text))) % " + numWorkers + " = " + workerId; @@ -78,12 +89,19 @@ public static int parallelBatchUpdate( try (Statement stmt = c.createStatement()) { return stmt.executeUpdate(sql); } - }, maxRetries, baseDelay); + }, maxRetries, baseDelayMs); conn.commit(); totalUpdated += updated; + consecutiveFailures = 0; System.out.println("Worker " + workerId + ": Updated " + updated + " rows (total: " + totalUpdated + ")"); if (updated == 0) break; + } catch (OccRetry.MaxRetriesExceededException e) { + consecutiveFailures++; + System.out.println("Worker " + workerId + ": Batch OCC retries exhausted (" + + consecutiveFailures + "/" + MAX_CONSECUTIVE_FAILURES + + "), retrying batch with fresh connection"); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } return totalUpdated; @@ -112,6 +130,6 @@ public static int parallelBatchUpdate( public static int parallelBatchUpdate(DataSource pool, String table, String setClause, String condition) throws SQLException, OccRetry.MaxRetriesExceededException { - return parallelBatchUpdate(pool, table, setClause, condition, 4, 1000, 3, 0.1); + return parallelBatchUpdate(pool, table, setClause, condition, 4, 1000, 3, 100); } } diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/Main.java similarity index 89% rename from batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java rename to java/pgjdbc/src/main/java/com/example/dsql/batch_operations/Main.java index 7ac5c8f6..a5f8eb23 100644 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/Main.java +++ b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/Main.java @@ -1,8 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -package com.example.dsql; - +package com.example.dsql.batch_operations; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; @@ -102,27 +101,18 @@ public static void main(String[] args) { try { runOperation("Sequential Batch DELETE (category = 'electronics')", - () -> BatchDelete.batchDelete(pool, table, "category = 'electronics'", batchSize, 3, 0.1)); - - runOperation("Repopulate test data", - () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + () -> BatchDelete.batchDelete(pool, table, "category = 'electronics'", batchSize, 3, 100)); runOperation("Sequential Batch UPDATE (clothing -> processed)", () -> BatchUpdate.batchUpdate(pool, table, "status = 'processed'", - "category = 'clothing' AND status != 'processed'", batchSize, 3, 0.1)); - - runOperation("Repopulate test data", - () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + "category = 'clothing' AND status != 'processed'", batchSize, 3, 100)); runOperation("Parallel Batch DELETE (category = 'food') [" + numWorkers + " workers]", - () -> BatchDelete.parallelBatchDelete(pool, table, "category = 'food'", numWorkers, batchSize, 3, 0.1)); - - runOperation("Repopulate test data", - () -> Repopulate.repopulateTestData(pool, 5000, batchSize, 3, 0.1)); + () -> BatchDelete.parallelBatchDelete(pool, table, "category = 'food'", numWorkers, batchSize, 3, 100)); runOperation("Parallel Batch UPDATE (books -> archived) [" + numWorkers + " workers]", () -> BatchUpdate.parallelBatchUpdate(pool, table, "status = 'archived'", - "category = 'books' AND status != 'archived'", numWorkers, batchSize, 3, 0.1)); + "category = 'books' AND status != 'archived'", numWorkers, batchSize, 3, 100)); } catch (SQLException e) { System.err.println("Database error: " + e.getMessage()); diff --git a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/OccRetry.java similarity index 83% rename from batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java rename to java/pgjdbc/src/main/java/com/example/dsql/batch_operations/OccRetry.java index 503eb02f..efffa9a1 100644 --- a/batch-operations/java/pgjdbc/src/main/java/com/example/dsql/OccRetry.java +++ b/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/OccRetry.java @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -package com.example.dsql; +package com.example.dsql.batch_operations; import java.sql.Connection; import java.sql.SQLException; @@ -39,14 +39,14 @@ public int getMaxRetries() { * @param connection a JDBC connection (autoCommit should be false) * @param operation the database operation to execute * @param maxRetries maximum retry attempts (default 3) - * @param baseDelay base delay in seconds for backoff (default 0.1) + * @param baseDelayMs base delay in milliseconds for backoff (default 100) * @return the return value of the operation */ public static T executeWithRetry( Connection connection, DatabaseOperation operation, int maxRetries, - double baseDelay) throws MaxRetriesExceededException, SQLException { + long baseDelayMs) throws MaxRetriesExceededException, SQLException { for (int attempt = 0; attempt <= maxRetries; attempt++) { try { @@ -57,12 +57,12 @@ public static T executeWithRetry( if (attempt >= maxRetries) { throw new MaxRetriesExceededException(maxRetries); } - double delay = baseDelay * Math.pow(2, attempt); + long delayMs = baseDelayMs * (1L << attempt); logger.warning(String.format( - "OCC conflict, retry %d/%d after %.1fs", - attempt + 1, maxRetries, delay)); + "OCC conflict, retry %d/%d after %dms", + attempt + 1, maxRetries, delayMs)); try { - Thread.sleep((long) (delay * 1000)); + Thread.sleep(delayMs); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new SQLException("Retry interrupted", ie); @@ -78,6 +78,6 @@ public static T executeWithRetry( public static T executeWithRetry( Connection connection, DatabaseOperation operation) throws MaxRetriesExceededException, SQLException { - return executeWithRetry(connection, operation, 3, 0.1); + return executeWithRetry(connection, operation, 3, 100); } } diff --git a/java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java b/java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java new file mode 100644 index 00000000..60b0ac8b --- /dev/null +++ b/java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package com.example.dsql.batch_operations; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class MainTest { + @Test + public void testBatchOperations() { + assertAll(() -> Main.main(new String[]{ + "--endpoint", System.getenv("CLUSTER_ENDPOINT"), + "--user", System.getenv().getOrDefault("CLUSTER_USER", "admin") + })); + } +} diff --git a/javascript/node-postgres/README.md b/javascript/node-postgres/README.md index 0f371762..76daf217 100644 --- a/javascript/node-postgres/README.md +++ b/javascript/node-postgres/README.md @@ -1,102 +1,106 @@ -# Aurora DSQL with node-postgres +# Batch Operations with node-postgres ## Overview -This code example demonstrates how to use `node-postgres` with Amazon Aurora DSQL. -The example shows you how to connect to an Aurora DSQL cluster and perform basic database operations. +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[node-postgres](https://node-postgres.com/) with the +[Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) for automatic +IAM authentication. -Aurora DSQL is a distributed SQL database service that provides high availability and scalability for -your PostgreSQL-compatible applications. `node-postgres` is a popular PostgreSQL adapter for Node.js that allows -you to interact with PostgreSQL databases using JavaScript code. +Two patterns are provided: -## About the code example +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple concurrent async workers each process a disjoint partition of the dataset using + `hashtext()` partitioning, with each worker running its own batch loop. -This example uses the [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/node) which automatically handles IAM token generation for authentication. +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. -The example demonstrates a flexible connection approach that works for both admin and non-admin users: +## About the code example -* When connecting as an **admin user**, the example uses the `public` schema and generates an admin authentication token. -* When connecting as a **non-admin user**, the example uses a custom `myschema` schema and generates a standard authentication token. +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. -The code automatically detects the user type and adjusts its behavior accordingly. +The parallel pattern partitions rows across workers using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. -## ⚠️ Important +⚠️ **Important** -* Running this code might result in charges to your AWS account. -* We recommend that you grant your code least privilege. At most, grant only the - minimum permissions required to perform the task. For more information, see - [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -* This code is not tested in every AWS Region. For more information, see - [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. -## Run the example +## Prerequisites -### Prerequisites +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Node.js 18 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. -* You must have an AWS account, and have your default credentials and AWS Region - configured as described in the - [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) - guide. -* Node.js: Ensure you have Node.js 18+ installed. +## Set up + +Install the required packages: ```bash -node --version +npm install ``` -It should output something similar to `v18.x` or higher. - -* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) - guide. -* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` - schema. See the - [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) - guide. +## Set up the test table -### Run the code +Before running the examples, create and populate the test table: -The example demonstrates the following operations: - -- Opening a connection to an Aurora DSQL cluster -- Creating a table -- Inserting and querying data - -The example is designed to work with both admin and non-admin users: - -- When run as an admin user, it uses the `public` schema -- When run as a non-admin user, it uses the `myschema` schema +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` -**Note:** running the example will use actual resources in your AWS account and may incur charges. +## Run the example -Set environment variables for your cluster details: +Set environment variables for your cluster: ```bash # e.g. "admin" -export CLUSTER_USER="" - +export CLUSTER_USER="admin" + # e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" +export CLUSTER_ENDPOINT="" ``` -Run the example: +Run the demo: ```bash -npm install -npm test +node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" ``` -The example contains comments explaining the code and the operations being performed. +### Command-line options -## Additional resources +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel async workers | -* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) -* [Aurora DSQL Node.js Connector for node-postgres](https://github.com/awslabs/aurora-dsql-connectors/tree/main/node/node-postgres) -* [node-postgres Documentation](https://node-postgres.com/) +## Clean up -**Note:** The connector automatically extracts the region from the cluster endpoint and defaults to the `postgres` database. +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) +- [node-postgres Documentation](https://node-postgres.com/) --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - -SPDX-License-Identifier: MIT-0 \ No newline at end of file +SPDX-License-Identifier: MIT-0 diff --git a/javascript/node-postgres/package.json b/javascript/node-postgres/package.json index 74197c68..30cc24b8 100644 --- a/javascript/node-postgres/package.json +++ b/javascript/node-postgres/package.json @@ -1,20 +1,18 @@ { - "name": "dsql_nodejs_howto", + "name": "dsql-batch-operations", "version": "1.0.0", - "description": "How To", - "main": "index.js", - "type": "module", + "description": "Batch DELETE and UPDATE operations for Aurora DSQL", + "main": "src/batch_operations/main.js", "scripts": { - "test": "node --experimental-vm-modules node_modules/.bin/jest --runInBand --detectOpenHandles --forceExit" + "start": "node src/batch_operations/main.js", + "test": "jest --runInBand --detectOpenHandles --forceExit" }, - "author": "", - "license": "ISC", "dependencies": { "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", - "assert": "2.1.0", - "uuid": "^13.0.0" + "pg": "^8.13.0" }, "devDependencies": { "jest": "^30.3.0" - } + }, + "license": "MIT-0" } diff --git a/batch-operations/javascript/node-postgres/src/batchDelete.js b/javascript/node-postgres/src/batch_operations/batchDelete.js similarity index 67% rename from batch-operations/javascript/node-postgres/src/batchDelete.js rename to javascript/node-postgres/src/batch_operations/batchDelete.js index 98396ff0..6f94da15 100644 --- a/batch-operations/javascript/node-postgres/src/batchDelete.js +++ b/javascript/node-postgres/src/batch_operations/batchDelete.js @@ -9,21 +9,29 @@ * limit. */ -const { executeWithRetry } = require("./occRetry"); +const { MaxRetriesExceededError, executeWithRetry } = require("./occRetry"); + +/** Stop retrying a batch after this many consecutive OCC exhaustions. */ +const MAX_CONSECUTIVE_FAILURES = 10; /** * Delete rows in batches, committing each batch as a separate transaction. * + * If a single batch exhausts its OCC retries, the loop retries that batch + * with a fresh connection (up to MAX_CONSECUTIVE_FAILURES times) instead + * of aborting the entire operation. + * * @param {import('pg').Pool} pool * @param {string} table * @param {string} condition - SQL WHERE clause (without `WHERE`). * @param {number} [batchSize=1000] * @param {number} [maxRetries=3] - * @param {number} [baseDelay=0.1] + * @param {number} [baseDelayMs=100] * @returns {Promise} Total rows deleted. */ -async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { +async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries = 3, baseDelayMs = 100) { let totalDeleted = 0; + let consecutiveFailures = 0; while (true) { const client = await pool.connect(); @@ -38,17 +46,27 @@ async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries return result.rowCount; }, maxRetries, - baseDelay + baseDelayMs ); await client.query("COMMIT"); totalDeleted += deleted; + consecutiveFailures = 0; console.log(`Deleted ${deleted} rows (total: ${totalDeleted})`); if (deleted === 0) break; } catch (err) { - await client.query("ROLLBACK"); - throw err; + await client.query("ROLLBACK").catch(() => {}); + if (err instanceof MaxRetriesExceededError) { + consecutiveFailures++; + console.log( + `Batch OCC retries exhausted (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}), ` + + `retrying batch with fresh connection` + ); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err; + } else { + throw err; + } } finally { client.release(); } @@ -68,14 +86,15 @@ async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries * @param {number} [numWorkers=4] * @param {number} [batchSize=1000] * @param {number} [maxRetries=3] - * @param {number} [baseDelay=0.1] + * @param {number} [baseDelayMs=100] * @returns {Promise} Total rows deleted. */ async function parallelBatchDelete( - pool, table, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelay = 0.1 + pool, table, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelayMs = 100 ) { async function worker(workerId) { let totalDeleted = 0; + let consecutiveFailures = 0; const partitionCondition = `${condition} AND abs(hashtext(id::text)) % ${numWorkers} = ${workerId}`; @@ -92,17 +111,27 @@ async function parallelBatchDelete( return result.rowCount; }, maxRetries, - baseDelay + baseDelayMs ); await client.query("COMMIT"); totalDeleted += deleted; + consecutiveFailures = 0; console.log(`Worker ${workerId}: Deleted ${deleted} rows (total: ${totalDeleted})`); if (deleted === 0) break; } catch (err) { - await client.query("ROLLBACK"); - throw err; + await client.query("ROLLBACK").catch(() => {}); + if (err instanceof MaxRetriesExceededError) { + consecutiveFailures++; + console.log( + `Worker ${workerId}: Batch OCC retries exhausted (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}), ` + + `retrying batch with fresh connection` + ); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err; + } else { + throw err; + } } finally { client.release(); } diff --git a/batch-operations/javascript/node-postgres/src/batchUpdate.js b/javascript/node-postgres/src/batch_operations/batchUpdate.js similarity index 69% rename from batch-operations/javascript/node-postgres/src/batchUpdate.js rename to javascript/node-postgres/src/batch_operations/batchUpdate.js index e7e53c3b..ab4bf023 100644 --- a/batch-operations/javascript/node-postgres/src/batchUpdate.js +++ b/javascript/node-postgres/src/batch_operations/batchUpdate.js @@ -9,22 +9,30 @@ * limit. Uses a subquery to avoid reprocessing rows. */ -const { executeWithRetry } = require("./occRetry"); +const { MaxRetriesExceededError, executeWithRetry } = require("./occRetry"); + +/** Stop retrying a batch after this many consecutive OCC exhaustions. */ +const MAX_CONSECUTIVE_FAILURES = 10; /** * Update rows in batches, committing each batch as a separate transaction. * + * If a single batch exhausts its OCC retries, the loop retries that batch + * with a fresh connection (up to MAX_CONSECUTIVE_FAILURES times) instead + * of aborting the entire operation. + * * @param {import('pg').Pool} pool * @param {string} table * @param {string} setClause - SQL SET expressions (without `SET`). * @param {string} condition - SQL WHERE clause (without `WHERE`). * @param {number} [batchSize=1000] * @param {number} [maxRetries=3] - * @param {number} [baseDelay=0.1] + * @param {number} [baseDelayMs=100] * @returns {Promise} Total rows updated. */ -async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, maxRetries = 3, baseDelay = 0.1) { +async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, maxRetries = 3, baseDelayMs = 100) { let totalUpdated = 0; + let consecutiveFailures = 0; while (true) { const client = await pool.connect(); @@ -44,17 +52,27 @@ async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, return result.rowCount; }, maxRetries, - baseDelay + baseDelayMs ); await client.query("COMMIT"); totalUpdated += updated; + consecutiveFailures = 0; console.log(`Updated ${updated} rows (total: ${totalUpdated})`); if (updated === 0) break; } catch (err) { - await client.query("ROLLBACK"); - throw err; + await client.query("ROLLBACK").catch(() => {}); + if (err instanceof MaxRetriesExceededError) { + consecutiveFailures++; + console.log( + `Batch OCC retries exhausted (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}), ` + + `retrying batch with fresh connection` + ); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err; + } else { + throw err; + } } finally { client.release(); } @@ -75,14 +93,15 @@ async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, * @param {number} [numWorkers=4] * @param {number} [batchSize=1000] * @param {number} [maxRetries=3] - * @param {number} [baseDelay=0.1] + * @param {number} [baseDelayMs=100] * @returns {Promise} Total rows updated. */ async function parallelBatchUpdate( - pool, table, setClause, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelay = 0.1 + pool, table, setClause, condition, numWorkers = 4, batchSize = 1000, maxRetries = 3, baseDelayMs = 100 ) { async function worker(workerId) { let totalUpdated = 0; + let consecutiveFailures = 0; const partitionCondition = `${condition} AND abs(hashtext(id::text)) % ${numWorkers} = ${workerId}`; @@ -104,17 +123,27 @@ async function parallelBatchUpdate( return result.rowCount; }, maxRetries, - baseDelay + baseDelayMs ); await client.query("COMMIT"); totalUpdated += updated; + consecutiveFailures = 0; console.log(`Worker ${workerId}: Updated ${updated} rows (total: ${totalUpdated})`); if (updated === 0) break; } catch (err) { - await client.query("ROLLBACK"); - throw err; + await client.query("ROLLBACK").catch(() => {}); + if (err instanceof MaxRetriesExceededError) { + consecutiveFailures++; + console.log( + `Worker ${workerId}: Batch OCC retries exhausted (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}), ` + + `retrying batch with fresh connection` + ); + if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw err; + } else { + throw err; + } } finally { client.release(); } diff --git a/batch-operations/javascript/node-postgres/src/main.js b/javascript/node-postgres/src/batch_operations/main.js similarity index 86% rename from batch-operations/javascript/node-postgres/src/main.js rename to javascript/node-postgres/src/batch_operations/main.js index 247e50b8..56f49998 100644 --- a/batch-operations/javascript/node-postgres/src/main.js +++ b/javascript/node-postgres/src/batch_operations/main.js @@ -12,7 +12,6 @@ const { AuroraDSQLPool } = require("@aws/aurora-dsql-node-postgres-connector"); const { batchDelete, parallelBatchDelete } = require("./batchDelete"); const { batchUpdate, parallelBatchUpdate } = require("./batchUpdate"); -const { repopulateTestData } = require("./repopulate"); function parseArgs() { const args = process.argv.slice(2); @@ -74,22 +73,13 @@ async function main() { await runOperation("Sequential Batch DELETE (category = 'electronics')", () => batchDelete(pool, table, "category = 'electronics'", batchSize)); - await runOperation("Repopulate test data", - () => repopulateTestData(pool, 5000, batchSize)); - await runOperation("Sequential Batch UPDATE (clothing -> processed)", () => batchUpdate(pool, table, "status = 'processed'", "category = 'clothing' AND status != 'processed'", batchSize)); - await runOperation("Repopulate test data", - () => repopulateTestData(pool, 5000, batchSize)); - await runOperation(`Parallel Batch DELETE (category = 'food') [${numWorkers} workers]`, () => parallelBatchDelete(pool, table, "category = 'food'", numWorkers, batchSize)); - await runOperation("Repopulate test data", - () => repopulateTestData(pool, 5000, batchSize)); - await runOperation(`Parallel Batch UPDATE (books -> archived) [${numWorkers} workers]`, () => parallelBatchUpdate(pool, table, "status = 'archived'", "category = 'books' AND status != 'archived'", numWorkers, batchSize)); @@ -99,7 +89,11 @@ async function main() { } } -main().catch((err) => { - console.error("Fatal error:", err.message); - process.exit(1); -}); +if (require.main === module) { + main().catch((err) => { + console.error("Fatal error:", err.message); + process.exit(1); + }); +} + +module.exports = { main }; diff --git a/batch-operations/javascript/node-postgres/src/occRetry.js b/javascript/node-postgres/src/batch_operations/occRetry.js similarity index 84% rename from batch-operations/javascript/node-postgres/src/occRetry.js rename to javascript/node-postgres/src/batch_operations/occRetry.js index 26442e34..e541cfd0 100644 --- a/batch-operations/javascript/node-postgres/src/occRetry.js +++ b/javascript/node-postgres/src/batch_operations/occRetry.js @@ -13,8 +13,8 @@ class MaxRetriesExceededError extends Error { } } -function sleep(seconds) { - return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); } /** @@ -27,10 +27,10 @@ function sleep(seconds) { * @param {(client: import('pg').PoolClient) => Promise<*>} operation - Async * function that performs database work. Should NOT commit. * @param {number} [maxRetries=3] - Maximum retry attempts. - * @param {number} [baseDelay=0.1] - Base delay in seconds for backoff. + * @param {number} [baseDelayMs=100] - Base delay in milliseconds for backoff. * @returns {Promise<*>} The return value of `operation(client)`. */ -async function executeWithRetry(client, operation, maxRetries = 3, baseDelay = 0.1) { +async function executeWithRetry(client, operation, maxRetries = 3, baseDelayMs = 100) { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(client); @@ -40,11 +40,11 @@ async function executeWithRetry(client, operation, maxRetries = 3, baseDelay = 0 if (attempt >= maxRetries) { throw new MaxRetriesExceededError(maxRetries); } - const delay = baseDelay * Math.pow(2, attempt); + const delayMs = baseDelayMs * Math.pow(2, attempt); console.warn( - `OCC conflict, retry ${attempt + 1}/${maxRetries} after ${delay.toFixed(1)}s` + `OCC conflict, retry ${attempt + 1}/${maxRetries} after ${delayMs}ms` ); - await sleep(delay); + await sleep(delayMs); } else { throw e; } diff --git a/javascript/node-postgres/test/batch_operations/batch_operations.test.js b/javascript/node-postgres/test/batch_operations/batch_operations.test.js new file mode 100644 index 00000000..85e66c13 --- /dev/null +++ b/javascript/node-postgres/test/batch_operations/batch_operations.test.js @@ -0,0 +1,8 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +const { main } = require("../../src/batch_operations/main"); + +test("Batch operations demo runs successfully", async () => { + await main(); +}, 120000); diff --git a/python/psycopg2/README.md b/python/psycopg2/README.md index eae3007a..ca273c92 100644 --- a/python/psycopg2/README.md +++ b/python/psycopg2/README.md @@ -1,111 +1,125 @@ -# Aurora DSQL with Psycopg2 +# Batch Operations with psycopg2 ## Overview -This code example demonstrates how to use Psycopg (version 2) with Amazon Aurora DSQL. The example shows you how to -connect to an Aurora DSQL cluster and perform basic database operations. +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[psycopg2](https://www.psycopg.org/docs/) with the +[Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic +IAM authentication and connection pooling. -Aurora DSQL is a distributed SQL database service that provides high availability and scalability for -your PostgreSQL-compatible applications. Psycopg2 is a popular PostgreSQL adapter for Python that allows -you to interact with PostgreSQL databases using Python code. +Two patterns are provided: -## About the code example +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. -This example uses the [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/python/connector) which automatically handles IAM token generation for authentication. +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. -The example demonstrates a flexible connection approach that works for both admin and non-admin users: +## About the code example -* When connecting as an **admin user**, the example uses the `public` schema and generates an admin authentication token. -* When connecting as a **non-admin user**, the example uses a custom `myschema` schema and generates a standard authentication token. +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. -The code automatically detects the user type and adjusts its behavior accordingly. +The parallel pattern partitions rows across worker threads using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. -## ⚠️ Important +Connection pooling uses `AuroraDSQLThreadedConnectionPool` from the Aurora DSQL Python Connector, which +automatically handles IAM token refresh for long-running batch jobs. -* Running this code might result in charges to your AWS account. -* We recommend that you grant your code least privilege. At most, grant only the - minimum permissions required to perform the task. For more information, see - [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). -* This code is not tested in every AWS Region. For more information, see - [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). +⚠️ **Important** -## Run the example +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. -### Prerequisites +## Prerequisites -* You must have an AWS account, and have your default credentials and AWS Region - configured as described in the - [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) - guide. -* [Python 3.8.0](https://www.python.org/) or later. -* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) - guide. -* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` - schema. See the - [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) - guide. +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Python 3.8 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. -### Set up environment for examples +## Set up -1. Create and activate a Python virtual environment: +Create and activate a Python virtual environment: ```bash python3 -m venv .venv -source .venv/bin/activate # Linux, macOS -# or -.venv\Scripts\activate # Windows +source .venv/bin/activate ``` -2. Install the required packages for running the examples: +Install the required packages: ```bash pip install -r requirements.txt ``` -### Run the code - -The example demonstrates the following operations: - -- Opening a connection to an Aurora DSQL cluster -- Creating a table -- Inserting and querying data +## Set up the test table -The example is designed to work with both admin and non-admin users: +Before running the examples, create and populate the test table. Run each INSERT as a separate +transaction (each block of 1,000 rows): -- When run as an admin user, it uses the `public` schema -- When run as a non-admin user, it uses the `myschema` schema +```bash +export CLUSTER_ENDPOINT="" +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ + -f ../../sql/batch_test_setup.sql +``` -**Note:** running the example will use actual resources in your AWS account and may incur charges. +## Run the example -Set environment variables for your cluster details: +Set environment variables for your cluster: ```bash # e.g. "admin" -export CLUSTER_USER="" +export CLUSTER_USER="admin" # e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" +export CLUSTER_ENDPOINT="" ``` -Run the example: +Run the demo: ```bash -python src/example_preferred.py +python src/main.py --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" ``` -The example contains comments explaining the code and the operations being performed. +### Command-line options -## Additional resources +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | -* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) -* [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/python/connector) -* [Psycopg2 Documentation](https://www.psycopg.org/docs/) +The demo runs the following sequence: -**Note:** The connector automatically extracts the region from the cluster endpoint, defaults to the `postgres` database, and handles SSL configuration. +1. Sequential batch DELETE (all `electronics` rows) +2. Sequential batch UPDATE (`clothing` → `processed`) +3. Parallel batch DELETE (all `food` rows) with N workers +4. Parallel batch UPDATE (`books` → `archived`) with N workers + +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) +- [Psycopg2 Documentation](https://www.psycopg.org/docs/) +- [Batch operations in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/batch-operations.html) --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: MIT-0 diff --git a/python/psycopg2/requirements.txt b/python/psycopg2/requirements.txt index 74894da1..c1e06f88 100644 --- a/python/psycopg2/requirements.txt +++ b/python/psycopg2/requirements.txt @@ -1,3 +1,3 @@ aurora-dsql-python-connector psycopg2-binary>=2.9 -pytest>=8 +pytest>=7.0 diff --git a/batch-operations/python/psycopg2/src/batch_delete.py b/python/psycopg2/src/batch_operations/batch_delete.py similarity index 67% rename from batch-operations/python/psycopg2/src/batch_delete.py rename to python/psycopg2/src/batch_operations/batch_delete.py index baa968fe..a8766e16 100644 --- a/batch-operations/python/psycopg2/src/batch_delete.py +++ b/python/psycopg2/src/batch_operations/batch_delete.py @@ -14,24 +14,33 @@ import threading -from occ_retry import execute_with_retry +from occ_retry import MaxRetriesExceeded, execute_with_retry +# Stop retrying a batch after this many consecutive OCC exhaustions. +MAX_CONSECUTIVE_FAILURES = 10 -def batch_delete(pool, table, condition, batch_size=1000, max_retries=3, base_delay=0.1): + +def batch_delete(pool, table, condition, batch_size=1000, max_retries=3, base_delay_ms=100): """Delete rows in batches, committing each batch as a separate transaction. + If a single batch exhausts its OCC retries, the loop retries that batch + with a fresh connection (up to ``MAX_CONSECUTIVE_FAILURES`` times) instead + of aborting the entire operation. Previously committed batches are durable, + so forward progress is preserved. + Args: pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. table: Name of the table to delete from. condition: SQL WHERE clause (without the ``WHERE`` keyword). batch_size: Number of rows to delete per transaction (default 1,000). max_retries: Maximum OCC retry attempts per batch (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). + base_delay_ms: Base delay in milliseconds for exponential backoff (default 100). Returns: Total number of rows deleted across all batches. """ total_deleted = 0 + consecutive_failures = 0 while True: conn = pool.getconn() @@ -50,14 +59,24 @@ def delete_batch(conn): return cur.rowcount deleted = execute_with_retry( - conn, delete_batch, max_retries=max_retries, base_delay=base_delay + conn, delete_batch, max_retries=max_retries, base_delay_ms=base_delay_ms ) conn.commit() total_deleted += deleted + consecutive_failures = 0 # reset on success print(f"Deleted {deleted} rows (total: {total_deleted})") if deleted == 0: break + except MaxRetriesExceeded: + conn.rollback() + consecutive_failures += 1 + print( + f"Batch OCC retries exhausted ({consecutive_failures}/" + f"{MAX_CONSECUTIVE_FAILURES}), retrying batch with fresh connection" + ) + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: + raise except Exception: conn.rollback() raise diff --git a/batch-operations/python/psycopg2/src/batch_update.py b/python/psycopg2/src/batch_operations/batch_update.py similarity index 70% rename from batch-operations/python/psycopg2/src/batch_update.py rename to python/psycopg2/src/batch_operations/batch_update.py index 7a6ad44d..4f59656a 100644 --- a/batch-operations/python/psycopg2/src/batch_update.py +++ b/python/psycopg2/src/batch_operations/batch_update.py @@ -9,15 +9,22 @@ NOW() to track processed rows. """ -from occ_retry import execute_with_retry +from occ_retry import MaxRetriesExceeded, execute_with_retry +# Stop retrying a batch after this many consecutive OCC exhaustions. +MAX_CONSECUTIVE_FAILURES = 10 -def batch_update(pool, table, set_clause, condition, batch_size=1000, max_retries=3, base_delay=0.1): + +def batch_update(pool, table, set_clause, condition, batch_size=1000, max_retries=3, base_delay_ms=100): """Update rows in batches, committing each batch as a separate transaction. Uses a subquery to select rows not yet updated and sets ``updated_at = NOW()`` on each batch to track which rows have been processed. + If a single batch exhausts its OCC retries, the loop retries that batch + with a fresh connection (up to ``MAX_CONSECUTIVE_FAILURES`` times) instead + of aborting the entire operation. + Args: pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance. table: Name of the table to update. @@ -27,12 +34,13 @@ def batch_update(pool, table, set_clause, condition, batch_size=1000, max_retrie identifies rows needing update. batch_size: Number of rows to update per transaction (default 1,000). max_retries: Maximum OCC retry attempts per batch (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). + base_delay_ms: Base delay in milliseconds for exponential backoff (default 100). Returns: Total number of rows updated across all batches. """ total_updated = 0 + consecutive_failures = 0 while True: conn = pool.getconn() @@ -51,14 +59,24 @@ def update_batch(conn): return cur.rowcount updated = execute_with_retry( - conn, update_batch, max_retries=max_retries, base_delay=base_delay + conn, update_batch, max_retries=max_retries, base_delay_ms=base_delay_ms ) conn.commit() total_updated += updated + consecutive_failures = 0 # reset on success print(f"Updated {updated} rows (total: {total_updated})") if updated == 0: break + except MaxRetriesExceeded: + conn.rollback() + consecutive_failures += 1 + print( + f"Batch OCC retries exhausted ({consecutive_failures}/" + f"{MAX_CONSECUTIVE_FAILURES}), retrying batch with fresh connection" + ) + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: + raise except Exception: conn.rollback() raise diff --git a/batch-operations/python/psycopg2/src/main.py b/python/psycopg2/src/batch_operations/main.py similarity index 82% rename from batch-operations/python/psycopg2/src/main.py rename to python/psycopg2/src/batch_operations/main.py index 379dd0f0..ad1ebd35 100644 --- a/batch-operations/python/psycopg2/src/main.py +++ b/python/psycopg2/src/batch_operations/main.py @@ -20,7 +20,6 @@ from batch_update import batch_update from parallel_batch_delete import parallel_batch_delete from parallel_batch_update import parallel_batch_update -from repopulate import repopulate_test_data def parse_args(): @@ -94,15 +93,7 @@ def main(): batch_size=batch_size, ) - # 2. Repopulate test data - run_operation( - "Repopulate test data", - repopulate_test_data, - pool, - batch_size=batch_size, - ) - - # 3. Sequential batch UPDATE — update 'clothing' status to 'processed' + # 2. Sequential batch UPDATE — update 'clothing' status to 'processed' run_operation( "Sequential Batch UPDATE (clothing -> processed)", batch_update, @@ -111,15 +102,7 @@ def main(): batch_size=batch_size, ) - # 4. Repopulate test data - run_operation( - "Repopulate test data", - repopulate_test_data, - pool, - batch_size=batch_size, - ) - - # 5. Parallel batch DELETE — delete all 'food' rows + # 3. Parallel batch DELETE — delete all 'food' rows run_operation( f"Parallel Batch DELETE (category = 'food') [{num_workers} workers]", parallel_batch_delete, @@ -128,15 +111,7 @@ def main(): batch_size=batch_size, ) - # 6. Repopulate test data - run_operation( - "Repopulate test data", - repopulate_test_data, - pool, - batch_size=batch_size, - ) - - # 7. Parallel batch UPDATE — update 'books' status to 'archived' + # 4. Parallel batch UPDATE — update 'books' status to 'archived' run_operation( f"Parallel Batch UPDATE (books -> archived) [{num_workers} workers]", parallel_batch_update, diff --git a/batch-operations/python/psycopg2/src/occ_retry.py b/python/psycopg2/src/batch_operations/occ_retry.py similarity index 85% rename from batch-operations/python/psycopg2/src/occ_retry.py rename to python/psycopg2/src/batch_operations/occ_retry.py index 1a150f6b..03c3fa1b 100644 --- a/batch-operations/python/psycopg2/src/occ_retry.py +++ b/python/psycopg2/src/batch_operations/occ_retry.py @@ -22,7 +22,7 @@ def __init__(self, max_retries): self.max_retries = max_retries -def execute_with_retry(connection, operation, max_retries=3, base_delay=0.1): +def execute_with_retry(connection, operation, max_retries=3, base_delay_ms=100): """Execute a database operation with OCC conflict retry and exponential backoff. Runs ``operation(connection)`` and retries on serialization failures @@ -34,7 +34,8 @@ def execute_with_retry(connection, operation, max_retries=3, base_delay=0.1): operation: A callable that accepts a connection and performs database work. It should NOT commit — the caller is responsible for commits. max_retries: Maximum number of retry attempts (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). + base_delay_ms: Base delay in milliseconds for exponential backoff + (default 100). Returns: The return value of ``operation(connection)``. @@ -50,11 +51,11 @@ def execute_with_retry(connection, operation, max_retries=3, base_delay=0.1): connection.rollback() if attempt >= max_retries: raise MaxRetriesExceeded(max_retries) - delay = base_delay * (2 ** attempt) + delay_ms = base_delay_ms * (2 ** attempt) logger.warning( - "OCC conflict, retry %d/%d after %.1fs", + "OCC conflict, retry %d/%d after %dms", attempt + 1, max_retries, - delay, + delay_ms, ) - time.sleep(delay) + time.sleep(delay_ms / 1000.0) diff --git a/batch-operations/python/psycopg2/src/parallel_batch_delete.py b/python/psycopg2/src/batch_operations/parallel_batch_delete.py similarity index 73% rename from batch-operations/python/psycopg2/src/parallel_batch_delete.py rename to python/psycopg2/src/batch_operations/parallel_batch_delete.py index 9347af8a..40d70105 100644 --- a/batch-operations/python/psycopg2/src/parallel_batch_delete.py +++ b/python/psycopg2/src/batch_operations/parallel_batch_delete.py @@ -10,14 +10,20 @@ import threading -from occ_retry import execute_with_retry +from occ_retry import MaxRetriesExceeded, execute_with_retry + +# Stop retrying a batch after this many consecutive OCC exhaustions. +MAX_CONSECUTIVE_FAILURES = 10 def parallel_batch_delete( - pool, table, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay=0.1 + pool, table, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay_ms=100 ): """Delete rows in parallel using multiple worker threads. + Each worker retries a batch with a fresh connection if OCC retries are + exhausted, up to ``MAX_CONSECUTIVE_FAILURES`` times before giving up. + Args: pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance sized to at least *num_workers* connections. @@ -26,7 +32,7 @@ def parallel_batch_delete( num_workers: Number of parallel worker threads (default 4). batch_size: Number of rows to delete per transaction (default 1,000). max_retries: Maximum OCC retry attempts per batch (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). + base_delay_ms: Base delay in milliseconds for exponential backoff (default 100). Returns: Total number of rows deleted across all workers. @@ -36,6 +42,7 @@ def parallel_batch_delete( def worker(worker_id): total_deleted = 0 + consecutive_failures = 0 partition_condition = ( f"{condition} AND abs(hashtext(id::text)) % {num_workers} = {worker_id}" ) @@ -57,10 +64,11 @@ def delete_batch(conn): return cur.rowcount deleted = execute_with_retry( - conn, delete_batch, max_retries=max_retries, base_delay=base_delay + conn, delete_batch, max_retries=max_retries, base_delay_ms=base_delay_ms ) conn.commit() total_deleted += deleted + consecutive_failures = 0 print( f"Worker {worker_id}: Deleted {deleted} rows " f"(total: {total_deleted})" @@ -68,6 +76,17 @@ def delete_batch(conn): if deleted == 0: break + except MaxRetriesExceeded: + conn.rollback() + consecutive_failures += 1 + print( + f"Worker {worker_id}: Batch OCC retries exhausted " + f"({consecutive_failures}/{MAX_CONSECUTIVE_FAILURES}), " + f"retrying batch with fresh connection" + ) + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: + errors[worker_id] = MaxRetriesExceeded(max_retries) + break except Exception as exc: conn.rollback() errors[worker_id] = exc diff --git a/batch-operations/python/psycopg2/src/parallel_batch_update.py b/python/psycopg2/src/batch_operations/parallel_batch_update.py similarity index 73% rename from batch-operations/python/psycopg2/src/parallel_batch_update.py rename to python/psycopg2/src/batch_operations/parallel_batch_update.py index 4bb73405..6e6849c7 100644 --- a/batch-operations/python/psycopg2/src/parallel_batch_update.py +++ b/python/psycopg2/src/batch_operations/parallel_batch_update.py @@ -10,14 +10,20 @@ import threading -from occ_retry import execute_with_retry +from occ_retry import MaxRetriesExceeded, execute_with_retry + +# Stop retrying a batch after this many consecutive OCC exhaustions. +MAX_CONSECUTIVE_FAILURES = 10 def parallel_batch_update( - pool, table, set_clause, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay=0.1 + pool, table, set_clause, condition, num_workers=4, batch_size=1000, max_retries=3, base_delay_ms=100 ): """Update rows in parallel using multiple worker threads. + Each worker retries a batch with a fresh connection if OCC retries are + exhausted, up to ``MAX_CONSECUTIVE_FAILURES`` times before giving up. + Args: pool: A ``dsql.AuroraDSQLThreadedConnectionPool`` instance sized to at least *num_workers* connections. @@ -27,7 +33,7 @@ def parallel_batch_update( num_workers: Number of parallel worker threads (default 4). batch_size: Number of rows to update per transaction (default 1,000). max_retries: Maximum OCC retry attempts per batch (default 3). - base_delay: Base delay in seconds for exponential backoff (default 0.1). + base_delay_ms: Base delay in milliseconds for exponential backoff (default 100). Returns: Total number of rows updated across all workers. @@ -37,6 +43,7 @@ def parallel_batch_update( def worker(worker_id): total_updated = 0 + consecutive_failures = 0 partition_condition = ( f"{condition} AND abs(hashtext(id::text)) % {num_workers} = {worker_id}" ) @@ -58,10 +65,11 @@ def update_batch(conn): return cur.rowcount updated = execute_with_retry( - conn, update_batch, max_retries=max_retries, base_delay=base_delay + conn, update_batch, max_retries=max_retries, base_delay_ms=base_delay_ms ) conn.commit() total_updated += updated + consecutive_failures = 0 print( f"Worker {worker_id}: Updated {updated} rows " f"(total: {total_updated})" @@ -69,6 +77,17 @@ def update_batch(conn): if updated == 0: break + except MaxRetriesExceeded: + conn.rollback() + consecutive_failures += 1 + print( + f"Worker {worker_id}: Batch OCC retries exhausted " + f"({consecutive_failures}/{MAX_CONSECUTIVE_FAILURES}), " + f"retrying batch with fresh connection" + ) + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: + errors[worker_id] = MaxRetriesExceeded(max_retries) + break except Exception as exc: conn.rollback() errors[worker_id] = exc diff --git a/python/psycopg2/test/batch_operations/test_batch_operations.py b/python/psycopg2/test/batch_operations/test_batch_operations.py new file mode 100644 index 00000000..efe009e0 --- /dev/null +++ b/python/psycopg2/test/batch_operations/test_batch_operations.py @@ -0,0 +1,18 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import pytest +from main import main + + +def test_batch_operations(): + """Integration test: runs the full batch operations demo against a live cluster. + + Requires CLUSTER_ENDPOINT and CLUSTER_USER environment variables. + Run with: pytest test/ --endpoint --user + Or set env vars and run: pytest test/ + """ + try: + main() + except Exception as e: + pytest.fail(f"Unexpected exception: {e}") diff --git a/sql/batch_operations/batch_test_setup.sql b/sql/batch_operations/batch_test_setup.sql new file mode 100644 index 00000000..3ac6da4b --- /dev/null +++ b/sql/batch_operations/batch_test_setup.sql @@ -0,0 +1,214 @@ +-- ============================================================================= +-- Sample table setup for Aurora DSQL batch operations +-- +-- This script creates a test table and populates it with 25,000 rows of sample +-- data distributed across 5 categories (~5,000 rows each). Each category is +-- used by a different demo operation: +-- +-- electronics → Sequential batch DELETE +-- clothing → Sequential batch UPDATE +-- food → Parallel batch DELETE +-- books → Parallel batch UPDATE +-- toys → (unused, remains in table) +-- +-- Aurora DSQL limits each transaction to 3,000 row mutations, so we insert +-- in batches of 1,000 rows. Run each INSERT as a separate transaction. +-- ============================================================================= + +-- Drop the table if it already exists +DROP TABLE IF EXISTS batch_test; + +-- Create the sample table. +-- Uses UUID primary key with gen_random_uuid() to minimize OCC contention, +-- following Aurora DSQL best practice of random keys. +CREATE TABLE batch_test ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + value NUMERIC(10,2), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create an asynchronous index on the category column. +CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); + +-- ============================================================================= +-- Populate the table with 25,000 rows of test data (25 batches of 1,000). +-- Each INSERT is 1,000 rows — run each as a separate transaction. +-- ============================================================================= + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); From 3e8768ce81c23921a4da630c479f17106eb9b85f Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Wed, 18 Mar 2026 10:15:34 -0700 Subject: [PATCH 6/8] Move batch operations to standalone directories under each language --- java/batch_operations/README.md | 114 + .../batch_operations/batch_test_setup.sql | 0 .../{pgjdbc => batch_operations}/build.gradle | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + java/batch_operations/gradlew | 248 + java/batch_operations/gradlew.bat | 93 + java/batch_operations/settings.gradle | 1 + .../dsql/batch_operations/BatchDelete.java | 0 .../dsql/batch_operations/BatchUpdate.java | 0 .../example/dsql/batch_operations/Main.java | 0 .../dsql/batch_operations/OccRetry.java | 0 .../dsql/batch_operations/MainTest.java | 0 java/pgjdbc/README.md | 143 +- javascript/batch_operations/.gitignore | 3 + javascript/batch_operations/README.md | 121 + .../batch_operations/batch_test_setup.sql | 214 + javascript/batch_operations/package-lock.json | 5430 +++++++++++++++++ javascript/batch_operations/package.json | 18 + .../src}/batchDelete.js | 0 .../src}/batchUpdate.js | 0 .../src}/main.js | 0 .../src}/occRetry.js | 0 .../test}/batch_operations.test.js | 2 +- javascript/node-postgres/README.md | 130 +- javascript/node-postgres/package-lock.json | 556 +- javascript/node-postgres/package.json | 18 +- python/batch_operations/README.md | 140 + python/batch_operations/batch_test_setup.sql | 214 + .../src}/batch_delete.py | 0 .../src}/batch_update.py | 0 .../src}/main.py | 2 - .../src}/occ_retry.py | 0 .../src}/parallel_batch_delete.py | 0 .../src}/parallel_batch_update.py | 0 .../test}/test_batch_operations.py | 0 python/psycopg2/README.md | 138 +- python/psycopg2/requirements.txt | 2 +- 38 files changed, 6830 insertions(+), 764 deletions(-) create mode 100644 java/batch_operations/README.md rename {sql => java}/batch_operations/batch_test_setup.sql (100%) rename java/{pgjdbc => batch_operations}/build.gradle (100%) create mode 100644 java/batch_operations/gradle/wrapper/gradle-wrapper.jar create mode 100644 java/batch_operations/gradle/wrapper/gradle-wrapper.properties create mode 100755 java/batch_operations/gradlew create mode 100644 java/batch_operations/gradlew.bat create mode 100644 java/batch_operations/settings.gradle rename java/{pgjdbc => batch_operations}/src/main/java/com/example/dsql/batch_operations/BatchDelete.java (100%) rename java/{pgjdbc => batch_operations}/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java (100%) rename java/{pgjdbc => batch_operations}/src/main/java/com/example/dsql/batch_operations/Main.java (100%) rename java/{pgjdbc => batch_operations}/src/main/java/com/example/dsql/batch_operations/OccRetry.java (100%) rename java/{pgjdbc => batch_operations}/src/test/java/com/example/dsql/batch_operations/MainTest.java (100%) create mode 100644 javascript/batch_operations/.gitignore create mode 100644 javascript/batch_operations/README.md create mode 100644 javascript/batch_operations/batch_test_setup.sql create mode 100644 javascript/batch_operations/package-lock.json create mode 100644 javascript/batch_operations/package.json rename javascript/{node-postgres/src/batch_operations => batch_operations/src}/batchDelete.js (100%) rename javascript/{node-postgres/src/batch_operations => batch_operations/src}/batchUpdate.js (100%) rename javascript/{node-postgres/src/batch_operations => batch_operations/src}/main.js (100%) rename javascript/{node-postgres/src/batch_operations => batch_operations/src}/occRetry.js (100%) rename javascript/{node-postgres/test/batch_operations => batch_operations/test}/batch_operations.test.js (76%) create mode 100644 python/batch_operations/README.md create mode 100644 python/batch_operations/batch_test_setup.sql rename python/{psycopg2/src/batch_operations => batch_operations/src}/batch_delete.py (100%) rename python/{psycopg2/src/batch_operations => batch_operations/src}/batch_update.py (100%) rename python/{psycopg2/src/batch_operations => batch_operations/src}/main.py (98%) rename python/{psycopg2/src/batch_operations => batch_operations/src}/occ_retry.py (100%) rename python/{psycopg2/src/batch_operations => batch_operations/src}/parallel_batch_delete.py (100%) rename python/{psycopg2/src/batch_operations => batch_operations/src}/parallel_batch_update.py (100%) rename python/{psycopg2/test/batch_operations => batch_operations/test}/test_batch_operations.py (100%) diff --git a/java/batch_operations/README.md b/java/batch_operations/README.md new file mode 100644 index 00000000..f5b78caa --- /dev/null +++ b/java/batch_operations/README.md @@ -0,0 +1,114 @@ +# Batch Operations with pgJDBC + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[pgJDBC](https://jdbc.postgresql.org/) with the +[Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) for automatic +IAM authentication. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across worker threads using +`abs(hashtext(CAST(id AS text))) % num_workers = worker_id`, ensuring workers operate on disjoint sets +of rows and avoid OCC conflicts with each other. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Java Development Kit (JDK) 17 or later. +- Gradle (the wrapper is included in this project). +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up the test table + +Before running the examples, create and populate the test table. Aurora DSQL uses IAM authentication, +so you need to generate a fresh auth token each time you connect with `psql`: + +```bash +export CLUSTER_ENDPOINT="" +export CLUSTER_REGION="" + +# Generate a fresh auth token (expires in 3600 seconds) +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -f batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Build and run: + +```bash +./gradlew run --args="--endpoint $CLUSTER_ENDPOINT --user $CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | + +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +# Generate a fresh token if the previous one expired +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) +- [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/sql/batch_operations/batch_test_setup.sql b/java/batch_operations/batch_test_setup.sql similarity index 100% rename from sql/batch_operations/batch_test_setup.sql rename to java/batch_operations/batch_test_setup.sql diff --git a/java/pgjdbc/build.gradle b/java/batch_operations/build.gradle similarity index 100% rename from java/pgjdbc/build.gradle rename to java/batch_operations/build.gradle diff --git a/java/batch_operations/gradle/wrapper/gradle-wrapper.jar b/java/batch_operations/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/java/batch_operations/gradle/wrapper/gradle-wrapper.properties b/java/batch_operations/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..dbc3ce4a --- /dev/null +++ b/java/batch_operations/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/batch_operations/gradlew b/java/batch_operations/gradlew new file mode 100755 index 00000000..0262dcbd --- /dev/null +++ b/java/batch_operations/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/java/batch_operations/gradlew.bat b/java/batch_operations/gradlew.bat new file mode 100644 index 00000000..e509b2dd --- /dev/null +++ b/java/batch_operations/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/batch_operations/settings.gradle b/java/batch_operations/settings.gradle new file mode 100644 index 00000000..23e31e80 --- /dev/null +++ b/java/batch_operations/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'dsql-batch-operations' diff --git a/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchDelete.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java similarity index 100% rename from java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchDelete.java rename to java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java diff --git a/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java similarity index 100% rename from java/pgjdbc/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java rename to java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java diff --git a/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/Main.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/Main.java similarity index 100% rename from java/pgjdbc/src/main/java/com/example/dsql/batch_operations/Main.java rename to java/batch_operations/src/main/java/com/example/dsql/batch_operations/Main.java diff --git a/java/pgjdbc/src/main/java/com/example/dsql/batch_operations/OccRetry.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/OccRetry.java similarity index 100% rename from java/pgjdbc/src/main/java/com/example/dsql/batch_operations/OccRetry.java rename to java/batch_operations/src/main/java/com/example/dsql/batch_operations/OccRetry.java diff --git a/java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java b/java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java similarity index 100% rename from java/pgjdbc/src/test/java/com/example/dsql/batch_operations/MainTest.java rename to java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java diff --git a/java/pgjdbc/README.md b/java/pgjdbc/README.md index a158fe20..ad730e69 100644 --- a/java/pgjdbc/README.md +++ b/java/pgjdbc/README.md @@ -1,99 +1,120 @@ -# Batch Operations with pgJDBC +# Aurora DSQL with pgJDBC ## Overview -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[pgJDBC](https://jdbc.postgresql.org/) with the -[Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) for automatic -IAM authentication via the AWS JDBC Wrapper. +This code example demonstrates how to use `pgJDBC` with Amazon Aurora DSQL. +The example shows you how to connect to an Aurora DSQL cluster and perform basic database operations. -Two patterns are provided: +Aurora DSQL is a distributed SQL database service that provides high availability and scalability for +your PostgreSQL-compatible applications. `pgJDBC` is a popular PostgreSQL adapter for Java that allows +you to interact with PostgreSQL databases using Java code. -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using - `hashtext()` partitioning, with each worker running its own batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. +This example uses the Aurora DSQL JDBC Connector to handle IAM authentication automatically. ## About the code example -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. - -The parallel pattern partitions rows across worker threads using -`abs(hashtext(CAST(id AS text))) % num_workers = worker_id`, ensuring workers operate on disjoint sets -of rows and avoid OCC conflicts with each other. +The example demonstrates a flexible connection approach that works for both admin and non-admin users: -⚠️ **Important** +* When connecting as an **admin user**, the example uses the `public` schema. +* When connecting as a **non-admin user**, the example uses a custom `myschema` schema. -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. +The code automatically detects the user type and adjusts its behavior accordingly. -## Prerequisites +## ⚠️ Important -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Java Development Kit (JDK) 17 or later. -- Gradle (the wrapper is included in this project). -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. +* Running this code might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the + minimum permissions required to perform the task. For more information, see + [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see + [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). -## Set up the test table +## TLS connection configuration -Before running the examples, create and populate the test table: +This example uses direct TLS connections where supported, and verifies the server certificate is trusted. Verified SSL +connections should be used where possible to ensure data security during transmission. -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` +* Driver versions following the release of PostgreSQL 17 support direct TLS connections, bypassing the traditional + PostgreSQL connection preamble +* Direct TLS connections provide improved connection performance and enhanced security +* Not all PostgreSQL drivers support direct TLS connections yet, or only in recent versions following PostgreSQL 17 +* Ensure your installed driver version supports direct TLS negotiation, or use a version that is at least as recent as + the one used in this sample +* If your driver doesn't support direct TLS connections, you may need to use the traditional preamble connection instead ## Run the example -Set environment variables for your cluster: +### Prerequisites + +* You must have an AWS account, and have your default credentials and AWS Region + configured as described in the + [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) + guide. +* Java Development Kit (JDK): Ensure you have JDK 17+ installed. + + _To verify the java is installed, you can run_ + ```bash + java -version + +* Gradle: This example uses the Gradle wrapper included in the repository, so no separate installation is required. +* AWS SDK: Ensure that you setup the latest version of the AWS Java SDK [official website](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/setup.html) +* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) + guide. +* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` + schema. See the + [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) + guide. + +### Run the code + +The example demonstrates the following operations: + +- Opening a connection to an Aurora DSQL cluster +- Creating a table +- Inserting and querying data + +The example is designed to work with both admin and non-admin users: + +- When run as an admin user, it uses the `public` schema +- When run as a non-admin user, it uses the `myschema` schema + +**Note:** running the example will use actual resources in your AWS account and may incur charges. + +Set environment variables for your cluster details: ```bash # e.g. "admin" -export CLUSTER_USER="admin" - +export CLUSTER_USER="" + # e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" +export CLUSTER_ENDPOINT="" ``` -Build and run: +Run the example: ```bash -./gradlew run --args="--endpoint $CLUSTER_ENDPOINT --user $CLUSTER_USER" +./gradlew run ``` -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel worker threads | - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: +Run the tests: ```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" +./gradlew test ``` + +The example contains comments explaining the code and the operations being performed. + ## Additional resources -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-java-connector) -- [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) +* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) +* [Amazon Aurora DSQL JDBC Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/java/jdbc) +* [pgJDBC Documentation](https://jdbc.postgresql.org/documentation/) +* [AWS SDK for Java Documentation](https://docs.aws.amazon.com/sdk-for-java/) --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 + +SPDX-License-Identifier: Apache-2.0 diff --git a/javascript/batch_operations/.gitignore b/javascript/batch_operations/.gitignore new file mode 100644 index 00000000..114d0b6d --- /dev/null +++ b/javascript/batch_operations/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/build +/dist diff --git a/javascript/batch_operations/README.md b/javascript/batch_operations/README.md new file mode 100644 index 00000000..ecbb5599 --- /dev/null +++ b/javascript/batch_operations/README.md @@ -0,0 +1,121 @@ +# Batch Operations with node-postgres + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[node-postgres](https://node-postgres.com/) with the +[Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) for automatic +IAM authentication. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple concurrent async workers each process a disjoint partition of the dataset using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across workers using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Node.js 18 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up + +Install the required packages: + +```bash +npm install +``` + +## Set up the test table + +Before running the examples, create and populate the test table. Aurora DSQL uses IAM authentication, +so you need to generate a fresh auth token each time you connect with `psql`: + +```bash +export CLUSTER_ENDPOINT="" +export CLUSTER_REGION="" + +# Generate a fresh auth token (expires in 3600 seconds) +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -f batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Run the demo: + +```bash +node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel async workers | + +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +# Generate a fresh token if the previous one expired +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) +- [node-postgres Documentation](https://node-postgres.com/) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/javascript/batch_operations/batch_test_setup.sql b/javascript/batch_operations/batch_test_setup.sql new file mode 100644 index 00000000..3ac6da4b --- /dev/null +++ b/javascript/batch_operations/batch_test_setup.sql @@ -0,0 +1,214 @@ +-- ============================================================================= +-- Sample table setup for Aurora DSQL batch operations +-- +-- This script creates a test table and populates it with 25,000 rows of sample +-- data distributed across 5 categories (~5,000 rows each). Each category is +-- used by a different demo operation: +-- +-- electronics → Sequential batch DELETE +-- clothing → Sequential batch UPDATE +-- food → Parallel batch DELETE +-- books → Parallel batch UPDATE +-- toys → (unused, remains in table) +-- +-- Aurora DSQL limits each transaction to 3,000 row mutations, so we insert +-- in batches of 1,000 rows. Run each INSERT as a separate transaction. +-- ============================================================================= + +-- Drop the table if it already exists +DROP TABLE IF EXISTS batch_test; + +-- Create the sample table. +-- Uses UUID primary key with gen_random_uuid() to minimize OCC contention, +-- following Aurora DSQL best practice of random keys. +CREATE TABLE batch_test ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + value NUMERIC(10,2), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create an asynchronous index on the category column. +CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); + +-- ============================================================================= +-- Populate the table with 25,000 rows of test data (25 batches of 1,000). +-- Each INSERT is 1,000 rows — run each as a separate transaction. +-- ============================================================================= + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); diff --git a/javascript/batch_operations/package-lock.json b/javascript/batch_operations/package-lock.json new file mode 100644 index 00000000..369c8e09 --- /dev/null +++ b/javascript/batch_operations/package-lock.json @@ -0,0 +1,5430 @@ +{ + "name": "dsql-batch-operations", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsql-batch-operations", + "version": "1.0.0", + "license": "MIT-0", + "dependencies": { + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "pg": "^8.13.0" + }, + "devDependencies": { + "jest": "^30.3.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cognito-identity": { + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1011.0.tgz", + "integrity": "sha512-+B5jc1KuRLO7hNVCbA+BySNNe8+aclgVY+K+uwhrvbumMxK4RFi4VWMPliPq2u7HA4BwP/FhqjNu6zNjR/Y3Zg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-cognito-identity": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.13.tgz", + "integrity": "sha512-WZnIK8NPX+4OXkpVoNmUS+Ya1osqjszUsDqFEz97+a/LD5K012np9iR/eWEC43btx8zQjyRIK8kyiwbh8SiHzg==", + "dependencies": { + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-providers": { + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1011.0.tgz", + "integrity": "sha512-Ev3ZOqB+vBdg5WoKbyg/FQqXoICM+H5OoL7qPqbiww3LGIVuKbxbTBUtxhOynnyiKt1yD6rIP6KxL3XjXxTAXg==", + "dependencies": { + "@aws-sdk/client-cognito-identity": "3.1011.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.13", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dsql-signer": { + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/dsql-signer/-/dsql-signer-3.1011.0.tgz", + "integrity": "sha512-xym/AB8H/2jCP7pj9Iro7UMbXEF/MfJAkHAXPsOG3+ByUjSKS7PN4cguyB4WLqDY0fUKGRkpxdJy9VzRTDXtNw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/util-format-url": "^3.972.8", + "@smithy/config-resolver": "^4.4.11", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "dependencies": { + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", + "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.11.tgz", + "integrity": "sha512-iitV/gZKQMvY9d7ovmyFnFuTHbBAtrmLnvaSb/3X8vOKyevwtpmEtyc8AdhVWZe0pI/1GsHxlEvQeOePFzy7KQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/aurora-dsql-node-postgres-connector": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@aws/aurora-dsql-node-postgres-connector/-/aurora-dsql-node-postgres-connector-0.1.8.tgz", + "integrity": "sha512-R+S7tbIAZysbv1GLitYCNKvy96pcz95VSTwKBtxHrAvlDtozaUeBfJMPw1ZMhev2jGFRNqI25+eHqJ/ASVmAvA==", + "dependencies": { + "@aws-sdk/credential-providers": "^3.894.0", + "@aws-sdk/dsql-signer": "^3.940.0", + "@smithy/types": "^4.11.0" + }, + "peerDependencies": { + "pg": "^8.16.3", + "pg-connection-string": "^2.7.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "dependencies": { + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "dependencies": { + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "dependencies": { + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "dependencies": { + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/javascript/batch_operations/package.json b/javascript/batch_operations/package.json new file mode 100644 index 00000000..93d93056 --- /dev/null +++ b/javascript/batch_operations/package.json @@ -0,0 +1,18 @@ +{ + "name": "dsql-batch-operations", + "version": "1.0.0", + "description": "Batch DELETE and UPDATE operations for Aurora DSQL", + "main": "src/main.js", + "scripts": { + "start": "node src/main.js", + "test": "jest --runInBand --detectOpenHandles --forceExit" + }, + "dependencies": { + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "pg": "^8.13.0" + }, + "devDependencies": { + "jest": "^30.3.0" + }, + "license": "MIT-0" +} diff --git a/javascript/node-postgres/src/batch_operations/batchDelete.js b/javascript/batch_operations/src/batchDelete.js similarity index 100% rename from javascript/node-postgres/src/batch_operations/batchDelete.js rename to javascript/batch_operations/src/batchDelete.js diff --git a/javascript/node-postgres/src/batch_operations/batchUpdate.js b/javascript/batch_operations/src/batchUpdate.js similarity index 100% rename from javascript/node-postgres/src/batch_operations/batchUpdate.js rename to javascript/batch_operations/src/batchUpdate.js diff --git a/javascript/node-postgres/src/batch_operations/main.js b/javascript/batch_operations/src/main.js similarity index 100% rename from javascript/node-postgres/src/batch_operations/main.js rename to javascript/batch_operations/src/main.js diff --git a/javascript/node-postgres/src/batch_operations/occRetry.js b/javascript/batch_operations/src/occRetry.js similarity index 100% rename from javascript/node-postgres/src/batch_operations/occRetry.js rename to javascript/batch_operations/src/occRetry.js diff --git a/javascript/node-postgres/test/batch_operations/batch_operations.test.js b/javascript/batch_operations/test/batch_operations.test.js similarity index 76% rename from javascript/node-postgres/test/batch_operations/batch_operations.test.js rename to javascript/batch_operations/test/batch_operations.test.js index 85e66c13..706b8274 100644 --- a/javascript/node-postgres/test/batch_operations/batch_operations.test.js +++ b/javascript/batch_operations/test/batch_operations.test.js @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -const { main } = require("../../src/batch_operations/main"); +const { main } = require("../src/main"); test("Batch operations demo runs successfully", async () => { await main(); diff --git a/javascript/node-postgres/README.md b/javascript/node-postgres/README.md index 76daf217..0f371762 100644 --- a/javascript/node-postgres/README.md +++ b/javascript/node-postgres/README.md @@ -1,106 +1,102 @@ -# Batch Operations with node-postgres +# Aurora DSQL with node-postgres ## Overview -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[node-postgres](https://node-postgres.com/) with the -[Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) for automatic -IAM authentication. +This code example demonstrates how to use `node-postgres` with Amazon Aurora DSQL. +The example shows you how to connect to an Aurora DSQL cluster and perform basic database operations. -Two patterns are provided: - -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple concurrent async workers each process a disjoint partition of the dataset using - `hashtext()` partitioning, with each worker running its own batch loop. - -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. +Aurora DSQL is a distributed SQL database service that provides high availability and scalability for +your PostgreSQL-compatible applications. `node-postgres` is a popular PostgreSQL adapter for Node.js that allows +you to interact with PostgreSQL databases using JavaScript code. ## About the code example -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. +This example uses the [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/node) which automatically handles IAM token generation for authentication. + +The example demonstrates a flexible connection approach that works for both admin and non-admin users: -The parallel pattern partitions rows across workers using -`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows -and avoid OCC conflicts with each other. +* When connecting as an **admin user**, the example uses the `public` schema and generates an admin authentication token. +* When connecting as a **non-admin user**, the example uses a custom `myschema` schema and generates a standard authentication token. -⚠️ **Important** +The code automatically detects the user type and adjusts its behavior accordingly. -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. +## ⚠️ Important -## Prerequisites +* Running this code might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the + minimum permissions required to perform the task. For more information, see + [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see + [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Node.js 18 or later. -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. +## Run the example -## Set up +### Prerequisites -Install the required packages: +* You must have an AWS account, and have your default credentials and AWS Region + configured as described in the + [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) + guide. +* Node.js: Ensure you have Node.js 18+ installed. ```bash -npm install +node --version ``` -## Set up the test table +It should output something similar to `v18.x` or higher. -Before running the examples, create and populate the test table: +* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) + guide. +* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` + schema. See the + [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) + guide. -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` +### Run the code -## Run the example +The example demonstrates the following operations: -Set environment variables for your cluster: +- Opening a connection to an Aurora DSQL cluster +- Creating a table +- Inserting and querying data -```bash -# e.g. "admin" -export CLUSTER_USER="admin" +The example is designed to work with both admin and non-admin users: -# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" -``` +- When run as an admin user, it uses the `public` schema +- When run as a non-admin user, it uses the `myschema` schema + +**Note:** running the example will use actual resources in your AWS account and may incur charges. -Run the demo: +Set environment variables for your cluster details: ```bash -node src/main.js --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +# e.g. "admin" +export CLUSTER_USER="" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" ``` -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel async workers | - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: +Run the example: ```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" +npm install +npm test ``` +The example contains comments explaining the code and the operations being performed. + ## Additional resources -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL Node.js Connector](https://github.com/awslabs/aurora-dsql-nodejs-connector) -- [node-postgres Documentation](https://node-postgres.com/) +* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) +* [Aurora DSQL Node.js Connector for node-postgres](https://github.com/awslabs/aurora-dsql-connectors/tree/main/node/node-postgres) +* [node-postgres Documentation](https://node-postgres.com/) + +**Note:** The connector automatically extracts the region from the cluster endpoint and defaults to the `postgres` database. --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/javascript/node-postgres/package-lock.json b/javascript/node-postgres/package-lock.json index 69d1e3d9..879dd6f5 100644 --- a/javascript/node-postgres/package-lock.json +++ b/javascript/node-postgres/package-lock.json @@ -1,17 +1,16 @@ { - "name": "dsql_nodejs_howto", + "name": "dsql-batch-operations", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dsql_nodejs_howto", + "name": "dsql-batch-operations", "version": "1.0.0", - "license": "ISC", + "license": "MIT-0", "dependencies": { "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", - "assert": "2.1.0", - "uuid": "^13.0.0" + "pg": "^8.13.0" }, "devDependencies": { "jest": "^30.3.0" @@ -2919,34 +2918,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/babel-jest": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", @@ -3133,53 +3104,6 @@ "dev": true, "license": "MIT" }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3459,40 +3383,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3503,20 +3393,6 @@ "node": ">=8" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3561,36 +3437,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3750,21 +3596,6 @@ "node": ">=8" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3804,24 +3635,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3842,30 +3655,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3876,19 +3665,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3924,18 +3700,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3953,57 +3717,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4067,24 +3780,9 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4092,18 +3790,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -4124,59 +3810,6 @@ "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4190,21 +3823,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5037,15 +4655,6 @@ "tmpl": "1.0.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5156,51 +4765,6 @@ "node": ">=8" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5382,7 +4946,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -5410,22 +4973,19 @@ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/pg-connection-string": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "license": "ISC", - "peer": true, "engines": { "node": ">=4.0.0" } @@ -5435,7 +4995,6 @@ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", "license": "MIT", - "peer": true, "peerDependencies": { "pg": ">=8.0" } @@ -5444,15 +5003,13 @@ "version": "1.10.3", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", - "peer": true, "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", @@ -5469,7 +5026,6 @@ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", "license": "MIT", - "peer": true, "dependencies": { "split2": "^4.1.0" } @@ -5517,21 +5073,11 @@ "node": ">=8" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -5541,7 +5087,6 @@ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5551,7 +5096,6 @@ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5561,7 +5105,6 @@ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", - "peer": true, "dependencies": { "xtend": "^4.0.0" }, @@ -5654,23 +5197,6 @@ "node": ">=8" } }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5681,23 +5207,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5770,7 +5279,6 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "license": "ISC", - "peer": true, "engines": { "node": ">= 10.x" } @@ -6180,32 +5688,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6247,27 +5729,6 @@ "node": ">= 8" } }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -6389,7 +5850,6 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.4" } diff --git a/javascript/node-postgres/package.json b/javascript/node-postgres/package.json index 30cc24b8..74197c68 100644 --- a/javascript/node-postgres/package.json +++ b/javascript/node-postgres/package.json @@ -1,18 +1,20 @@ { - "name": "dsql-batch-operations", + "name": "dsql_nodejs_howto", "version": "1.0.0", - "description": "Batch DELETE and UPDATE operations for Aurora DSQL", - "main": "src/batch_operations/main.js", + "description": "How To", + "main": "index.js", + "type": "module", "scripts": { - "start": "node src/batch_operations/main.js", - "test": "jest --runInBand --detectOpenHandles --forceExit" + "test": "node --experimental-vm-modules node_modules/.bin/jest --runInBand --detectOpenHandles --forceExit" }, + "author": "", + "license": "ISC", "dependencies": { "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", - "pg": "^8.13.0" + "assert": "2.1.0", + "uuid": "^13.0.0" }, "devDependencies": { "jest": "^30.3.0" - }, - "license": "MIT-0" + } } diff --git a/python/batch_operations/README.md b/python/batch_operations/README.md new file mode 100644 index 00000000..666b8c7e --- /dev/null +++ b/python/batch_operations/README.md @@ -0,0 +1,140 @@ +# Batch Operations with psycopg2 + +## Overview + +This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL +when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses +[psycopg2](https://www.psycopg.org/docs/) with the +[Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic +IAM authentication and connection pooling. + +Two patterns are provided: + +- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), + committing each batch as a separate transaction. +- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using + `hashtext()` partitioning, with each worker running its own batch loop. + +Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. + +## About the code example + +Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, +you must split the work into batches, each committed as a separate transaction. + +The parallel pattern partitions rows across worker threads using +`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows +and avoid OCC conflicts with each other. + +Connection pooling uses `AuroraDSQLThreadedConnectionPool` from the Aurora DSQL Python Connector, which +automatically handles IAM token refresh for long-running batch jobs. + +⚠️ **Important** + +- Running this code might result in charges to your AWS account. +- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. + Design your operations to be idempotent where possible. + +## Prerequisites + +- You must have an AWS account, and have your default credentials and AWS Region configured as described + in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. +- Python 3.8 or later. +- You must have an Aurora DSQL cluster. For information about creating a cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. + +## Set up + +Create and activate a Python virtual environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +Install the required packages: + +```bash +pip install -r requirements.txt +``` + +## Set up the test table + +Before running the examples, create and populate the test table. Run each INSERT as a separate +transaction (each block of 1,000 rows). Aurora DSQL uses IAM authentication, so you need to +generate a fresh auth token each time you connect with `psql`: + +```bash +export CLUSTER_ENDPOINT="" +export CLUSTER_REGION="" + +# Generate a fresh auth token (expires in 3600 seconds) +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -f batch_test_setup.sql +``` + +## Run the example + +Set environment variables for your cluster: + +```bash +# e.g. "admin" +export CLUSTER_USER="admin" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Run the demo: + +```bash +python src/main.py --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +``` + +### Command-line options + +| Option | Default | Description | +|--------|---------|-------------| +| `--endpoint` | (required) | Aurora DSQL cluster endpoint | +| `--user` | `admin` | Database user | +| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | +| `--num-workers` | `4` | Number of parallel worker threads | + +The demo runs the following sequence: + +1. Sequential batch DELETE (all `electronics` rows) +2. Sequential batch UPDATE (`clothing` → `processed`) +3. Parallel batch DELETE (all `food` rows) with N workers +4. Parallel batch UPDATE (`books` → `archived`) with N workers + +## Clean up + +After running the demo, drop the test table to avoid unnecessary storage: + +```bash +# Generate a fresh token if the previous one expired +export PGPASSWORD=$(aws dsql generate-db-connect-admin-auth-token \ + --hostname $CLUSTER_ENDPOINT \ + --region $CLUSTER_REGION \ + --expires-in 3600) + +psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ + -c "DROP TABLE IF EXISTS batch_test;" +``` + +## Additional resources + +- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) +- [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) +- [Psycopg2 Documentation](https://www.psycopg.org/docs/) +- [Batch operations in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/batch-operations.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 diff --git a/python/batch_operations/batch_test_setup.sql b/python/batch_operations/batch_test_setup.sql new file mode 100644 index 00000000..3ac6da4b --- /dev/null +++ b/python/batch_operations/batch_test_setup.sql @@ -0,0 +1,214 @@ +-- ============================================================================= +-- Sample table setup for Aurora DSQL batch operations +-- +-- This script creates a test table and populates it with 25,000 rows of sample +-- data distributed across 5 categories (~5,000 rows each). Each category is +-- used by a different demo operation: +-- +-- electronics → Sequential batch DELETE +-- clothing → Sequential batch UPDATE +-- food → Parallel batch DELETE +-- books → Parallel batch UPDATE +-- toys → (unused, remains in table) +-- +-- Aurora DSQL limits each transaction to 3,000 row mutations, so we insert +-- in batches of 1,000 rows. Run each INSERT as a separate transaction. +-- ============================================================================= + +-- Drop the table if it already exists +DROP TABLE IF EXISTS batch_test; + +-- Create the sample table. +-- Uses UUID primary key with gen_random_uuid() to minimize OCC contention, +-- following Aurora DSQL best practice of random keys. +CREATE TABLE batch_test ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category VARCHAR(50) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', + value NUMERIC(10,2), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create an asynchronous index on the category column. +CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); + +-- ============================================================================= +-- Populate the table with 25,000 rows of test data (25 batches of 1,000). +-- Each INSERT is 1,000 rows — run each as a separate transaction. +-- ============================================================================= + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); + +INSERT INTO batch_test (category, status, value) +SELECT + (ARRAY['electronics', 'clothing', 'food', 'books', 'toys'])[floor(random() * 5 + 1)], + 'active', + round((random() * 1000)::numeric, 2) +FROM generate_series(1, 1000); diff --git a/python/psycopg2/src/batch_operations/batch_delete.py b/python/batch_operations/src/batch_delete.py similarity index 100% rename from python/psycopg2/src/batch_operations/batch_delete.py rename to python/batch_operations/src/batch_delete.py diff --git a/python/psycopg2/src/batch_operations/batch_update.py b/python/batch_operations/src/batch_update.py similarity index 100% rename from python/psycopg2/src/batch_operations/batch_update.py rename to python/batch_operations/src/batch_update.py diff --git a/python/psycopg2/src/batch_operations/main.py b/python/batch_operations/src/main.py similarity index 98% rename from python/psycopg2/src/batch_operations/main.py rename to python/batch_operations/src/main.py index ad1ebd35..af6639fc 100644 --- a/python/psycopg2/src/batch_operations/main.py +++ b/python/batch_operations/src/main.py @@ -53,8 +53,6 @@ def create_pool(endpoint, user, num_workers): conn_params = { "user": user, "host": endpoint, - "sslmode": "verify-full", - "sslrootcert": "./root.pem", } return dsql.AuroraDSQLThreadedConnectionPool( minconn=num_workers, diff --git a/python/psycopg2/src/batch_operations/occ_retry.py b/python/batch_operations/src/occ_retry.py similarity index 100% rename from python/psycopg2/src/batch_operations/occ_retry.py rename to python/batch_operations/src/occ_retry.py diff --git a/python/psycopg2/src/batch_operations/parallel_batch_delete.py b/python/batch_operations/src/parallel_batch_delete.py similarity index 100% rename from python/psycopg2/src/batch_operations/parallel_batch_delete.py rename to python/batch_operations/src/parallel_batch_delete.py diff --git a/python/psycopg2/src/batch_operations/parallel_batch_update.py b/python/batch_operations/src/parallel_batch_update.py similarity index 100% rename from python/psycopg2/src/batch_operations/parallel_batch_update.py rename to python/batch_operations/src/parallel_batch_update.py diff --git a/python/psycopg2/test/batch_operations/test_batch_operations.py b/python/batch_operations/test/test_batch_operations.py similarity index 100% rename from python/psycopg2/test/batch_operations/test_batch_operations.py rename to python/batch_operations/test/test_batch_operations.py diff --git a/python/psycopg2/README.md b/python/psycopg2/README.md index ca273c92..eae3007a 100644 --- a/python/psycopg2/README.md +++ b/python/psycopg2/README.md @@ -1,125 +1,111 @@ -# Batch Operations with psycopg2 +# Aurora DSQL with Psycopg2 ## Overview -This code example demonstrates how to perform batch DELETE and UPDATE operations in Amazon Aurora DSQL -when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses -[psycopg2](https://www.psycopg.org/docs/) with the -[Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic -IAM authentication and connection pooling. +This code example demonstrates how to use Psycopg (version 2) with Amazon Aurora DSQL. The example shows you how to +connect to an Aurora DSQL cluster and perform basic database operations. -Two patterns are provided: +Aurora DSQL is a distributed SQL database service that provides high availability and scalability for +your PostgreSQL-compatible applications. Psycopg2 is a popular PostgreSQL adapter for Python that allows +you to interact with PostgreSQL databases using Python code. -- **Sequential**: A single-threaded loop that processes rows in configurable-size batches (default 1,000), - committing each batch as a separate transaction. -- **Parallel**: Multiple worker threads each process a disjoint partition of the dataset concurrently using - `hashtext()` partitioning, with each worker running its own batch loop. +## About the code example -Both patterns include OCC (Optimistic Concurrency Control) retry logic with exponential backoff. +This example uses the [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/python/connector) which automatically handles IAM token generation for authentication. -## About the code example +The example demonstrates a flexible connection approach that works for both admin and non-admin users: -Aurora DSQL limits each transaction to 3,000 row mutations. To DELETE or UPDATE more than 3,000 rows, -you must split the work into batches, each committed as a separate transaction. +* When connecting as an **admin user**, the example uses the `public` schema and generates an admin authentication token. +* When connecting as a **non-admin user**, the example uses a custom `myschema` schema and generates a standard authentication token. -The parallel pattern partitions rows across worker threads using -`abs(hashtext(id::text)) % num_workers = worker_id`, ensuring workers operate on disjoint sets of rows -and avoid OCC conflicts with each other. +The code automatically detects the user type and adjusts its behavior accordingly. -Connection pooling uses `AuroraDSQLThreadedConnectionPool` from the Aurora DSQL Python Connector, which -automatically handles IAM token refresh for long-running batch jobs. +## ⚠️ Important -⚠️ **Important** +* Running this code might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the + minimum permissions required to perform the task. For more information, see + [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see + [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). -- Running this code might result in charges to your AWS account. -- Each batch is a separate transaction. A failure mid-way leaves the dataset partially modified. - Design your operations to be idempotent where possible. +## Run the example -## Prerequisites +### Prerequisites -- You must have an AWS account, and have your default credentials and AWS Region configured as described - in the [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html) guide. -- Python 3.8 or later. -- You must have an Aurora DSQL cluster. For information about creating a cluster, see the - [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) guide. +* You must have an AWS account, and have your default credentials and AWS Region + configured as described in the + [Globally configuring AWS SDKs and tools](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html) + guide. +* [Python 3.8.0](https://www.python.org/) or later. +* You must have an Aurora DSQL cluster. For information about creating an Aurora DSQL cluster, see the + [Getting started with Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/getting-started.html) + guide. +* If connecting as a non-admin user, ensure the user is linked to an IAM role and is granted access to the `myschema` + schema. See the + [Using database roles with IAM roles](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html) + guide. -## Set up +### Set up environment for examples -Create and activate a Python virtual environment: +1. Create and activate a Python virtual environment: ```bash python3 -m venv .venv -source .venv/bin/activate +source .venv/bin/activate # Linux, macOS +# or +.venv\Scripts\activate # Windows ``` -Install the required packages: +2. Install the required packages for running the examples: ```bash pip install -r requirements.txt ``` -## Set up the test table +### Run the code -Before running the examples, create and populate the test table. Run each INSERT as a separate -transaction (each block of 1,000 rows): +The example demonstrates the following operations: -```bash -export CLUSTER_ENDPOINT="" -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full" \ - -f ../../sql/batch_test_setup.sql -``` +- Opening a connection to an Aurora DSQL cluster +- Creating a table +- Inserting and querying data -## Run the example +The example is designed to work with both admin and non-admin users: + +- When run as an admin user, it uses the `public` schema +- When run as a non-admin user, it uses the `myschema` schema -Set environment variables for your cluster: +**Note:** running the example will use actual resources in your AWS account and may incur charges. + +Set environment variables for your cluster details: ```bash # e.g. "admin" -export CLUSTER_USER="admin" +export CLUSTER_USER="" # e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" -export CLUSTER_ENDPOINT="" +export CLUSTER_ENDPOINT="" ``` -Run the demo: +Run the example: ```bash -python src/main.py --endpoint "$CLUSTER_ENDPOINT" --user "$CLUSTER_USER" +python src/example_preferred.py ``` -### Command-line options - -| Option | Default | Description | -|--------|---------|-------------| -| `--endpoint` | (required) | Aurora DSQL cluster endpoint | -| `--user` | `admin` | Database user | -| `--batch-size` | `1000` | Rows per batch transaction (must be < 3000) | -| `--num-workers` | `4` | Number of parallel worker threads | - -The demo runs the following sequence: - -1. Sequential batch DELETE (all `electronics` rows) -2. Sequential batch UPDATE (`clothing` → `processed`) -3. Parallel batch DELETE (all `food` rows) with N workers -4. Parallel batch UPDATE (`books` → `archived`) with N workers - -## Clean up - -After running the demo, drop the test table to avoid unnecessary storage: - -```bash -psql "host=$CLUSTER_ENDPOINT dbname=postgres user=admin sslmode=verify-full sslrootcert=system" \ - -c "DROP TABLE IF EXISTS batch_test;" -``` +The example contains comments explaining the code and the operations being performed. ## Additional resources -- [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/) -- [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) -- [Psycopg2 Documentation](https://www.psycopg.org/docs/) -- [Batch operations in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/batch-operations.html) +* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) +* [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-connectors/tree/main/python/connector) +* [Psycopg2 Documentation](https://www.psycopg.org/docs/) + +**Note:** The connector automatically extracts the region from the cluster endpoint, defaults to the `postgres` database, and handles SSL configuration. --- Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: MIT-0 diff --git a/python/psycopg2/requirements.txt b/python/psycopg2/requirements.txt index c1e06f88..74894da1 100644 --- a/python/psycopg2/requirements.txt +++ b/python/psycopg2/requirements.txt @@ -1,3 +1,3 @@ aurora-dsql-python-connector psycopg2-binary>=2.9 -pytest>=7.0 +pytest>=8 From 94efc6831a7d703fc51e58868a2e2233c681a646 Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Thu, 19 Mar 2026 16:43:58 -0700 Subject: [PATCH 7/8] Add post-verification to batch operations and update integration tests - Add SELECT COUNT(*) post-check after each batch loop to verify all matching rows were processed (sequential and parallel, all 3 languages) - Update integration tests to seed data via psql -f batch_test_setup.sql - Add connect_timeout to Python pool creation for IPv6 fallback --- java/batch_operations/batch_test_setup.sql | 2 +- .../dsql/batch_operations/BatchDelete.java | 26 ++++++++ .../dsql/batch_operations/BatchUpdate.java | 26 ++++++++ .../dsql/batch_operations/MainTest.java | 40 +++++++++++ .../batch_operations/batch_test_setup.sql | 2 +- .../batch_operations/src/batchDelete.js | 25 +++++++ .../batch_operations/src/batchUpdate.js | 25 +++++++ .../test/batch_operations.test.js | 38 ++++++++++- python/batch_operations/README.md | 2 +- python/batch_operations/batch_test_setup.sql | 2 +- python/batch_operations/src/batch_delete.py | 12 ++++ python/batch_operations/src/batch_update.py | 12 ++++ python/batch_operations/src/main.py | 1 + .../src/parallel_batch_delete.py | 13 ++++ .../src/parallel_batch_update.py | 13 ++++ .../test/test_batch_operations.py | 66 +++++++++++++++++-- 16 files changed, 295 insertions(+), 10 deletions(-) diff --git a/java/batch_operations/batch_test_setup.sql b/java/batch_operations/batch_test_setup.sql index 3ac6da4b..3243506d 100644 --- a/java/batch_operations/batch_test_setup.sql +++ b/java/batch_operations/batch_test_setup.sql @@ -30,7 +30,7 @@ CREATE TABLE batch_test ( updated_at TIMESTAMPTZ DEFAULT NOW() ); --- Create an asynchronous index on the category column. +-- Create an index on the category column. CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); -- ============================================================================= diff --git a/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java index d2b2cef4..a457d732 100644 --- a/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java +++ b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchDelete.java @@ -53,6 +53,19 @@ public static int batchDelete( if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } + + // Post-verification: ensure no matching rows remain + try (Connection conn = pool.getConnection(); + Statement stmt = conn.createStatement(); + var rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE " + condition)) { + if (rs.next()) { + int remaining = rs.getInt(1); + if (remaining > 0) { + System.out.println("WARNING: " + remaining + " rows still match condition after deleting " + totalDeleted + " rows"); + } + } + } + return totalDeleted; } @@ -123,6 +136,19 @@ public static int parallelBatchDelete( } System.out.println("Parallel delete complete: " + total + " rows deleted by " + numWorkers + " workers"); + + // Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + try (Connection conn = pool.getConnection(); + Statement stmt = conn.createStatement(); + var rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE " + condition)) { + if (rs.next()) { + int remaining = rs.getInt(1); + if (remaining > 0) { + System.out.println("WARNING: " + remaining + " rows still match condition after deleting " + total + " rows"); + } + } + } + return total; } diff --git a/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java index df906e26..1d7bf365 100644 --- a/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java +++ b/java/batch_operations/src/main/java/com/example/dsql/batch_operations/BatchUpdate.java @@ -54,6 +54,19 @@ public static int batchUpdate( if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) throw e; } } + + // Post-verification: ensure no matching rows remain + try (Connection conn = pool.getConnection(); + Statement stmt = conn.createStatement(); + var rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE " + condition)) { + if (rs.next()) { + int remaining = rs.getInt(1); + if (remaining > 0) { + System.out.println("WARNING: " + remaining + " rows still match condition after updating " + totalUpdated + " rows"); + } + } + } + return totalUpdated; } @@ -125,6 +138,19 @@ public static int parallelBatchUpdate( } System.out.println("Parallel update complete: " + total + " rows updated by " + numWorkers + " workers"); + + // Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + try (Connection conn = pool.getConnection(); + Statement stmt = conn.createStatement(); + var rs = stmt.executeQuery("SELECT COUNT(*) FROM " + table + " WHERE " + condition)) { + if (rs.next()) { + int remaining = rs.getInt(1); + if (remaining > 0) { + System.out.println("WARNING: " + remaining + " rows still match condition after updating " + total + " rows"); + } + } + } + return total; } diff --git a/java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java b/java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java index 60b0ac8b..d93b25e6 100644 --- a/java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java +++ b/java/batch_operations/src/test/java/com/example/dsql/batch_operations/MainTest.java @@ -5,9 +5,49 @@ import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class MainTest { + + private static String extractRegion(String endpoint) { + Matcher m = Pattern.compile("\\.dsql\\.([^.]+)\\.on\\.aws").matcher(endpoint); + assertTrue(m.find(), "Cannot extract region from endpoint: " + endpoint); + return m.group(1); + } + + @BeforeAll + static void seedDatabase() throws Exception { + String endpoint = System.getenv("CLUSTER_ENDPOINT"); + assertNotNull(endpoint, "CLUSTER_ENDPOINT environment variable is required"); + String user = System.getenv().getOrDefault("CLUSTER_USER", "admin"); + String region = extractRegion(endpoint); + + // Generate auth token + Process tokenProc = new ProcessBuilder( + "aws", "dsql", "generate-db-connect-admin-auth-token", + "--hostname", endpoint, "--region", region, "--expires-in", "3600" + ).redirectErrorStream(true).start(); + String token = new String(tokenProc.getInputStream().readAllBytes()).trim(); + assertEquals(0, tokenProc.waitFor(), "Failed to generate auth token"); + + // Run setup SQL via psql + ProcessBuilder pb = new ProcessBuilder( + "psql", + "host=" + endpoint + " dbname=postgres user=" + user + " sslmode=verify-full sslrootcert=system connect_timeout=10", + "-f", "batch_test_setup.sql" + ); + Map env = pb.environment(); + env.put("PGPASSWORD", token); + pb.inheritIO(); + Process psql = pb.start(); + assertEquals(0, psql.waitFor(), "psql batch_test_setup.sql failed"); + } + @Test public void testBatchOperations() { assertAll(() -> Main.main(new String[]{ diff --git a/javascript/batch_operations/batch_test_setup.sql b/javascript/batch_operations/batch_test_setup.sql index 3ac6da4b..3243506d 100644 --- a/javascript/batch_operations/batch_test_setup.sql +++ b/javascript/batch_operations/batch_test_setup.sql @@ -30,7 +30,7 @@ CREATE TABLE batch_test ( updated_at TIMESTAMPTZ DEFAULT NOW() ); --- Create an asynchronous index on the category column. +-- Create an index on the category column. CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); -- ============================================================================= diff --git a/javascript/batch_operations/src/batchDelete.js b/javascript/batch_operations/src/batchDelete.js index 6f94da15..fb1f580c 100644 --- a/javascript/batch_operations/src/batchDelete.js +++ b/javascript/batch_operations/src/batchDelete.js @@ -72,6 +72,18 @@ async function batchDelete(pool, table, condition, batchSize = 1000, maxRetries } } + // Post-verification: ensure no matching rows remain + const verifyClient = await pool.connect(); + try { + const res = await verifyClient.query(`SELECT COUNT(*) FROM ${table} WHERE ${condition}`); + const remaining = parseInt(res.rows[0].count, 10); + if (remaining > 0) { + console.log(`WARNING: ${remaining} rows still match condition after deleting ${totalDeleted} rows`); + } + } finally { + verifyClient.release(); + } + return totalDeleted; } @@ -144,6 +156,19 @@ async function parallelBatchDelete( const results = await Promise.all(workers); const total = results.reduce((sum, n) => sum + n, 0); console.log(`Parallel delete complete: ${total} rows deleted by ${numWorkers} workers`); + + // Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + const verifyClient = await pool.connect(); + try { + const res = await verifyClient.query(`SELECT COUNT(*) FROM ${table} WHERE ${condition}`); + const remaining = parseInt(res.rows[0].count, 10); + if (remaining > 0) { + console.log(`WARNING: ${remaining} rows still match condition after deleting ${total} rows`); + } + } finally { + verifyClient.release(); + } + return total; } diff --git a/javascript/batch_operations/src/batchUpdate.js b/javascript/batch_operations/src/batchUpdate.js index ab4bf023..00b709c9 100644 --- a/javascript/batch_operations/src/batchUpdate.js +++ b/javascript/batch_operations/src/batchUpdate.js @@ -78,6 +78,18 @@ async function batchUpdate(pool, table, setClause, condition, batchSize = 1000, } } + // Post-verification: ensure no matching rows remain + const verifyClient = await pool.connect(); + try { + const res = await verifyClient.query(`SELECT COUNT(*) FROM ${table} WHERE ${condition}`); + const remaining = parseInt(res.rows[0].count, 10); + if (remaining > 0) { + console.log(`WARNING: ${remaining} rows still match condition after updating ${totalUpdated} rows`); + } + } finally { + verifyClient.release(); + } + return totalUpdated; } @@ -156,6 +168,19 @@ async function parallelBatchUpdate( const results = await Promise.all(workers); const total = results.reduce((sum, n) => sum + n, 0); console.log(`Parallel update complete: ${total} rows updated by ${numWorkers} workers`); + + // Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + const verifyClient = await pool.connect(); + try { + const res = await verifyClient.query(`SELECT COUNT(*) FROM ${table} WHERE ${condition}`); + const remaining = parseInt(res.rows[0].count, 10); + if (remaining > 0) { + console.log(`WARNING: ${remaining} rows still match condition after updating ${total} rows`); + } + } finally { + verifyClient.release(); + } + return total; } diff --git a/javascript/batch_operations/test/batch_operations.test.js b/javascript/batch_operations/test/batch_operations.test.js index 706b8274..004059ac 100644 --- a/javascript/batch_operations/test/batch_operations.test.js +++ b/javascript/batch_operations/test/batch_operations.test.js @@ -1,8 +1,44 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -const { main } = require("../src/main"); +const path = require("path"); +const { execSync } = require("child_process"); + +const SETUP_SQL_PATH = path.join(__dirname, "..", "batch_test_setup.sql"); + +function extractRegion(endpoint) { + const m = endpoint.match(/\.dsql\.([^.]+)\.on\.aws/); + if (!m) throw new Error(`Cannot extract region from endpoint: ${endpoint}`); + return m[1]; +} + +function seedDatabase() { + const endpoint = process.env.CLUSTER_ENDPOINT; + if (!endpoint) throw new Error("CLUSTER_ENDPOINT environment variable is required"); + const user = process.env.CLUSTER_USER || "admin"; + const region = extractRegion(endpoint); + + const token = execSync( + `aws dsql generate-db-connect-admin-auth-token --hostname ${endpoint} --region ${region} --expires-in 3600`, + { encoding: "utf8" } + ).trim(); + + execSync( + `psql "host=${endpoint} dbname=postgres user=${user} sslmode=verify-full sslrootcert=system connect_timeout=10" -f ${SETUP_SQL_PATH}`, + { env: { ...process.env, PGPASSWORD: token }, stdio: "inherit" } + ); +} + +beforeAll(() => { + seedDatabase(); +}, 120000); test("Batch operations demo runs successfully", async () => { + // Set process.argv so main()'s parseArgs picks up the endpoint + const endpoint = process.env.CLUSTER_ENDPOINT; + const user = process.env.CLUSTER_USER || "admin"; + process.argv = ["node", "main.js", "--endpoint", endpoint, "--user", user]; + + const { main } = require("../src/main"); await main(); }, 120000); diff --git a/python/batch_operations/README.md b/python/batch_operations/README.md index 666b8c7e..784ec58d 100644 --- a/python/batch_operations/README.md +++ b/python/batch_operations/README.md @@ -6,7 +6,7 @@ This code example demonstrates how to perform batch DELETE and UPDATE operations when working with datasets exceeding the 3,000-row transaction mutation limit. The example uses [psycopg2](https://www.psycopg.org/docs/) with the [Aurora DSQL Python Connector](https://github.com/awslabs/aurora-dsql-python-connector) for automatic -IAM authentication and connection pooling. +IAM authentication, and connection pooling. Two patterns are provided: diff --git a/python/batch_operations/batch_test_setup.sql b/python/batch_operations/batch_test_setup.sql index 3ac6da4b..3243506d 100644 --- a/python/batch_operations/batch_test_setup.sql +++ b/python/batch_operations/batch_test_setup.sql @@ -30,7 +30,7 @@ CREATE TABLE batch_test ( updated_at TIMESTAMPTZ DEFAULT NOW() ); --- Create an asynchronous index on the category column. +-- Create an index on the category column. CREATE INDEX ASYNC idx_batch_test_category ON batch_test (category); -- ============================================================================= diff --git a/python/batch_operations/src/batch_delete.py b/python/batch_operations/src/batch_delete.py index a8766e16..783261f0 100644 --- a/python/batch_operations/src/batch_delete.py +++ b/python/batch_operations/src/batch_delete.py @@ -83,4 +83,16 @@ def delete_batch(conn): finally: pool.putconn(conn) + # Post-verification: ensure no matching rows remain + conn = pool.getconn() + try: + with conn.cursor() as cur: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {condition}") + remaining = cur.fetchone()[0] + conn.commit() + if remaining > 0: + print(f"WARNING: {remaining} rows still match condition after deleting {total_deleted} rows") + finally: + pool.putconn(conn) + return total_deleted diff --git a/python/batch_operations/src/batch_update.py b/python/batch_operations/src/batch_update.py index 4f59656a..7200592a 100644 --- a/python/batch_operations/src/batch_update.py +++ b/python/batch_operations/src/batch_update.py @@ -83,4 +83,16 @@ def update_batch(conn): finally: pool.putconn(conn) + # Post-verification: ensure no matching rows remain + conn = pool.getconn() + try: + with conn.cursor() as cur: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {condition}") + remaining = cur.fetchone()[0] + conn.commit() + if remaining > 0: + print(f"WARNING: {remaining} rows still match condition after updating {total_updated} rows") + finally: + pool.putconn(conn) + return total_updated diff --git a/python/batch_operations/src/main.py b/python/batch_operations/src/main.py index af6639fc..218f4f93 100644 --- a/python/batch_operations/src/main.py +++ b/python/batch_operations/src/main.py @@ -53,6 +53,7 @@ def create_pool(endpoint, user, num_workers): conn_params = { "user": user, "host": endpoint, + "connect_timeout": 10, } return dsql.AuroraDSQLThreadedConnectionPool( minconn=num_workers, diff --git a/python/batch_operations/src/parallel_batch_delete.py b/python/batch_operations/src/parallel_batch_delete.py index 40d70105..5fbb9213 100644 --- a/python/batch_operations/src/parallel_batch_delete.py +++ b/python/batch_operations/src/parallel_batch_delete.py @@ -110,4 +110,17 @@ def delete_batch(conn): total = sum(results) print(f"Parallel delete complete: {total} rows deleted by {num_workers} workers") + + # Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + conn = pool.getconn() + try: + with conn.cursor() as cur: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {condition}") + remaining = cur.fetchone()[0] + conn.commit() + if remaining > 0: + print(f"WARNING: {remaining} rows still match condition after deleting {total} rows") + finally: + pool.putconn(conn) + return total diff --git a/python/batch_operations/src/parallel_batch_update.py b/python/batch_operations/src/parallel_batch_update.py index 6e6849c7..f1037a38 100644 --- a/python/batch_operations/src/parallel_batch_update.py +++ b/python/batch_operations/src/parallel_batch_update.py @@ -111,4 +111,17 @@ def update_batch(conn): total = sum(results) print(f"Parallel update complete: {total} rows updated by {num_workers} workers") + + # Post-verification: ensure no matching rows remain (uses original condition, not partitioned) + conn = pool.getconn() + try: + with conn.cursor() as cur: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {condition}") + remaining = cur.fetchone()[0] + conn.commit() + if remaining > 0: + print(f"WARNING: {remaining} rows still match condition after updating {total} rows") + finally: + pool.putconn(conn) + return total diff --git a/python/batch_operations/test/test_batch_operations.py b/python/batch_operations/test/test_batch_operations.py index efe009e0..d35aee85 100644 --- a/python/batch_operations/test/test_batch_operations.py +++ b/python/batch_operations/test/test_batch_operations.py @@ -1,17 +1,73 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 +import os +import re +import subprocess +import sys + import pytest -from main import main + + +SETUP_SQL_PATH = os.path.join(os.path.dirname(__file__), "..", "batch_test_setup.sql") + + +def get_endpoint(): + ep = os.environ.get("CLUSTER_ENDPOINT") + assert ep, "CLUSTER_ENDPOINT environment variable is required" + return ep + + +def get_user(): + return os.environ.get("CLUSTER_USER", "admin") + + +def extract_region(endpoint): + """Extract AWS region from a DSQL endpoint like 'xxx.dsql.us-east-1.on.aws'.""" + m = re.search(r"\.dsql\.([^.]+)\.on\.aws", endpoint) + assert m, f"Cannot extract region from endpoint: {endpoint}" + return m.group(1) + + +def run_setup_sql(endpoint, user): + """Run batch_test_setup.sql via psql.""" + region = extract_region(endpoint) + token = subprocess.check_output( + [ + "aws", "dsql", "generate-db-connect-admin-auth-token", + "--hostname", endpoint, + "--region", region, + "--expires-in", "3600", + ], + text=True, + ).strip() + + subprocess.run( + [ + "psql", + f"host={endpoint} dbname=postgres user={user} sslmode=verify-full sslrootcert=system connect_timeout=10", + "-f", SETUP_SQL_PATH, + ], + env={**os.environ, "PGPASSWORD": token}, + check=True, + ) def test_batch_operations(): - """Integration test: runs the full batch operations demo against a live cluster. + """Integration test: seeds the table, then runs the full batch operations demo. - Requires CLUSTER_ENDPOINT and CLUSTER_USER environment variables. - Run with: pytest test/ --endpoint --user - Or set env vars and run: pytest test/ + Requires CLUSTER_ENDPOINT environment variable (and optionally CLUSTER_USER). """ + endpoint = get_endpoint() + user = get_user() + + # Seed the test table + run_setup_sql(endpoint, user) + + # Set sys.argv so main()'s argparse picks up the endpoint + sys.argv = ["main.py", "--endpoint", endpoint, "--user", user] + + from main import main try: main() except Exception as e: From b056a3d9d1a45774fd81a4e11a2645d12b907242 Mon Sep 17 00:00:00 2001 From: Raluca Constantin Date: Thu, 19 Mar 2026 16:45:47 -0700 Subject: [PATCH 8/8] Add __pycache__ and *.pyc to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ddd8ac8e..7ac5bd41 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,10 @@ Icon? .VolumeIcon.icns .com.apple.timemachine.donotpresent +# Python +__pycache__/ +*.pyc + # TypeScript build outputs dist/ .venv/