diff --git a/.github/workflows/java-pgjdbc-integ-tests.yml b/.github/workflows/java-pgjdbc-integ-tests.yml index 87706fd1..908c4a09 100644 --- a/.github/workflows/java-pgjdbc-integ-tests.yml +++ b/.github/workflows/java-pgjdbc-integ-tests.yml @@ -40,7 +40,7 @@ jobs: java-version: '17' distribution: 'corretto' architecture: x64 - cache: maven + cache: gradle - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 @@ -54,8 +54,4 @@ jobs: CLUSTER_ENDPOINT: ${{ secrets.JAVA_PGJDBC_CLUSTER_ENDPOINT }} CLUSTER_USER: admin REGION: ${{ secrets.JAVA_PGJDBC_CLUSTER_REGION }} - run: | - mvn validate - mvn initialize - mvn compile - mvn test + run: ./gradlew test diff --git a/.github/workflows/python-asyncpg-integ-tests.yml b/.github/workflows/python-asyncpg-integ-tests.yml new file mode 100644 index 00000000..92fd0d03 --- /dev/null +++ b/.github/workflows/python-asyncpg-integ-tests.yml @@ -0,0 +1,70 @@ +name: Python asyncpg integration tests + +permissions: {} + +on: + push: + branches: [ main ] + paths: + - 'python/asyncpg/**' + - '.github/workflows/python-asyncpg-integ-tests.yml' + pull_request: + branches: [ main ] + paths: + - 'python/asyncpg/**' + - '.github/workflows/python-asyncpg-integ-tests.yml' + # Give us a button to allow running the workflow on demand for testing. + workflow_dispatch: + inputs: + tags: + description: 'Manual Workflow Run' + required: false + type: string + +jobs: + python-asyncpg-integ-test: + runs-on: ubuntu-latest + permissions: + id-token: write # required by aws-actions/configure-aws-credentials + concurrency: + # Ensure only 1 job uses the workflow cluster at a time. + group: ${{ github.workflow }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.PYTHON_IAM_ROLE }} + aws-region: us-east-1 + + - name: Configure and run integration for asyncpg - admin + working-directory: ./python/asyncpg + env: + CLUSTER_USER: "admin" + CLUSTER_ENDPOINT: ${{ secrets.PYTHON_PSYCOPG3_CLUSTER_ENDPOINT }} + run: | + python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install --force-reinstall -r requirements.txt + python3 -c "import boto3; print(boto3.__version__)" + pip list + echo "$GITHUB_WORKSPACE" >> $GITHUB_PATH + wget https://www.amazontrust.com/repository/AmazonRootCA1.pem -O root.pem + pytest diff --git a/java/pgjdbc/README.md b/java/pgjdbc/README.md index 2d38e23a..c1241584 100644 --- a/java/pgjdbc/README.md +++ b/java/pgjdbc/README.md @@ -56,9 +56,7 @@ connections should be used where possible to ensure data security during transmi ```bash java -version -* Build Tool (Maven or Gradle) - - _Maven_: Ensure Maven is installed if that is your preferred option. You can download it from the [official website](https://maven.apache.org/download.cgi). - - _Gradle_: Ensure Gradle is installed if that is your preferred option. You can download it from the [official website](https://gradle.org/install/). +* 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) @@ -95,18 +93,15 @@ export CLUSTER_ENDPOINT="" Run the example: - - _Maven_: - - ```bash - mvn compile - mvn test - ``` +```bash +./gradlew run +``` - - _Gradle_: +Run the tests: - ```bash - gradle run - ``` +```bash +./gradlew test +``` The example contains comments explaining the code and the operations being performed. @@ -122,4 +117,4 @@ The example contains comments explaining the code and the operations being perfo Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 +SPDX-License-Identifier: Apache-2.0 diff --git a/java/pgjdbc/build.gradle.kts b/java/pgjdbc/build.gradle.kts index ac2190f5..d456ab1e 100644 --- a/java/pgjdbc/build.gradle.kts +++ b/java/pgjdbc/build.gradle.kts @@ -1,3 +1,6 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { @@ -6,10 +9,10 @@ plugins { } application { - mainClass = "org.example.Example" + mainClass = "software.amazon.dsql.examples.ExamplePreferred" } -group = "org.example" +group = "software.amazon.dsql.examples" version = "1.0-SNAPSHOT" repositories { @@ -17,7 +20,11 @@ repositories { } dependencies { + implementation("com.zaxxer:HikariCP:5.1.0") implementation("software.amazon.dsql:aurora-dsql-jdbc-connector:1.2.0") + // AWS SDK dependencies for SDK-only example (ExampleWithNoConnector) + implementation("software.amazon.awssdk:dsql:2.31.32") + implementation("org.postgresql:postgresql:42.7.7") testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") @@ -41,4 +48,4 @@ tasks.withType { tasks.withType { this.enableAssertions = true -} \ No newline at end of file +} diff --git a/java/pgjdbc/gradle/wrapper/gradle-wrapper.jar b/java/pgjdbc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..8bdaf60c Binary files /dev/null and b/java/pgjdbc/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java/pgjdbc/gradle/wrapper/gradle-wrapper.properties b/java/pgjdbc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..2e111328 --- /dev/null +++ b/java/pgjdbc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/pgjdbc/gradlew b/java/pgjdbc/gradlew new file mode 100755 index 00000000..adff685a --- /dev/null +++ b/java/pgjdbc/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/HEAD/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/pgjdbc/gradlew.bat b/java/pgjdbc/gradlew.bat new file mode 100644 index 00000000..e509b2dd --- /dev/null +++ b/java/pgjdbc/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/pgjdbc/pom.xml b/java/pgjdbc/pom.xml deleted file mode 100644 index 7cd6d277..00000000 --- a/java/pgjdbc/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - 4.0.0 - - org.example - AuroraDSQLExample - 1.0-SNAPSHOT - - - 17 - UTF-8 - org.example.ExamplePreferred - 1.2.0 - 5.1.0 - 2.31.32 - 5.10.0 - false - - - - - com.zaxxer - HikariCP - ${hikaricp.version} - - - software.amazon.dsql - aurora-dsql-jdbc-connector - ${connector.version} - - - - software.amazon.awssdk - dsql - ${aws.sdk.version} - - - org.postgresql - postgresql - 42.7.7 - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M5 - - - org.apache.maven.plugins - maven-jar-plugin - 3.2.0 - - - - true - ${mainClass} - - - - - - - maven-assembly-plugin - - - - ${mainClass} - - - - jar-with-dependencies - - - - - - - - local-maven-repository - file://${local.repository.folder} - - true - - - true - - - - - diff --git a/java/pgjdbc/settings.gradle.kts b/java/pgjdbc/settings.gradle.kts index a5687556..12f5333b 100644 --- a/java/pgjdbc/settings.gradle.kts +++ b/java/pgjdbc/settings.gradle.kts @@ -1,2 +1,4 @@ -rootProject.name = "DSQLPGJDBCPetClinicExample" +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +rootProject.name = "DSQLPGJDBCExample" diff --git a/java/pgjdbc/src/main/java/org/example/ExamplePreferred.java b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/ExamplePreferred.java similarity index 97% rename from java/pgjdbc/src/main/java/org/example/ExamplePreferred.java rename to java/pgjdbc/src/main/java/software/amazon/dsql/examples/ExamplePreferred.java index 07af01a4..cb86e348 100644 --- a/java/pgjdbc/src/main/java/org/example/ExamplePreferred.java +++ b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/ExamplePreferred.java @@ -1,4 +1,9 @@ -package org.example; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; diff --git a/java/pgjdbc/src/main/java/org/example/alternatives/README.md b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/README.md similarity index 87% rename from java/pgjdbc/src/main/java/org/example/alternatives/README.md rename to java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/README.md index 9d365e3c..89ad4ee4 100644 --- a/java/pgjdbc/src/main/java/org/example/alternatives/README.md +++ b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/README.md @@ -19,4 +19,4 @@ The connector + pool combination handles this automatically: ### `no_connection_pool/` Examples without pooling: - `ExampleWithNoConnectionPool.java` - Single connection with connector -- `ExampleWithNoConnector.java` - SDK-only, for environments where the connector cannot be used (requires manual token management) +- `ExampleWithNoConnector.java` - Single connection without connector (uses AWS SDK directly) diff --git a/java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java similarity index 94% rename from java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java rename to java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java index 7aee8da5..df063bae 100644 --- a/java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java +++ b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPool.java @@ -1,4 +1,9 @@ -package org.example.alternatives.no_connection_pool; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples.alternatives.no_connection_pool; import java.sql.Connection; diff --git a/java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnector.java b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnector.java similarity index 96% rename from java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnector.java rename to java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnector.java index 50f6edfb..709dcb25 100644 --- a/java/pgjdbc/src/main/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnector.java +++ b/java/pgjdbc/src/main/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnector.java @@ -1,4 +1,9 @@ -package org.example.alternatives.no_connection_pool; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples.alternatives.no_connection_pool; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.services.dsql.DsqlUtilities; diff --git a/java/pgjdbc/src/test/java/org/example/ExamplePreferredTest.java b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/ExamplePreferredTest.java similarity index 60% rename from java/pgjdbc/src/test/java/org/example/ExamplePreferredTest.java rename to java/pgjdbc/src/test/java/software/amazon/dsql/examples/ExamplePreferredTest.java index 3ae0461b..666d91fa 100644 --- a/java/pgjdbc/src/test/java/org/example/ExamplePreferredTest.java +++ b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/ExamplePreferredTest.java @@ -1,4 +1,9 @@ -package org.example; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples; import static org.junit.jupiter.api.Assertions.*; @@ -9,4 +14,4 @@ public class ExamplePreferredTest { public void testExamplePreferred() { assertAll(() -> ExamplePreferred.main(new String[]{})); } -} \ No newline at end of file +} diff --git a/java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java similarity index 59% rename from java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java rename to java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java index 73ff6028..d3b1cfb1 100644 --- a/java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java +++ b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectionPoolTest.java @@ -1,4 +1,9 @@ -package org.example.alternatives.no_connection_pool; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples.alternatives.no_connection_pool; import static org.junit.jupiter.api.Assertions.*; diff --git a/java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java similarity index 58% rename from java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java rename to java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java index 6a641ae4..3f52ac75 100644 --- a/java/pgjdbc/src/test/java/org/example/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java +++ b/java/pgjdbc/src/test/java/software/amazon/dsql/examples/alternatives/no_connection_pool/ExampleWithNoConnectorTest.java @@ -1,4 +1,9 @@ -package org.example.alternatives.no_connection_pool; +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.dsql.examples.alternatives.no_connection_pool; import static org.junit.jupiter.api.Assertions.*; diff --git a/python/asyncpg/README.md b/python/asyncpg/README.md new file mode 100644 index 00000000..078f8ae9 --- /dev/null +++ b/python/asyncpg/README.md @@ -0,0 +1,159 @@ +# Aurora DSQL with asyncpg + +This example demonstrates how to use the Aurora DSQL Python Connector with asyncpg to connect to Amazon Aurora DSQL clusters and perform basic database operations. + +Aurora DSQL is a distributed SQL database service that provides high availability and scalability for +your PostgreSQL-compatible applications. +Asyncpg is a popular PostgreSQL database library for Python that allows +you to interact with PostgreSQL databases using Python code. + +## 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 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 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). + + +## 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 + + +### 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.10.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. + +### Download the Amazon root certificate from the official trust store + +Download the Amazon root certificate from the official trust store: + +``` +wget https://www.amazontrust.com/repository/AmazonRootCA1.pem -O root.pem +``` + +### Set up environment for examples + +1. Create and activate a Python virtual environment: + +```bash +python3 -m venv .venv +source .venv/bin/activate # Linux, macOS +# or +.venv\Scripts\activate # Windows +``` + +2. Install the required packages for running the examples: + +```bash +pip install -e . + +# Install optional dependencies for tests +pip install -e ".[test]" +``` + +### 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. + +The connetion pool examples demonstrate: +- Creating a connection pool for Aurora DSQL +- Using async context managers for connection management +- Performing database operations through the pool +- Running multiple concurrent database operations +- Using asyncio.gather() for parallel execution +- Proper resource management with connection pools + + +Set environment variables for your cluster details: + +```bash +# e.g. "admin" +export CLUSTER_USER="" + +# e.g. "foo0bar1baz2quux3quuux4.dsql.us-east-1.on.aws" +export CLUSTER_ENDPOINT="" +``` + +Run the example: + +```bash +# Run example directly +python src/example.py + +# Run example using pytest +pytest ./test/test_example.py + +# Run all using pytest +pytest ./test +``` + +The example contains comments explaining the code and the operations being performed. + +### Connection defaults + +The connector automatically handles the following parameters: + +| Parameter | Default | Notes | +|-----------|---------|-------| +| `database` | `postgres` | Aurora DSQL's default database | +| `port` | `5432` | Standard PostgreSQL port | +| `region` | Extracted from endpoint | Parsed from `*.dsql..on.aws` | + +You can override any of these defaults by explicitly passing them in your connection parameters. + +## Additional resources + +* [Amazon Aurora DSQL Documentation](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html) +* [Asyncpg Documentation](https://magicstack.github.io/asyncpg/current/) +* [AWS SDK for Python (Boto3) Documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/python/asyncpg/pyproject.toml b/python/asyncpg/pyproject.toml new file mode 100644 index 00000000..ffb5fac2 --- /dev/null +++ b/python/asyncpg/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "aurora-dsql-test-asyncpg" +version = "0.1.1" +description = "A test project that utilizes Aurora DSQL Python Connector with asyncpg to connect to Amazon Aurora DSQL clusters." +readme = "README.md" +license = "Apache-2.0" +authors = [ + {name = "Amazon Web Services", email = "aws-aurora-dsql-feedback@amazon.com"} +] +requires-python = ">=3.10" + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13" +] + +dependencies = [ + "aurora-dsql-python-connector", + "boto3>=1.35.74", + "asyncpg>=0.30.0", +] + +[project.optional-dependencies] + +test = [ + "pytest>=8.0", + "pytest-asyncio>=1.2.0", +] + +[tool.pytest.ini_options] +pythonpath = [ + "src" +] diff --git a/python/asyncpg/requirements.txt b/python/asyncpg/requirements.txt new file mode 100644 index 00000000..413fefbb --- /dev/null +++ b/python/asyncpg/requirements.txt @@ -0,0 +1,4 @@ +aurora-dsql-python-connector +asyncpg>=0.30.0 +pytest>=8 +pytest-asyncio>=1.2.0 diff --git a/python/asyncpg/src/alternatives/README.md b/python/asyncpg/src/alternatives/README.md new file mode 100644 index 00000000..56a07440 --- /dev/null +++ b/python/asyncpg/src/alternatives/README.md @@ -0,0 +1,27 @@ +# Alternative Examples + +The recommended approach is `example_preferred.py` in the parent directory, which uses asyncpg connection pool with the Aurora DSQL Python Connector. + +## Why Connection Pooling with the Connector? + +Aurora DSQL has specific connection characteristics: +- **60-minute max connection lifetime** - connections are terminated after 1 hour +- **15-minute token expiry** - IAM auth tokens must be refreshed +- **Optimized for concurrency** - more concurrent connections with smaller batches yields better throughput + +The connector + pool combination handles this automatically: +- Generates fresh IAM tokens per connection +- Recycles connections before the 60-minute limit +- Reuses warmed connections for better performance + +## Alternatives + +### `pool/` +Other pool configurations: +- `example_with_async_connection_pool.py` - Async pool usage +- `example_with_nonconcurrent_connection_pool.py` - Sequential pool usage + +### `no_connection_pool/` +Examples without pooling: +- `example_with_no_connection_pool.py` - Single connection with connector +- `example_async_with_no_connection_pool.py` - Async single connection with connector diff --git a/python/asyncpg/src/alternatives/__init__.py b/python/asyncpg/src/alternatives/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/asyncpg/src/alternatives/no_connection_pool/__init__.py b/python/asyncpg/src/alternatives/no_connection_pool/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/asyncpg/src/alternatives/no_connection_pool/example_with_no_connection_pool.py b/python/asyncpg/src/alternatives/no_connection_pool/example_with_no_connection_pool.py new file mode 100644 index 00000000..460ad063 --- /dev/null +++ b/python/asyncpg/src/alternatives/no_connection_pool/example_with_no_connection_pool.py @@ -0,0 +1,95 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import asyncio +import os + +import aurora_dsql_asyncpg as dsql + + +async def create_connection(cluster_user, cluster_endpoint): + ssl_cert_path = "./root.pem" + if not os.path.isfile(ssl_cert_path): + raise FileNotFoundError(f"SSL certificate file not found: {ssl_cert_path}") + + conn_params = { + "user": cluster_user, + "host": cluster_endpoint, + "ssl": "verify-full", + "sslrootcert": ssl_cert_path, + } + + # Make a connection to the cluster + conn = await dsql.connect(**conn_params) + + if cluster_user == "admin": + schema = "public" + else: + schema = "myschema" + + try: + await conn.execute(f"SET search_path = {schema};") + except Exception as e: + await conn.close() + raise e + + return conn + + +async def exercise_connection(conn): + await conn.execute( + """ + CREATE TABLE IF NOT EXISTS owner( + id uuid NOT NULL DEFAULT gen_random_uuid(), + name varchar(30) NOT NULL, + city varchar(80) NOT NULL, + telephone varchar(20) DEFAULT NULL, + PRIMARY KEY (id)) + """ + ) + + # Insert some rows + await conn.execute( + "INSERT INTO owner(name, city, telephone) VALUES($1, $2, $3)", + "John Doe", + "Anytown", + "555-555-1999", + ) + + row = await conn.fetchrow("SELECT * FROM owner WHERE name=$1", "John Doe") + + # Verify the result we got is what we inserted before + assert row[0] is not None + assert row[1] == "John Doe" + assert row[2] == "Anytown" + assert row[3] == "555-555-1999" + + # Clean up the table after the example. If we run the example again + # we do not have to worry about data inserted by previous runs + await conn.execute("DELETE FROM owner WHERE name = $1", "John Doe") + + +async def main(): + conn = None + try: + cluster_user = os.environ.get("CLUSTER_USER", None) + assert cluster_user is not None, "CLUSTER_USER environment variable is not set" + + cluster_endpoint = os.environ.get("CLUSTER_ENDPOINT", None) + assert ( + cluster_endpoint is not None + ), "CLUSTER_ENDPOINT environment variable is not set" + + conn = await create_connection(cluster_user, cluster_endpoint) + await exercise_connection(conn) + finally: + if conn is not None: + await conn.close() + + print("Connection exercised successfully") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/asyncpg/src/alternatives/pool/__init__.py b/python/asyncpg/src/alternatives/pool/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/asyncpg/src/alternatives/pool/example_with_nonconcurrent_connection_pool.py b/python/asyncpg/src/alternatives/pool/example_with_nonconcurrent_connection_pool.py new file mode 100644 index 00000000..40e848dc --- /dev/null +++ b/python/asyncpg/src/alternatives/pool/example_with_nonconcurrent_connection_pool.py @@ -0,0 +1,58 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import asyncio +import os + +import aurora_dsql_asyncpg as dsql + + +async def connect_with_pool(cluster_user, cluster_endpoint): + ssl_cert_path = "./root.pem" + if not os.path.isfile(ssl_cert_path): + raise FileNotFoundError(f"SSL certificate file not found: {ssl_cert_path}") + + pool_params = { + "user": cluster_user, + "host": cluster_endpoint, + "ssl": "verify-full", + "sslrootcert": ssl_cert_path, + "min_size": 2, + "max_size": 5, + } + + pool = await dsql.create_pool(**pool_params) + try: + async with pool.acquire() as conn: + result = await conn.fetchval("SELECT 1") + assert result == 1 + finally: + await pool.close() + + +async def main(): + try: + cluster_user = os.environ.get("CLUSTER_USER", None) + assert cluster_user is not None, "CLUSTER_USER environment variable is not set" + + cluster_endpoint = os.environ.get("CLUSTER_ENDPOINT", None) + assert ( + cluster_endpoint is not None + ), "CLUSTER_ENDPOINT environment variable is not set" + + ssl_cert_path = "./root.pem" + if not os.path.isfile(ssl_cert_path): + raise FileNotFoundError(f"SSL certificate file not found: {ssl_cert_path}") + + await connect_with_pool(cluster_user, cluster_endpoint) + + finally: + pass + + print("Pool exercised successfully") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/asyncpg/src/example_preferred.py b/python/asyncpg/src/example_preferred.py new file mode 100644 index 00000000..c4c70ea6 --- /dev/null +++ b/python/asyncpg/src/example_preferred.py @@ -0,0 +1,73 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import asyncio +import os + +import aurora_dsql_asyncpg as dsql + + +async def worker_task(pool, worker_id): + """Simulate concurrent database operations.""" + + async with pool.acquire() as conn: + result = await conn.fetchval("SELECT $1::int", worker_id) + return result + + +async def connect_with_pool_concurrent_connections( + cluster_user, cluster_endpoint +): + + ssl_cert_path = "./root.pem" + if not os.path.isfile(ssl_cert_path): + raise FileNotFoundError(f"SSL certificate file not found: {ssl_cert_path}") + + pool_params = { + "user": cluster_user, + "host": cluster_endpoint, + "ssl": "verify-full", + "sslrootcert": ssl_cert_path, + "min_size": 5, + "max_size": 10, + } + + pool = None + try: + pool = await dsql.create_pool(**pool_params) + # Run multiple concurrent workers + num_workers = 5 + tasks = [worker_task(pool, i) for i in range(num_workers)] + results = await asyncio.gather(*tasks) + for result in results: + print(result) + finally: + if pool is not None: + await pool.close() + + +async def main(): + + try: + cluster_user = os.environ.get("CLUSTER_USER", None) + assert cluster_user is not None, "CLUSTER_USER environment variable is not set" + + cluster_endpoint = os.environ.get("CLUSTER_ENDPOINT", None) + assert ( + cluster_endpoint is not None + ), "CLUSTER_ENDPOINT environment variable is not set" + + await connect_with_pool_concurrent_connections( + cluster_user, cluster_endpoint + ) + + finally: + pass + + print("Concurrent pool operations completed successfully") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/asyncpg/test/alternatives/no_connection_pool/test_example_with_no_connection_pool.py b/python/asyncpg/test/alternatives/no_connection_pool/test_example_with_no_connection_pool.py new file mode 100644 index 00000000..86e4424d --- /dev/null +++ b/python/asyncpg/test/alternatives/no_connection_pool/test_example_with_no_connection_pool.py @@ -0,0 +1,16 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import pytest +from alternatives.no_connection_pool.example_with_no_connection_pool import main + + +# Smoke tests that our example works fine +@pytest.mark.asyncio +async def test_example_with_no_connection_pool(): + try: + await main() + except Exception as e: + pytest.fail(f"Unexpected exception: {e}") diff --git a/python/asyncpg/test/alternatives/pool/test_example_with_nonconcurrent_connection_pool.py b/python/asyncpg/test/alternatives/pool/test_example_with_nonconcurrent_connection_pool.py new file mode 100644 index 00000000..050e5488 --- /dev/null +++ b/python/asyncpg/test/alternatives/pool/test_example_with_nonconcurrent_connection_pool.py @@ -0,0 +1,16 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import pytest +from alternatives.pool.example_with_nonconcurrent_connection_pool import main + + +# Smoke tests that our example works fine +@pytest.mark.asyncio +async def test_example_with_nonconcurrent_connection_pool(): + try: + await main() + except Exception as e: + pytest.fail(f"Unexpected exception: {e}") diff --git a/python/asyncpg/test/test_example_preferred.py b/python/asyncpg/test/test_example_preferred.py new file mode 100644 index 00000000..d3fc88c9 --- /dev/null +++ b/python/asyncpg/test/test_example_preferred.py @@ -0,0 +1,16 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +""" + +import pytest +from example_preferred import main + + +# Smoke tests that our example works fine +@pytest.mark.asyncio +async def test_example_preferred(): + try: + await main() + except Exception as e: + pytest.fail(f"Unexpected exception: {e}")