diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..47bcbe0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,205 @@ +name: Java CI Combined + +on: + push: + pull_request: + workflow_dispatch: + workflow_call: + inputs: + LUCEE_TEST_JAVA_VERSION: + required: true + type: string + default: '21' + LUCEE_VERSION_QUERY: + required: true + type: string + default: '["6.2/snapshot/light","7/snapshot/light"]' + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract-version.outputs.VERSION }} + lucee-versions: ${{ steps.convert-lucee-versions.outputs.LUCEE_VERSIONS }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Extract version number + id: extract-version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + - name: Convert Lucee versions + id: convert-lucee-versions + env: + REPO_VERSIONS: ${{ vars.LUCEE_TEST_VERSIONS_PLUS }} + INPUT_VERSIONS: ${{ inputs.LUCEE_VERSION_QUERY }} + run: | + # Use input versions if provided, otherwise use repo versions + SIMPLE_VERSIONS="${INPUT_VERSIONS:-$REPO_VERSIONS}" + # Convert simple format to complex format + echo "$SIMPLE_VERSIONS" | jq -c 'map({version: ("light-" + split("/")[0] + ".0.0-SNAPSHOT"), query: .})' > lucee_versions.json + LUCEE_VERSIONS=$(cat lucee_versions.json) + echo "LUCEE_VERSIONS=$LUCEE_VERSIONS" >> $GITHUB_OUTPUT + + - name: Cache Lucee files + uses: actions/cache@v4 + with: + path: ~/work/_actions/lucee/script-runner/main/lucee-download-cache + key: lucee-downloads + + - name: Import GPG key + run: | + echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + + build: + runs-on: ubuntu-latest + needs: setup + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.LUCEE_TEST_JAVA_VERSION || '21' }} + distribution: 'temurin' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build and Install with Maven + run: | + echo "------- Maven Install -------"; + mvn -B -e -f pom.xml clean install + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: cockroachdb-lex + path: target/*.lex + + test: + runs-on: ubuntu-latest + needs: [setup, build] + strategy: + fail-fast: false + matrix: + lucee: ${{ fromJSON(needs.setup.outputs.lucee-versions) }} + steps: + - uses: actions/checkout@v4 + + - name: Start CockroachDB + run: | + docker run -d --name cockroachdb \ + -p 26257:26257 -p 8080:8080 \ + cockroachdb/cockroach:v23.1.11 start-single-node --insecure + + - name: Wait for CockroachDB and setup database + run: | + # Download cockroach CLI + curl -L https://binaries.cockroachdb.com/cockroach-v23.1.11.linux-amd64.tgz | tar -xz + sudo cp cockroach-v23.1.11.linux-amd64/cockroach /usr/local/bin/ + + # Wait for CockroachDB to be ready + for i in {1..10}; do + cockroach sql --insecure --host=localhost:26257 --execute="SELECT 1;" && break || sleep 5; + done + + # Create database + cockroach sql --insecure --host=localhost:26257 --execute="CREATE DATABASE IF NOT EXISTS testdb;" + + - name: Download built artifact + uses: actions/download-artifact@v4 + with: + name: cockroachdb-lex + path: target/ + + - name: Checkout Lucee + uses: actions/checkout@v4 + with: + repository: lucee/lucee + path: lucee + + - name: Run Lucee Test Suite + uses: lucee/script-runner@main + with: + webroot: ${{ github.workspace }}/lucee/test + execute: /bootstrap-tests.cfm + luceeVersionQuery: ${{ matrix.lucee.query }} + extensionDir: ${{ github.workspace }}/target + env: + testLabels: cockroachdb + testServices: none # we don't need any services for this test, avoid errors due to bundle download + LUCEE_ENABLE_BUNDLE_DOWNLOAD: false # we want to test the extension we just built + COCKROACHDB_HOST: localhost + COCKROACHDB_USERNAME: root + COCKROACHDB_PORT: 26257 + COCKROACHDB_DATABASE: testdb + LUCEE_LOGGING_FORCE_APPENDER: console + LUCEE_LOGGING_FORCE_LEVEL: info + testAdditional: ${{ github.workspace }}/tests + + deploy: + runs-on: ubuntu-latest + needs: [test] + if: always() && needs.test.result == 'success' && github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Import GPG key + run: | + echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: Build and Deploy with Maven + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + if [[ "${{ needs.setup.outputs.version }}" == *-SNAPSHOT ]]; then + echo "------- Maven Deploy snapshot on ${{ github.event_name }} -------"; + mvn -B -e -f pom.xml clean deploy --settings maven-settings.xml + else + echo "------- Maven Deploy release on ${{ github.event_name }} -------"; + mvn -B -e -f pom.xml clean deploy -DperformRelease=true --settings maven-settings.xml + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28e1415 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/dist +/notUsedYet +**/Test.java +**/.DS_Store +**/.svn/ +/*.project +va specific +*.class +target +target/* +*.lock +*.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c1fb686 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#023608", + "titleBar.activeBackground": "#034B0B", + "titleBar.activeForeground": "#ECFEEE" + } +} \ No newline at end of file diff --git a/CockroachDB.cfc b/CockroachDB.cfc new file mode 100644 index 0000000..20cc058 --- /dev/null +++ b/CockroachDB.cfc @@ -0,0 +1,86 @@ +component extends="types.Driver" output="no" implements="types.IDatasource" { + + fields = array( + field("SSL Mode", "sslmode", "disable,require,verify-ca,verify-full", "disable", "SSL mode to use for the connection.", "radio"), + field("Application Name", "application_name", "", false, "Optional application name for the connection."), + field("Timezone", "timezone", "", false, "Set the session timezone for the connection."), + field("Retry Transient Errors", "retryTransientErrors", "true,false", "false", "Automatically retry serialization failures during transactions.", "radio"), + field("Implicit SELECT FOR UPDATE", "implicitSelectForUpdate", "true,false", "false", "Automatically append FOR UPDATE to qualified SELECT statements to reduce contention.", "radio"), + field("Rewrite Batched Inserts", "reWriteBatchedInserts", "true,false", "false", "Enable array-based rewrites for bulk INSERT operations.", "radio"), + field("Rewrite Batched Updates", "reWriteBatchedUpdates", "true,false", "false", "Enable array-based rewrites for bulk UPDATE operations.", "radio"), + field("Rewrite Batched Upserts", "reWriteBatchedUpserts", "true,false", "false", "Enable array-based rewrites for bulk UPSERT operations.", "radio"), + field("Retry Max Attempts", "retryMaxAttempts", "", "15", "Maximum number of retry attempts for transient errors (default: 15)."), + field("Retry Max Backoff Time", "retryMaxBackoffTime", "", "30s", "Maximum backoff time between retries (default: 30s).") + ); + + this.type.port = this.TYPE_FREE; + this.value.host = "localhost"; + this.value.port = 26257; + this.className = "{class-name}"; + this.bundleName = "{bundle-name}"; + this.dsn = "{connString}"; + + /** + * Custom parameter syntax for CockroachDB JDBC URLs + */ + public struct function customParameterSyntax() { + return {leadingdelimiter:'?', delimiter:'&', separator:'='}; + } + + /** + * Validate field combinations before saving + */ + public void function onBeforeUpdate() { + // Validate retry settings + if (len(form.custom_retryMaxAttempts ?: "")) { + var attempts = val(form.custom_retryMaxAttempts); + if (attempts < 1 || attempts > 100) { + throw message="Retry Max Attempts must be between 1 and 100"; + } + } + + // Validate backoff time format + if (len(form.custom_retryMaxBackoffTime ?: "")) { + var backoffTime = form.custom_retryMaxBackoffTime; + if (!reFindNoCase("^\d+[smh]?$", backoffTime)) { + throw message="Retry Max Backoff Time must be in format like '30s', '5m', or '1h'"; + } + } + } + + /** + * returns display name of the driver + */ + public string function getName() { + return "{label}"; + } + + /** + * returns the id of the driver + */ + public string function getId() { + return "{id}"; + } + + /** + * returns the description of the driver + */ + public string function getDescription() { + return "{description}"; + } + + /** + * returns array of fields + */ + public array function getFields() { + return fields; + } + + public boolean function literalTimestampWithTSOffset() { + return false; + } + + public boolean function alwaysSetTimeout() { + return true; + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..84d5836 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Lucee CockroachDB JDBC Extension + +![CockroachDB Logo](logo.png) + +[![Java CI](https://github.com/lucee/extension-jdbc-cockroachdb/actions/workflows/main.yml/badge.svg)](https://github.com/lucee/extension-jdbc-cockroachdb/actions/workflows/main.yml) + +Issues: https://luceeserver.atlassian.net/issues/?jql=labels%20%3D%20cockroachdb + +## Coackroach DB + +https://www.cockroachlabs.com/product/overview/ + +## JDBC client + +While you can just use the Postgres JDBC client, this JDBC client adds some extra Cockroachdb specific support (it does bundle the postgres jdbc client and extends it) + +https://github.com/cloudneutral/cockroachdb-jdbc + +## Requirements + +**Java 17 or higher** is required for this extension due to the CockroachDB JDBC driver 2.0.1+ dependency. +**Lucee 6.2.2.91 or newer** diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..4c28a92 --- /dev/null +++ b/build.xml @@ -0,0 +1,286 @@ + + + + Building Datasource Extension + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Manifest-Version: 1.0 +Built-Date: ${NOW} +version: "${symbolicVersion}" +id: "${id}" +name: "${label}" +description: "${desc}" +category: "Datasource" +lucee-core-version: "6.2.2.91" +start-bundles: false +jdbc: "[{'label':'${label}','id':'${jdbcid}','connectionString':'${_connstr}','class':'${className}','bundleName':'${symbolicName}','bundleVersion':'${symbolicVersion}'}]" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..70c9744 Binary files /dev/null and b/logo.png differ diff --git a/maven-settings.xml b/maven-settings.xml new file mode 100644 index 0000000..3647539 --- /dev/null +++ b/maven-settings.xml @@ -0,0 +1,27 @@ + + + + + + + ossrh + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + + + + ossrh + + true + + + gpg2 + ${env.GPG_PASSPHRASE} + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..66af6fa --- /dev/null +++ b/pom.xml @@ -0,0 +1,362 @@ + + 4.0.0 + org.lucee + cockroachdb-jdbc-extension + ${cockroachdb.jdbc.version} + pom + CockroachDB JDBC + + + UTF-8 + 17 + 17 + C1A2B3C4-D5E6-47F8-9A0BC1D2E3F4A5B6 + 6.2.2.91 + server + 2.0.1 + jdbc:cockroachdb://{host}:{port}/{database} + cockroachdb + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://oss.sonatype.org/content/repositories/snapshots/ + + + + org.openjdk.nashorn + nashorn-core + 15.4 + provided + + + io.github.kai-niemi.cockroachdb.jdbc + cockroachdb-jdbc + ${cockroachdb.jdbc.version} + + + org.postgresql + postgresql + 42.7.7 + + + + https://github.com/lucee/extension-jdbc-cockroachdb + scm:git:git://github.com/lucee/extension-jdbc-cockroachdb.git + scm:git:git@github.com:lucee/extension-jdbc-cockroachdb.git + ${project.version} + + + + + The GNU Lesser General Public License, Version 2.1 + http://www.gnu.org/licenses/lgpl-2.1.txt + repo + + + + + + zspitzer + Zac Spitzer + zac@lucee.org + Lucee Association Switzerland + http://lucee.org + + Project-Administrator + Developer + + +1 + + + + JDBC Type 4 Driver for CockroachDB. + https://github.com/lucee/extension-jdbc-cockroachdb + + + + release-sign-artifacts + + + performRelease + true + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + ${project.build.directory}/${project.artifactId}-${project.version}.lex + + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.0.0 + + true + false + release + deploy + + + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.2.0 + + + copy-cockroachdb-jar + generate-resources + + copy-dependencies + + + io.github.kai-niemi.cockroachdb.jdbc,org.postgresql + cockroachdb-jdbc,postgresql + ${project.build.directory}/dependency + false + true + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.0.0 + + + org.openjdk.nashorn + nashorn-core + 15.4 + + + + + package + + run + + + + + + + + + + + + + + + + + + + + Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: cockroachdb-jdbc +Bundle-Name: CockroachDB JDBC Driver OSGi Bundle +Bundle-Version: ${cockroachdb.jdbc.version} +Bundle-Description: OSGi bundle for CockroachDB JDBC driver +Bundle-Vendor: Lucee Association Switzerland +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Export-Package: io.cockroachdb.jdbc;version="${cockroachdb.jdbc.version}", + io.github.kai_niemi.cockroachdb.jdbc;version="${cockroachdb.jdbc.version}", + org.postgresql;version="42.7.7", + org.postgresql.copy;version="42.7.7", + org.postgresql.core;version="42.7.7", + org.postgresql.ds;version="42.7.7", + org.postgresql.ds.common;version="42.7.7", + org.postgresql.fastpath;version="42.7.7", + org.postgresql.geometric;version="42.7.7", + org.postgresql.jdbc;version="42.7.7", + org.postgresql.largeobject;version="42.7.7", + org.postgresql.ssl;version="42.7.7", + org.postgresql.util;version="42.7.7", + org.postgresql.xa;version="42.7.7" +Import-Package: javax.sql, + javax.naming;resolution:=optional, + javax.net.ssl;resolution:=optional, + javax.transaction.xa;resolution:=optional, + org.slf4j;resolution:=optional;version="[1.6,3)", + javax.xml.parsers;resolution:=optional, + javax.xml.transform;resolution:=optional, + javax.xml.transform.stream;resolution:=optional + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + attach-artifacts + package + + attach-artifact + + + + + + ${project.build.directory}/${project.artifactId}-${project.version}.lex + lex + + + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 3.1.0 + + + cleanup + post-package + + clean + + + + + ${project.build.directory}/antrun + + **/* + + + + ${project.build.directory}/archive-tmp + + **/* + + + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + + + default-deploy + deploy + + deploy-file + + + ${project.build.directory}/${project.artifactId}-${project.version}.lex + ${project.groupId} + ${project.artifactId} + ${project.version} + ossrh + ${deploy.url} + lex + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + ossrh + true + + + + + \ No newline at end of file diff --git a/tests/cockroachdb.cfc b/tests/cockroachdb.cfc new file mode 100644 index 0000000..664728a --- /dev/null +++ b/tests/cockroachdb.cfc @@ -0,0 +1,88 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="cockroachdb" { + + function beforeAll (){ + variables.bundleName = "cockroachdb-jdbc"; + if ( _isConfEmpty() ) { + systemOutput( "CockroachDB environment variables not set, skipping tests", true ); + return; + } + variables.conf = _getCockroachEnvVars(); + variables.ds = { + class: "io.cockroachdb.jdbc.CockroachDriver" + // , bundleName: bundleName + // , bundleVersion: bundleVersion + , connectionString: "jdbc:cockroachdb://#variables.conf.host#:#variables.conf.port#/#variables.conf.database#?sslmode=disable" + , username: variables.conf.username + // In insecure mode, omit password to avoid authentication issues + }; + } + + function run(){ + describe( title="CockroachDB basic connection test", body=function(){ + + it("expect config to not be empty", function(){ + expect( _getCockroachEnvVars() ).notToBeEmpty("cockroachdb environment variables not set"); + }); + + it(title="verify cockroachdb with dbinfo", skip=_isConfEmpty(), body=function(){ + dbinfo datasource="#ds#" name="local.result" type="version"; + systemOutput( "", true ); + systemOutput( ds, true ); + systemOutput( local.result.toJson(), true ); + expect( local.result ).notToBeEmpty(); + }); + + }); + + describe( title="CockroachDB basic CRUD tests", body=function(){ + it(title="can create a table", skip=_isConfEmpty(), body=function(){ + // Clean up any existing test data first + QueryExecute( "DROP TABLE IF EXISTS test_table", {}, {datasource=ds} ); + var sql = "CREATE TABLE test_table (id INT PRIMARY KEY, name VARCHAR(255))"; + var result = QueryExecute( sql, {}, {datasource=ds} ); + expect( result ).toBeQuery(); + }); + it(title="can insert a row", skip=_isConfEmpty(), body=function(){ + var sql = "INSERT INTO test_table (id, name) VALUES (?, ?)"; + var params = [1, "Alice"]; + var result = QueryExecute( sql, params, {datasource=ds} ); + expect( result ).toBeQuery(); + }); + it(title="can select a row", skip=_isConfEmpty(), body=function(){ + var sql = "SELECT * FROM test_table WHERE id = ?"; + var params = [{value=1, cfsqltype="CF_SQL_INTEGER"}]; + var result = QueryExecute( sql, params, {datasource=ds} ); + expect( result.recordCount ).toBe( 1 ); + expect( result.name ).toBe( "Alice" ); + }); + it(title="can update a row", skip=_isConfEmpty(), body=function(){ + var sql = "UPDATE test_table SET name = ? WHERE id = ?"; + var params = ["Bob", {value=1, cfsqltype="CF_SQL_INTEGER"}]; + var result = QueryExecute( sql, params, {datasource=ds} ); + expect( result ).toBeQuery(); + // Verify update + var verify = QueryExecute( "SELECT * FROM test_table WHERE id = ?", [{value=1, cfsqltype="CF_SQL_INTEGER"}], {datasource=ds} ); + expect( verify.name ).toBe( "Bob" ); + }); + it(title="can delete a row", skip=_isConfEmpty(), body=function(){ + var sql = "DELETE FROM test_table WHERE id = ?"; + var params = [{value=1, cfsqltype="CF_SQL_INTEGER"}]; + var result = QueryExecute( sql, params, {datasource=ds} ); + expect( result ).toBeQuery(); + // Verify delete + var verify = QueryExecute( "SELECT * FROM test_table WHERE id = ?", [{value=1, cfsqltype="CF_SQL_INTEGER"}], {datasource=ds} ); + expect( verify.recordCount ).toBe( 0 ); + }); + + }) + } + + private boolean function _isConfEmpty() { + return isEmpty(_getCockroachEnvVars()); + } + + private struct function _getCockroachEnvVars() { + return server._getSystemPropOrEnvVars("HOST,USERNAME,PORT,DATABASE", "COCKROACHDB_"); + } + +}