├── .github └── workflows │ ├── build.yml │ └── snyk-scan.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── examples.md ├── params.md └── tls.md ├── images ├── DBeaverAerospike.png └── JetBrainsDataGripAerospike.png ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── aerospike │ │ └── jdbc │ │ ├── AerospikeConnection.java │ │ ├── AerospikeDatabaseMetadata.java │ │ ├── AerospikeDriver.java │ │ ├── AerospikeDriverAction.java │ │ ├── AerospikePreparedStatement.java │ │ ├── AerospikeStatement.java │ │ ├── async │ │ ├── EventLoopProvider.java │ │ ├── FutureBatchOperateListListener.java │ │ ├── FutureDeleteListener.java │ │ ├── FutureWriteListener.java │ │ ├── RecordSet.java │ │ ├── RecordSetBatchSequenceListener.java │ │ ├── RecordSetRecordSequenceListener.java │ │ ├── ScanQueryHandler.java │ │ └── SecondaryIndexQueryHandler.java │ │ ├── model │ │ ├── AerospikeClusterInfo.java │ │ ├── AerospikeQuery.java │ │ ├── AerospikeSecondaryIndex.java │ │ ├── AerospikeSqlVisitor.java │ │ ├── CatalogTableName.java │ │ ├── DataColumn.java │ │ ├── DriverConfiguration.java │ │ ├── DriverPolicy.java │ │ ├── Pair.java │ │ └── QueryType.java │ │ ├── predicate │ │ ├── Operator.java │ │ ├── OperatorBinary.java │ │ ├── OperatorUnary.java │ │ ├── OperatorVarArgs.java │ │ ├── QueryPredicate.java │ │ ├── QueryPredicateBase.java │ │ ├── QueryPredicateBinary.java │ │ ├── QueryPredicateBoolean.java │ │ ├── QueryPredicateIsNotNull.java │ │ ├── QueryPredicateIsNull.java │ │ ├── QueryPredicateLike.java │ │ ├── QueryPredicateList.java │ │ ├── QueryPredicatePrefix.java │ │ ├── QueryPredicateRange.java │ │ └── VarArgsFunction.java │ │ ├── query │ │ ├── BaseQueryHandler.java │ │ ├── DeleteQueryHandler.java │ │ ├── IndexCreateHandler.java │ │ ├── IndexDropHandler.java │ │ ├── InsertQueryHandler.java │ │ ├── PolicyBuilder.java │ │ ├── QueryHandler.java │ │ ├── QueryPerformer.java │ │ ├── SelectQueryHandler.java │ │ ├── TruncateQueryHandler.java │ │ └── UpdateQueryHandler.java │ │ ├── schema │ │ ├── AerospikeSchemaBuilder.java │ │ ├── AerospikeSchemaCache.java │ │ └── OptionalCache.java │ │ ├── sql │ │ ├── AerospikeRecordResultSet.java │ │ ├── AerospikeResultSetMetaData.java │ │ ├── BaseResultSet.java │ │ ├── IndexToLabelResultSet.java │ │ ├── ListRecordSet.java │ │ ├── SimpleParameterMetaData.java │ │ ├── SimpleWrapper.java │ │ ├── UpdateResultSet.java │ │ └── type │ │ │ ├── BasicArray.java │ │ │ ├── ByteArrayBlob.java │ │ │ └── StringClob.java │ │ ├── tls │ │ ├── AerospikeTLSPolicyBuilder.java │ │ └── AerospikeTLSPolicyConfig.java │ │ └── util │ │ ├── AerospikeClientLogger.java │ │ ├── AerospikeUtils.java │ │ ├── AerospikeVersion.java │ │ ├── AuxStatementParser.java │ │ ├── Constants.java │ │ ├── DatabaseMetadataBuilder.java │ │ ├── IOUtils.java │ │ ├── PreparedStatement.java │ │ └── SqlLiterals.java └── resources │ └── logging.properties └── test ├── java └── com │ └── aerospike │ └── jdbc │ ├── DatabaseMetadataTest.java │ ├── IndexQueriesTest.java │ ├── JdbcBaseTest.java │ ├── JdbiQueriesTest.java │ ├── ParseJdbcUrlTest.java │ ├── PreparedQueriesTest.java │ ├── QueryCustomParserTest.java │ ├── QueryParserTest.java │ ├── QuerySendKeyFalseTest.java │ ├── RecordMetadataTest.java │ ├── SimpleQueriesTest.java │ ├── TransactionTest.java │ └── util │ ├── TestConfig.java │ ├── TestRecord.java │ └── TestUtil.java └── resources └── testng.xml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up JDK 1.8 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'temurin' 22 | java-version: 8 23 | cache: 'maven' 24 | 25 | - name: Set up Aerospike Database 26 | uses: reugn/github-action-aerospike@v1 27 | 28 | - name: Build with Maven 29 | run: mvn clean package -B -U -------------------------------------------------------------------------------- /.github/workflows/snyk-scan.yml: -------------------------------------------------------------------------------- 1 | name: Snyk scan 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | snyk-security: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Run Snyk to check for vulnerabilities 18 | uses: snyk/actions/maven@master 19 | continue-on-error: true # To make sure that SARIF upload gets called 20 | env: 21 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 22 | with: 23 | args: --all-projects --sarif-file-output=snyk.sarif 24 | 25 | - name: Check output file 26 | id: out-file 27 | run: | 28 | if test -f "snyk.sarif"; then 29 | echo "::set-output name=exists::true"; else 30 | echo "::set-output name=exists::false" 31 | fi 32 | 33 | - name: Handle undefined security-severity 34 | if: steps.out-file.outputs.exists == 'true' 35 | run: | 36 | sed -i 's/"security-severity": "undefined"/"security-severity": "0"/g' snyk.sarif 37 | 38 | - name: Upload result to GitHub Code Scanning 39 | if: steps.out-file.outputs.exists == 'true' 40 | uses: github/codeql-action/upload-sarif@v3 41 | with: 42 | sarif_file: snyk.sarif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target 3 | *.iml 4 | *.jar 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aerospike JDBC Driver 2 | ![Build](https://github.com/aerospike/aerospike-jdbc/workflows/Build/badge.svg) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.aerospike/aerospike-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.aerospike/aerospike-jdbc/) 4 | [![javadoc](https://javadoc.io/badge2/com.aerospike/aerospike-jdbc/javadoc.svg)](https://javadoc.io/doc/com.aerospike/aerospike-jdbc) 5 | 6 | Aerospike JDBC Driver allows you to interact with Aerospike clusters by using SQL statements from your Java application. 7 | Read [Java Tutorials](https://docs.oracle.com/javase/tutorial/jdbc/basics/index.html) to get started with JDBC. 8 | 9 | ## Prerequisites 10 | * Java 8 or later 11 | * Aerospike Server version 5.2+ 12 | 13 | ## Build 14 | ```sh 15 | mvn clean package 16 | ``` 17 | The JDBC driver jar `uber-aerospike-jdbc-.jar` will be created under the target folder. 18 | 19 | Pre-built versions of the driver are available in the [Releases](https://github.com/aerospike/aerospike-jdbc/releases). 20 | 21 | ## JDBC connection properties 22 | | | | 23 | | --- | --- | 24 | | JDBC Driver | `com.aerospike.jdbc.AerospikeDriver` | 25 | | JDBC URL | `jdbc:aerospike:HOST[:PORT][/NAMESPACE][?PARAM1=VALUE1[&PARAM2=VALUE2]`[1](#jdbc-url) | 26 | 27 | 1 For example `jdbc:aerospike:localhost` connects to the Aerospike database running on a local machine and listening on the default port (3000). 28 | The `jdbc:aerospike:172.17.0.5:3300/test` URL connects to the `test` namespace on the Aerospike database running on `172.17.0.5:3300`. When the namespace is provided in the JDBC URL a table name is assumed to be in that connection's namespace, and there is no need to mention the namespace in the query. In this example the following will get the records in namespace _test_ and set _demo_. 29 | ```sql 30 | SELECT * FROM demo; 31 | ``` 32 | 33 | See more about optional [configuration parameters](docs/params.md). 34 | 35 | ## Usage example 36 | ```java 37 | try { 38 | String url = "jdbc:aerospike:localhost:3300/test"; 39 | Connection connection = DriverManager.getConnection(url); 40 | 41 | String query = "select * from ns1 limit 10"; 42 | ResultSet resultSet = connection.createStatement().executeQuery(query); 43 | while (resultSet.next()) { 44 | String bin1 = resultSet.getString("bin1"); 45 | System.out.println(bin1); 46 | } 47 | } catch (Exception e) { 48 | System.err.println(e.getMessage()); 49 | } 50 | ``` 51 | Packages documentation can be found [here](https://javadoc.io/doc/com.aerospike/aerospike-jdbc). 52 | 53 | ## Supported SQL Statements 54 | * SELECT 55 | * INSERT 56 | * UPDATE 57 | * DELETE 58 | * TRUNCATE TABLE 59 | * CREATE INDEX 60 | * DROP INDEX 61 | * Transactions 62 | 63 | See [examples](docs/examples.md) of SQL. 64 | 65 | 1 JOIN, nested SELECT queries and GROUP BY statements are not in the scope of the current version. 66 | 67 | 2 The development is in progress, and minor features documentation is not maintained now. 68 | 69 | ## JDBC Client tools 70 | * [DBeaver](https://dbeaver.io/) 71 | Configure the Aerospike JDBC Driver: 72 | * Database -> Driver Manager -> New 73 | Fill in settings: 74 | * Driver Name: Aerospike 75 | * Driver Type: Generic 76 | * Class Name: `com.aerospike.jdbc.AerospikeDriver` 77 | * URL Template: `jdbc:aerospike:{host}[:{port}]/[{database}]`[1](#jdbc-database) 78 | * Host: _host_ URL (such as `localhost` or `0.0.0.0`) 79 | * Port: _port_ by default should be `3000` 80 | * Database/Schema: _database_ by default should be `test` 81 | * Click the `Add File` button and add the JDBC jar file. 82 | * Click the `Find Class` button. 83 | * Click `OK`. 84 | 85 | Create a connection: 86 | * Database -> New Database Connection 87 | * Select `Aerospike` and click `Next`. 88 | * Fill in the connection settings 89 | * Host and Port 90 | * Database/Schema: the namespace you are connecting to 91 | * Username and Password if you have security turned on in Aerospike Database Enterprise Edition 92 | * Click `Finish`. 93 | 94 | 1 Specify the `database` parameter for proper functionality. 95 | 96 | ![DBeaverAerospike](/images/DBeaverAerospike.png) 97 | 98 | * [JetBrains DataGrip](https://www.jetbrains.com/datagrip/) 99 | 100 | Configure the Aerospike JDBC Driver: 101 | * Database > + > Driver 102 | * Name: Aerospike 103 | * Comment (Optional): Aerospike Driver 104 | * Driver Files > + > Custom JARs… > add the Aerospike JDBC jar file 105 | * URL Template > + > jdbc:aerospike:{host}[:{port}]/[{database}] 106 | * Class: select “com.aerospike.jdbc.AerospikeDriver” (should appear after doing the previous steps). 107 | * Apply. 108 | 109 | Configure the data source connection: 110 | * Go to the Data Sources tab > + > Aerospike 111 | * Choose No Auth or Username & Password if you have security turned on in Aerospike Database Enterprise Edition. 112 | * URL: fill the Host, Port and the namespace (For example: jdbc:aerospike:localhost:3000/test). 113 | * Apply (ignore the warning). 114 | 115 | ![JetBrainsDataGripAerospike](/images/JetBrainsDataGripAerospike.png) 116 | 117 | * [SQuirreL](http://squirrel-sql.sourceforge.net/) 118 | 119 | ## License 120 | Licensed under the Apache 2.0 License. 121 | -------------------------------------------------------------------------------- /docs/params.md: -------------------------------------------------------------------------------- 1 | # Aerospike JDBC Configuration Parameters 2 | 3 | The Aerospike JDBC driver can be configured through the JDBC connection URL. For example: 4 | 5 | `jdbc:aerospike:localhost/test?sendKey=true&durableDelete=true&expiration=-1` 6 | 7 | For detailed information about configuration properties, see the following sections. 8 | 9 | ## Aerospike Java client configuration 10 | 11 | The parameters need to match fields of the [Aerospike Java client's](https://javadoc.io/doc/com.aerospike/aerospike-client/latest/index.html) 12 | `Policy` and its subclasses, which take `String`, `int` or `boolean` values. 13 | 14 | The optional configuration parameters include the following 15 | 16 | | Param | Default | Description | Aerospike Java Client | 17 | |----------------------|---------|---------------------------------------------------------------------------------------------------------------|-------------------------------------| 18 | | useBoolBin | `true` | Use the boolean data type | `Value.UseBoolBin` | 19 | | compress | `false` | Use zlib compression on commands to the server | `Policy.compress` | 20 | | connectTimeout | 0 | Socket connect timeout in milliseconds | `Policy.connectTimeout` | 21 | | loginTimeout | 5000 | Login timeout in milliseconds | `ClientPolicy.loginTimeout` | 22 | | tendInterval | 1000 | Interval in milliseconds between cluster tends | `ClientPolicy.tendInterval` | 23 | | timeout | 1000 | Cluster tend info call timeout in milliseconds | `ClientPolicy.timeout` | 24 | | totalTimeout | 0 | Total transaction timeout in milliseconds | `Policy.totalTimeout` | 25 | | useServicesAlternate | `false` | Use "services-alternate" instead of "services" for cluster tending | `ClientPolicy.useServicesAlternate` | 26 | | sendKey | `false` | Send user key on both reads and writes | `Policy.sendKey` | 27 | | durableDelete | `false` | If the transaction results in a record deletion, leave a tombstone for the record | `Policy.durableDelete` | 28 | | expiration | 0 | 0 : use namespace `default-ttl`; -1: never expire; -2 don't change the ttl; otherwise seconds till expiration | `Policy.expiration` | 29 | 30 | ## JDBC Driver configuration 31 | 32 | The following parameters configure the internal state of the driver. 33 | Their default values are sufficient in most cases. 34 | Consider setting a custom value if really necessary. 35 | 36 | | Param | Default | Description | 37 | |-------------------------|---------|------------------------------------------------------------------------| 38 | | recordSetQueueCapacity | 256 | The capacity of the record queue for asynchronous Aerospike operations | 39 | | recordSetTimeoutMs | 1000 | Timeout for the asynchronous queue write operation in milliseconds | 40 | | metadataCacheTtlSeconds | 3600 | Database metadata cache TTL in seconds | 41 | | schemaBuilderMaxRecords | 1000 | The number of records to be used to build the table schema | 42 | | showRecordMetadata | false | Add record metadata columns (__digest, __ttl, __gen) | 43 | | txnTimeoutSeconds | 10 | Multi-record transaction timeout in seconds | 44 | -------------------------------------------------------------------------------- /docs/tls.md: -------------------------------------------------------------------------------- 1 | # Aerospike JDBC TLS Configuration Parameters 2 | 3 | The Aerospike JDBC driver can be configured to use SSL through the JDBC connection URL. 4 | 5 | Below are the tls-related configs for aerospike-jdbc (example with value): 6 | 7 | ``` 8 | tlsEnabled true 9 | tlsName TLS_NAME 10 | tlsStoreType (like jks) 11 | tlsTruststorePassword ******* (if required) 12 | tlsTruststorePath ./../XXX.jks (the full path) 13 | ``` 14 | The full list of the valid values can be found at [AerospikeTLSPolicyConfig](https://github.com/aerospike/aerospike-jdbc/blob/main/src/main/java/com/aerospike/jdbc/tls/AerospikeTLSPolicyConfig.java). 15 | 16 | The configuration expects a standard Java TrustStore. An example of adding a CA certificate to the Java TrustStore can be found at [Add CA certificate to Java TrustStore on client nodes](https://docs.aerospike.com/server/operations/configure/network/tls/mtls_java#add-ca-certificate-to-java-truststore-on-client-nodes). 17 | 18 | ``` 19 | keytool -import -alias tls1 -file /etc/aerospike/ssl/tls1/cert.pem -keystore //usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts -storePass changeit 20 | ``` 21 | Here are some working URL examples using the above configuration properties: 22 | 23 | ``` 24 | //aerospike server detail 25 | //host:172.17.0.9 port:4333 namespace:test tlsname:tls1 (as per the CN, same used in the Java TrustStore alias as well) 26 | 27 | //connection URL example1 28 | "jdbc:aerospike:172.17.0.9:4333/test?tlsEnabled=true&tlsName=tls1&tlsTruststorePath=/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts&tlsTruststorePassword=changeit"; 29 | 30 | //connection URL example2 31 | "jdbc:aerospike:172.17.0.9:4333/test?tlsEnabled=true&tlsName=tls1&tlsStoreType=jks&tlsTruststorePath=/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts&tlsTruststorePassword=changeit"; 32 | ``` 33 | -------------------------------------------------------------------------------- /images/DBeaverAerospike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike/aerospike-jdbc/f775ef3db4b4599371319340ecc5b90763f9c654/images/DBeaverAerospike.png -------------------------------------------------------------------------------- /images/JetBrainsDataGripAerospike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike/aerospike-jdbc/f775ef3db4b4599371319340ecc5b90763f9c654/images/JetBrainsDataGripAerospike.png -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/AerospikeDriver.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.client.Log; 4 | import com.aerospike.jdbc.util.AerospikeClientLogger; 5 | 6 | import java.sql.Connection; 7 | import java.sql.Driver; 8 | import java.sql.DriverPropertyInfo; 9 | import java.sql.SQLException; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collection; 13 | import java.util.Objects; 14 | import java.util.Properties; 15 | import java.util.logging.Logger; 16 | 17 | import static com.aerospike.jdbc.util.Constants.DRIVER_MAJOR_VERSION; 18 | import static com.aerospike.jdbc.util.Constants.DRIVER_MINOR_VERSION; 19 | import static java.util.stream.Collectors.toList; 20 | 21 | public class AerospikeDriver implements Driver { 22 | 23 | private static final Logger logger = Logger.getLogger("com.aerospike.jdbc"); 24 | private static final String AEROSPIKE_URL_PREFIX = "jdbc:aerospike:"; 25 | 26 | static { 27 | try { 28 | java.sql.DriverManager.registerDriver(new AerospikeDriver(), new AerospikeDriverAction()); 29 | } catch (SQLException e) { 30 | throw new ExceptionInInitializerError("Can not register AerospikeDriver"); 31 | } 32 | logger.info("Set callback for Java client logs"); 33 | Log.Callback asLoggerCallback = new AerospikeClientLogger(); 34 | Log.setCallback(asLoggerCallback); 35 | } 36 | 37 | public Connection connect(String url, Properties info) throws SQLException { 38 | if (url == null) { 39 | throw new SQLException("url is null"); 40 | } 41 | if (!url.startsWith(AEROSPIKE_URL_PREFIX)) { 42 | return null; 43 | } 44 | 45 | return new AerospikeConnection(url, info); 46 | } 47 | 48 | public boolean acceptsURL(String url) { 49 | return Objects.nonNull(url) && url.startsWith(AEROSPIKE_URL_PREFIX); 50 | } 51 | 52 | public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { 53 | int questionPos = url.indexOf('?'); 54 | Collection allInfo = new ArrayList<>(); 55 | if (questionPos > 0 && questionPos < url.length() - 1) { 56 | Arrays.stream(url.substring(questionPos + 1).split("&")).forEach(p -> { 57 | String[] kv = p.split("="); 58 | allInfo.add(new DriverPropertyInfo(kv[0], kv.length > 1 ? kv[1] : null)); 59 | }); 60 | } 61 | allInfo.addAll(info.entrySet().stream() 62 | .map(e -> new DriverPropertyInfo((String) e.getKey(), (String) e.getValue())) 63 | .collect(toList())); 64 | return allInfo.toArray(new DriverPropertyInfo[0]); 65 | } 66 | 67 | public int getMajorVersion() { 68 | return DRIVER_MAJOR_VERSION; 69 | } 70 | 71 | public int getMinorVersion() { 72 | return DRIVER_MINOR_VERSION; 73 | } 74 | 75 | public boolean jdbcCompliant() { 76 | return false; 77 | } 78 | 79 | public Logger getParentLogger() { 80 | return logger; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/AerospikeDriverAction.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.async.EventLoopProvider; 4 | 5 | import java.sql.DriverAction; 6 | import java.util.logging.Logger; 7 | 8 | public class AerospikeDriverAction implements DriverAction { 9 | 10 | private static final Logger logger = Logger.getLogger(AerospikeDriverAction.class.getName()); 11 | 12 | @Override 13 | public void deregister() { 14 | logger.info("Deregister AerospikeDriver"); 15 | EventLoopProvider.close(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/EventLoopProvider.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.async.EventLoop; 4 | import com.aerospike.client.async.EventLoops; 5 | import com.aerospike.client.async.EventPolicy; 6 | import com.aerospike.client.async.NettyEventLoops; 7 | import io.netty.channel.nio.NioEventLoopGroup; 8 | 9 | import java.util.logging.Logger; 10 | 11 | import static java.util.Objects.requireNonNull; 12 | 13 | public final class EventLoopProvider { 14 | 15 | private static final Logger logger = Logger.getLogger(EventLoopProvider.class.getName()); 16 | private static volatile EventLoops eventLoops; 17 | 18 | private EventLoopProvider() { 19 | } 20 | 21 | public static EventLoop getEventLoop() { 22 | initEventLoops(); 23 | return eventLoops.next(); 24 | } 25 | 26 | public static EventLoops getEventLoops() { 27 | initEventLoops(); 28 | return eventLoops; 29 | } 30 | 31 | public static synchronized void close() { 32 | if (null != eventLoops) { 33 | logger.info(() -> "Close eventLoops"); 34 | eventLoops.close(); 35 | eventLoops = null; 36 | } 37 | } 38 | 39 | private static void initEventLoops() { 40 | if (null == eventLoops) { 41 | synchronized (EventLoopProvider.class) { 42 | if (null == eventLoops) { 43 | logger.info(() -> "Init eventLoops"); 44 | int nThreads = Math.max(2, Runtime.getRuntime().availableProcessors()); 45 | EventLoops nettyEventLoops = new NettyEventLoops( 46 | new EventPolicy(), 47 | new NioEventLoopGroup(nThreads) 48 | ); 49 | requireNonNull(nettyEventLoops.get(0)); 50 | eventLoops = nettyEventLoops; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/FutureBatchOperateListListener.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.BatchRecord; 5 | import com.aerospike.client.listener.BatchOperateListListener; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.Future; 11 | 12 | public class FutureBatchOperateListListener implements BatchOperateListListener { 13 | 14 | private final CompletableFuture totalFuture = new CompletableFuture<>(); 15 | 16 | @Override 17 | public void onSuccess(List list, boolean b) { 18 | totalFuture.complete((int) list.stream().filter(Objects::nonNull).count()); 19 | } 20 | 21 | @Override 22 | public void onFailure(AerospikeException e) { 23 | totalFuture.completeExceptionally(e); 24 | } 25 | 26 | public Future getTotal() { 27 | return totalFuture; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/FutureDeleteListener.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Key; 5 | import com.aerospike.client.listener.DeleteListener; 6 | 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | public class FutureDeleteListener implements DeleteListener { 12 | 13 | private final CompletableFuture totalFuture = new CompletableFuture<>(); 14 | private final AtomicInteger totalRecordsCount = new AtomicInteger(); 15 | private final AtomicInteger successRecordsCount = new AtomicInteger(); 16 | private final int totalRecords; 17 | 18 | public FutureDeleteListener(int totalRecords) { 19 | this.totalRecords = totalRecords; 20 | } 21 | 22 | @Override 23 | public void onSuccess(Key key, boolean b) { 24 | if (b) { 25 | successRecordsCount.incrementAndGet(); 26 | } 27 | tryCompleteFuture(); 28 | } 29 | 30 | @Override 31 | public void onFailure(AerospikeException e) { 32 | tryCompleteFuture(); 33 | } 34 | 35 | private void tryCompleteFuture() { 36 | if (totalRecordsCount.incrementAndGet() == totalRecords) { 37 | totalFuture.complete(successRecordsCount.get()); 38 | } 39 | } 40 | 41 | public Future getTotal() { 42 | return totalFuture; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/FutureWriteListener.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Key; 5 | import com.aerospike.client.listener.WriteListener; 6 | 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.Future; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | public class FutureWriteListener implements WriteListener { 12 | 13 | private final CompletableFuture totalFuture = new CompletableFuture<>(); 14 | private final AtomicInteger totalRecordsCount = new AtomicInteger(); 15 | private final AtomicInteger successRecordsCount = new AtomicInteger(); 16 | private final int totalRecords; 17 | 18 | public FutureWriteListener(int totalRecords) { 19 | this.totalRecords = totalRecords; 20 | } 21 | 22 | @Override 23 | public void onSuccess(Key key) { 24 | successRecordsCount.incrementAndGet(); 25 | tryCompleteFuture(); 26 | } 27 | 28 | @Override 29 | public void onFailure(AerospikeException e) { 30 | tryCompleteFuture(); 31 | } 32 | 33 | private void tryCompleteFuture() { 34 | if (totalRecordsCount.incrementAndGet() == totalRecords) { 35 | totalFuture.complete(successRecordsCount.get()); 36 | } 37 | } 38 | 39 | public Future getTotal() { 40 | return totalFuture; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/RecordSet.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Key; 5 | import com.aerospike.client.Record; 6 | import com.aerospike.client.query.KeyRecord; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.io.Closeable; 10 | import java.util.Iterator; 11 | import java.util.concurrent.ArrayBlockingQueue; 12 | import java.util.concurrent.BlockingQueue; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.logging.Logger; 15 | 16 | public final class RecordSet 17 | implements Iterable, Closeable { 18 | 19 | private static final Logger logger = Logger.getLogger(RecordSet.class.getName()); 20 | 21 | private static final KeyRecord END = new KeyRecord(null, null); 22 | private static final KeyRecord FAILURE = new KeyRecord(null, null); 23 | 24 | private final BlockingQueue queue; 25 | private final int timeoutMs; 26 | private KeyRecord keyRecord; 27 | private volatile boolean valid = true; 28 | private volatile boolean interrupted; 29 | 30 | public RecordSet(int capacity, int timeoutMs) { 31 | this.queue = new ArrayBlockingQueue<>(capacity); 32 | this.timeoutMs = timeoutMs; 33 | } 34 | 35 | public boolean next() 36 | throws AerospikeException { 37 | if (valid) { 38 | try { 39 | keyRecord = queue.take(); 40 | } catch (InterruptedException e) { 41 | logger.info(() -> "InterruptedException in next"); 42 | interrupt(); 43 | } 44 | if (keyRecord == FAILURE) { 45 | logger.info(() -> String.format("timeoutMs: %d", timeoutMs)); 46 | throw new AerospikeException("Aerospike asynchronous command failure"); 47 | } 48 | if (keyRecord == END) { 49 | valid = false; 50 | } 51 | } 52 | return valid; 53 | } 54 | 55 | private void interrupt() { 56 | interrupted = true; 57 | queue.clear(); 58 | } 59 | 60 | @Override 61 | public void close() { 62 | put(END); 63 | } 64 | 65 | @Override 66 | @Nonnull 67 | public Iterator iterator() { 68 | return new RecordSetIterator(this); 69 | } 70 | 71 | public Key getKey() { 72 | return keyRecord.key; 73 | } 74 | 75 | public Record getRecord() { 76 | return keyRecord.record; 77 | } 78 | 79 | public boolean put(KeyRecord keyRecord) { 80 | if (valid) { 81 | try { 82 | if (!queue.offer(keyRecord, timeoutMs, TimeUnit.MILLISECONDS)) { 83 | logger.fine(() -> "Timeout in put"); 84 | abort(); 85 | throw new AerospikeException.QueryTerminated(); 86 | } 87 | } catch (InterruptedException e) { 88 | logger.info(() -> "InterruptedException in put"); 89 | abort(); 90 | throw new AerospikeException.QueryTerminated(e); 91 | } 92 | if (interrupted) { 93 | logger.info(() -> "Interrupted in put"); 94 | throw new AerospikeException.QueryTerminated(); 95 | } 96 | } 97 | return valid; 98 | } 99 | 100 | public void abort() { 101 | queue.clear(); 102 | put(FAILURE); 103 | } 104 | 105 | private static class RecordSetIterator 106 | implements Iterator, Closeable { 107 | private final RecordSet recordSet; 108 | private boolean more; 109 | 110 | RecordSetIterator(RecordSet recordSet) { 111 | this.recordSet = recordSet; 112 | this.more = this.recordSet.next(); 113 | } 114 | 115 | @Override 116 | public boolean hasNext() { 117 | return more; 118 | } 119 | 120 | @Override 121 | public KeyRecord next() { 122 | KeyRecord nextKeyRecord = recordSet.keyRecord; 123 | more = recordSet.next(); 124 | return nextKeyRecord; 125 | } 126 | 127 | @Override 128 | public void close() { 129 | recordSet.close(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/RecordSetBatchSequenceListener.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.BatchRead; 5 | import com.aerospike.client.listener.BatchSequenceListener; 6 | import com.aerospike.client.query.KeyRecord; 7 | import com.aerospike.jdbc.model.DriverPolicy; 8 | 9 | import java.util.logging.Level; 10 | import java.util.logging.Logger; 11 | 12 | public class RecordSetBatchSequenceListener implements BatchSequenceListener { 13 | 14 | private static final Logger logger = Logger.getLogger(RecordSetBatchSequenceListener.class.getName()); 15 | 16 | private final RecordSet recordSet; 17 | 18 | public RecordSetBatchSequenceListener(DriverPolicy driverPolicy) { 19 | recordSet = new RecordSet( 20 | driverPolicy.getRecordSetQueueCapacity(), 21 | driverPolicy.getRecordSetTimeoutMs() 22 | ); 23 | } 24 | 25 | @Override 26 | public void onRecord(BatchRead batchRead) { 27 | if (batchRead != null && batchRead.record != null) { 28 | recordSet.put(new KeyRecord(batchRead.key, batchRead.record)); 29 | } 30 | } 31 | 32 | @Override 33 | public void onSuccess() { 34 | recordSet.close(); 35 | } 36 | 37 | @Override 38 | public void onFailure(AerospikeException e) { 39 | logger.log(Level.SEVERE, "Aerospike listener failure", e); 40 | recordSet.abort(); 41 | } 42 | 43 | public RecordSet getRecordSet() { 44 | return recordSet; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/RecordSetRecordSequenceListener.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Key; 5 | import com.aerospike.client.Record; 6 | import com.aerospike.client.ResultCode; 7 | import com.aerospike.client.listener.RecordSequenceListener; 8 | import com.aerospike.client.query.KeyRecord; 9 | import com.aerospike.jdbc.model.DriverPolicy; 10 | 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | public class RecordSetRecordSequenceListener implements RecordSequenceListener { 15 | 16 | private static final Logger logger = Logger.getLogger(RecordSetRecordSequenceListener.class.getName()); 17 | 18 | private final RecordSet recordSet; 19 | 20 | public RecordSetRecordSequenceListener(DriverPolicy driverPolicy) { 21 | recordSet = new RecordSet( 22 | driverPolicy.getRecordSetQueueCapacity(), 23 | driverPolicy.getRecordSetTimeoutMs() 24 | ); 25 | } 26 | 27 | @Override 28 | public void onRecord(Key key, Record rec) throws AerospikeException { 29 | recordSet.put(new KeyRecord(key, rec)); 30 | } 31 | 32 | @Override 33 | public void onSuccess() { 34 | recordSet.close(); 35 | } 36 | 37 | @Override 38 | public void onFailure(AerospikeException exception) { 39 | if (exception.getResultCode() == ResultCode.QUERY_TERMINATED) { 40 | logger.warning(exception::getMessage); 41 | } else { 42 | logger.log(Level.SEVERE, "Aerospike listener failure", exception); 43 | } 44 | recordSet.abort(); 45 | } 46 | 47 | public RecordSet getRecordSet() { 48 | return recordSet; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/ScanQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.Key; 5 | import com.aerospike.client.ScanCallback; 6 | import com.aerospike.client.cluster.Node; 7 | import com.aerospike.client.cluster.Partition; 8 | import com.aerospike.client.policy.ScanPolicy; 9 | import com.aerospike.client.query.PartitionFilter; 10 | import com.aerospike.jdbc.model.AerospikeQuery; 11 | import com.aerospike.jdbc.model.DriverPolicy; 12 | 13 | import java.util.Objects; 14 | 15 | public class ScanQueryHandler { 16 | 17 | private final IAerospikeClient client; 18 | private RecordSetRecordSequenceListener listener; 19 | 20 | private int currentPartition; 21 | private int count; 22 | 23 | private final ScanCallback callback = ((key, rec) -> { 24 | listener.onRecord(key, rec); 25 | count++; 26 | }); 27 | 28 | public ScanQueryHandler(IAerospikeClient client, DriverPolicy driverPolicy) { 29 | this.client = client; 30 | this.listener = new RecordSetRecordSequenceListener(driverPolicy); 31 | } 32 | 33 | public static ScanQueryHandler create(IAerospikeClient client, DriverPolicy driverPolicy) { 34 | return new ScanQueryHandler(client, driverPolicy); 35 | } 36 | 37 | public RecordSet execute(ScanPolicy scanPolicy, AerospikeQuery query) { 38 | if (query.isPrimaryKeyOnly()) { 39 | scanPolicy.includeBinData = false; 40 | } 41 | if (Objects.nonNull(query.getOffset())) { 42 | long maxRecords = scanPolicy.maxRecords; 43 | PartitionFilter filter = getPartitionFilter(query); 44 | while (isScanRequired(maxRecords)) { 45 | client.scanPartitions(scanPolicy, filter, query.getCatalog(), query.getSetName(), 46 | callback, query.columnBins()); 47 | scanPolicy.maxRecords = maxRecords > 0 ? maxRecords - count : maxRecords; 48 | filter = PartitionFilter.id(++currentPartition); 49 | } 50 | listener.onSuccess(); 51 | } else { 52 | client.scanAll(EventLoopProvider.getEventLoop(), listener, scanPolicy, query.getCatalog(), 53 | query.getSetName(), query.columnBins()); 54 | } 55 | return listener.getRecordSet(); 56 | } 57 | 58 | private PartitionFilter getPartitionFilter(AerospikeQuery query) { 59 | Key key = new Key(query.getCatalog(), query.getSetName(), query.getOffset()); 60 | currentPartition = Partition.getPartitionId(key.digest); 61 | return PartitionFilter.after(key); 62 | } 63 | 64 | private boolean isScanRequired(final long maxRecords) { 65 | return (maxRecords == 0 || count < maxRecords) && isValidPartition(); 66 | } 67 | 68 | private boolean isValidPartition() { 69 | return currentPartition >= 0 && currentPartition < Node.PARTITIONS; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/async/SecondaryIndexQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.async; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.policy.QueryPolicy; 5 | import com.aerospike.jdbc.model.AerospikeQuery; 6 | import com.aerospike.jdbc.model.AerospikeSecondaryIndex; 7 | import com.aerospike.jdbc.model.DriverPolicy; 8 | 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | 12 | public class SecondaryIndexQueryHandler { 13 | 14 | private final IAerospikeClient client; 15 | private final RecordSetRecordSequenceListener listener; 16 | 17 | public SecondaryIndexQueryHandler(IAerospikeClient client, DriverPolicy driverPolicy) { 18 | this.client = client; 19 | this.listener = new RecordSetRecordSequenceListener(driverPolicy); 20 | } 21 | 22 | public static SecondaryIndexQueryHandler create(IAerospikeClient client, DriverPolicy driverPolicy) { 23 | return new SecondaryIndexQueryHandler(client, driverPolicy); 24 | } 25 | 26 | public RecordSet execute(QueryPolicy queryPolicy, AerospikeQuery query, 27 | AerospikeSecondaryIndex secondaryIndex) { 28 | com.aerospike.client.query.Statement statement = new com.aerospike.client.query.Statement(); 29 | Optional.ofNullable(query.getLimit()).ifPresent(statement::setMaxRecords); 30 | statement.setRecordsPerSecond(client.getScanPolicyDefault().recordsPerSecond); 31 | 32 | statement.setIndexName(secondaryIndex.getIndexName()); 33 | statement.setNamespace(query.getCatalog()); 34 | statement.setSetName(query.getTable()); 35 | statement.setBinNames(query.columnBins()); 36 | 37 | if (Objects.nonNull(query.getPredicate())) { 38 | query.getPredicate().toFilter(secondaryIndex.getBinName()).ifPresent(statement::setFilter); 39 | } 40 | 41 | if (query.isPrimaryKeyOnly()) { 42 | queryPolicy.includeBinData = false; 43 | } 44 | client.query(EventLoopProvider.getEventLoop(), listener, queryPolicy, statement); 45 | 46 | return listener.getRecordSet(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/AerospikeClusterInfo.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | public class AerospikeClusterInfo { 7 | 8 | private final String build; 9 | private final String edition; 10 | private final Collection catalogs; 11 | private final Map> tables; 12 | 13 | public AerospikeClusterInfo(String build, String edition, Collection catalogs, 14 | Map> tables) { 15 | this.build = build; 16 | this.edition = edition; 17 | this.catalogs = catalogs; 18 | this.tables = tables; 19 | } 20 | 21 | public String getBuild() { 22 | return build; 23 | } 24 | 25 | public String getEdition() { 26 | return edition; 27 | } 28 | 29 | public Collection getCatalogs() { 30 | return catalogs; 31 | } 32 | 33 | public Map> getTables() { 34 | return tables; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/AerospikeQuery.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import com.aerospike.client.Txn; 4 | import com.aerospike.client.exp.Exp; 5 | import com.aerospike.client.exp.Expression; 6 | import com.aerospike.jdbc.predicate.QueryPredicate; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import com.google.common.annotations.VisibleForTesting; 10 | import org.apache.calcite.avatica.util.Casing; 11 | import org.apache.calcite.sql.SqlNode; 12 | import org.apache.calcite.sql.parser.SqlParseException; 13 | import org.apache.calcite.sql.parser.SqlParser; 14 | import org.apache.calcite.sql.parser.ddl.SqlDdlParserImpl; 15 | 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.List; 19 | import java.util.Locale; 20 | import java.util.Objects; 21 | 22 | import static com.aerospike.jdbc.util.Constants.DEFAULT_SCHEMA_NAME; 23 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 24 | 25 | public class AerospikeQuery { 26 | 27 | @VisibleForTesting 28 | public static final SqlParser.Config sqlParserConfig = SqlParser.config() 29 | .withParserFactory(SqlDdlParserImpl.FACTORY) 30 | .withCaseSensitive(true) 31 | .withUnquotedCasing(Casing.UNCHANGED) 32 | .withQuotedCasing(Casing.UNCHANGED); 33 | 34 | private static final String ASTERISK = "*"; 35 | 36 | private String catalog; 37 | private String table; 38 | private QueryType queryType; 39 | private Integer offset; 40 | private Integer limit; 41 | private String index; 42 | 43 | private QueryPredicate predicate; 44 | private List values; 45 | private List columns; 46 | 47 | private Txn txn; 48 | 49 | public AerospikeQuery() { 50 | this.queryType = QueryType.UNKNOWN; 51 | } 52 | 53 | public static AerospikeQuery parse(String sql, List sqlParameters) throws SqlParseException { 54 | SqlParser parser = SqlParser.create(sql, sqlParserConfig); 55 | SqlNode parsed = parser.parseQuery(); 56 | return parsed.accept(new AerospikeSqlVisitor(sqlParameters)); 57 | } 58 | 59 | public String getCatalog() { 60 | return catalog; 61 | } 62 | 63 | public void setCatalog(String catalog) { 64 | this.catalog = catalog; 65 | } 66 | 67 | public String getTable() { 68 | return table; 69 | } 70 | 71 | public void setTable(String table) { 72 | String[] spec = table.split("\\."); 73 | switch (spec.length) { 74 | case 2: 75 | this.catalog = spec[0]; 76 | this.table = spec[1]; 77 | break; 78 | case 1: 79 | this.table = spec[0]; 80 | break; 81 | default: 82 | throw new IllegalArgumentException("Invalid table name"); 83 | } 84 | } 85 | 86 | public String getSetName() { 87 | if (table.equals(DEFAULT_SCHEMA_NAME)) { 88 | return null; 89 | } 90 | return table; 91 | } 92 | 93 | public CatalogTableName getCatalogTable() { 94 | return new CatalogTableName(catalog, table); 95 | } 96 | 97 | public QueryType getQueryType() { 98 | return queryType; 99 | } 100 | 101 | public void setQueryType(QueryType queryType) { 102 | this.queryType = queryType; 103 | } 104 | 105 | public Integer getOffset() { 106 | return offset; 107 | } 108 | 109 | public void setOffset(int offset) { 110 | this.offset = offset; 111 | } 112 | 113 | public Integer getLimit() { 114 | return limit; 115 | } 116 | 117 | public void setLimit(int limit) { 118 | this.limit = limit; 119 | } 120 | 121 | public String getIndex() { 122 | return index; 123 | } 124 | 125 | public void setIndex(String index) { 126 | this.index = index; 127 | } 128 | 129 | public QueryPredicate getPredicate() { 130 | return predicate; 131 | } 132 | 133 | public void setPredicate(QueryPredicate predicate) { 134 | this.predicate = predicate; 135 | } 136 | 137 | public List getValues() { 138 | return values; 139 | } 140 | 141 | public void setValues(List values) { 142 | this.values = values; 143 | } 144 | 145 | public List getColumns() { 146 | return columns; 147 | } 148 | 149 | public void setColumns(List columns) { 150 | this.columns = columns; 151 | } 152 | 153 | public Txn getTxn() { 154 | return txn; 155 | } 156 | 157 | public void setTxn(Txn txn) { 158 | this.txn = txn; 159 | } 160 | 161 | public String[] columnBins() { 162 | String[] binNames = columns.stream() 163 | .filter(c -> !Objects.equals(c, ASTERISK)) 164 | .filter(c -> !Objects.equals(c, PRIMARY_KEY_COLUMN_NAME)) 165 | .toArray(String[]::new); 166 | return binNames.length == 0 ? null : binNames; 167 | } 168 | 169 | public Collection getPrimaryKeys() { 170 | if (predicate != null) { 171 | return predicate.getPrimaryKeys(); 172 | } 173 | return Collections.emptyList(); 174 | } 175 | 176 | public boolean isPrimaryKeyOnly() { 177 | return columns.size() == 1 && columns.get(0).equals(PRIMARY_KEY_COLUMN_NAME); 178 | } 179 | 180 | public boolean isStar() { 181 | return columns.stream().anyMatch(c -> c.equals(ASTERISK)); 182 | } 183 | 184 | public boolean isCount() { 185 | return columns.size() == 1 && columns.get(0).toLowerCase(Locale.ENGLISH).startsWith("count("); 186 | } 187 | 188 | public boolean isIndexable() { 189 | return Objects.nonNull(predicate) && predicate.isIndexable() && Objects.isNull(offset); 190 | } 191 | 192 | public Expression toFilterExpression(boolean withPrimaryKey) { 193 | if (predicate == null) { 194 | return null; 195 | } 196 | Exp exp = predicate.toFilterExpression(withPrimaryKey); 197 | return exp == null ? null : Exp.build(exp); 198 | } 199 | 200 | @Override 201 | public String toString() { 202 | try { 203 | return (new ObjectMapper()).writeValueAsString(this); 204 | } catch (JsonProcessingException e) { 205 | return getClass().getName() + "@" + Integer.toHexString(hashCode()); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/AerospikeSecondaryIndex.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import com.aerospike.client.query.IndexType; 4 | import com.fasterxml.jackson.annotation.JsonFormat; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | import java.lang.reflect.Field; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.stream.Collectors; 18 | 19 | public class AerospikeSecondaryIndex { 20 | 21 | @Retention(RetentionPolicy.RUNTIME) 22 | @Target(ElementType.FIELD) 23 | private @interface Order { 24 | int value(); 25 | } 26 | 27 | @Order(0) 28 | @JsonProperty("column_name") 29 | private final String binName; 30 | 31 | @Order(1) 32 | @JsonProperty("sindex_name") 33 | private final String indexName; 34 | 35 | @Order(2) 36 | @JsonFormat(shape = JsonFormat.Shape.STRING) 37 | @JsonProperty("sindex_type") 38 | private final IndexType indexType; 39 | 40 | @Order(3) 41 | @JsonProperty("catalog_name") 42 | private final String namespace; 43 | 44 | @Order(4) 45 | @JsonProperty("table_name") 46 | private final String set; 47 | 48 | @Order(5) 49 | @JsonProperty("sindex_ratio") 50 | private final Integer binValuesRatio; 51 | 52 | public AerospikeSecondaryIndex( 53 | String namespace, 54 | String set, 55 | String binName, 56 | String indexName, 57 | IndexType indexType, 58 | Integer binValuesRatio) { 59 | this.namespace = namespace; 60 | this.set = set; 61 | this.binName = binName; 62 | this.indexName = indexName; 63 | this.indexType = indexType; 64 | this.binValuesRatio = binValuesRatio; 65 | } 66 | 67 | public String getBinName() { 68 | return binName; 69 | } 70 | 71 | public String getIndexName() { 72 | return indexName; 73 | } 74 | 75 | public IndexType getIndexType() { 76 | return indexType; 77 | } 78 | 79 | public String getNamespace() { 80 | return namespace; 81 | } 82 | 83 | public String getSet() { 84 | return set; 85 | } 86 | 87 | public Integer getBinValuesRatio() { 88 | return binValuesRatio; 89 | } 90 | 91 | public String toKey() { 92 | return key(namespace, set, binName); 93 | } 94 | 95 | public static String key(String namespace, String set, String binName) { 96 | return String.format("%s.%s.%s", namespace, set, binName); 97 | } 98 | 99 | @SuppressWarnings("all") 100 | public Map toMap() { 101 | ObjectMapper mapper = new ObjectMapper(); 102 | return (Map) mapper.convertValue(this, Map.class); 103 | } 104 | 105 | public static List getOrderedFieldNames() { 106 | Field[] fields = AerospikeSecondaryIndex.class.getDeclaredFields(); 107 | Arrays.sort(fields, (o1, o2) -> { 108 | Order order1 = o1.getAnnotation(Order.class); 109 | Order order2 = o2.getAnnotation(Order.class); 110 | if (order1 != null && order2 != null) { 111 | return order1.value() - order2.value(); 112 | } else if (order1 != null) { 113 | return -1; 114 | } else if (order2 != null) { 115 | return 1; 116 | } 117 | return o1.getName().compareTo(o2.getName()); 118 | }); 119 | return Arrays.stream(fields).map(field -> { 120 | JsonProperty columnName = field.getAnnotation(JsonProperty.class); 121 | if (columnName != null) { 122 | return columnName.value(); 123 | } 124 | return field.getName(); 125 | }).collect(Collectors.toList()); 126 | } 127 | 128 | @Override 129 | public boolean equals(Object obj) { 130 | if (obj == null) { 131 | return false; 132 | } 133 | if (getClass() != obj.getClass()) { 134 | return false; 135 | } 136 | final AerospikeSecondaryIndex other = (AerospikeSecondaryIndex) obj; 137 | return Objects.equals(binName, other.binName) 138 | && Objects.equals(indexName, other.indexName) 139 | && Objects.equals(indexType, other.indexType) 140 | && Objects.equals(namespace, other.namespace) 141 | && Objects.equals(set, other.set); 142 | } 143 | 144 | @Override 145 | public int hashCode() { 146 | return Objects.hash(binName, indexName, indexType, namespace, set); 147 | } 148 | 149 | @Override 150 | public String toString() { 151 | return String.format("%s(%s, %s, %s, %s, %s)", getClass().getSimpleName(), 152 | binName, indexName, indexType, namespace, set); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/CatalogTableName.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import java.util.Objects; 4 | 5 | public class CatalogTableName { 6 | 7 | private final String catalogName; 8 | private final String tableName; 9 | 10 | public CatalogTableName(String catalogName, String tableName) { 11 | this.catalogName = catalogName; 12 | this.tableName = tableName; 13 | } 14 | 15 | public String getCatalogName() { 16 | return catalogName; 17 | } 18 | 19 | public String getTableName() { 20 | return tableName; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) return true; 26 | if (o == null || getClass() != o.getClass()) return false; 27 | CatalogTableName that = (CatalogTableName) o; 28 | return Objects.equals(catalogName, that.catalogName) && 29 | Objects.equals(tableName, that.tableName); 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return Objects.hash(catalogName, tableName); 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return String.format("%s(%s, %s)", getClass().getSimpleName(), catalogName, tableName); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/DataColumn.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import java.util.Objects; 4 | 5 | public class DataColumn { 6 | 7 | private final String name; 8 | private String catalog; 9 | private String table; 10 | private String label; 11 | private int type; 12 | 13 | public DataColumn(String catalog, String table, String name, String label) { 14 | this.catalog = catalog; 15 | this.table = table; 16 | this.name = name; 17 | this.label = label; 18 | } 19 | 20 | public DataColumn(String catalog, String table, int type, String name, String label) { 21 | this(catalog, table, name, label); 22 | this.type = type; 23 | } 24 | 25 | public DataColumn withCatalog(String catalog) { 26 | this.catalog = catalog; 27 | return this; 28 | } 29 | 30 | public DataColumn withTable(String table) { 31 | this.table = table; 32 | return this; 33 | } 34 | 35 | public DataColumn withType(int type) { 36 | this.type = type; 37 | return this; 38 | } 39 | 40 | public DataColumn withLabel(String label) { 41 | this.label = label; 42 | return this; 43 | } 44 | 45 | public String getCatalog() { 46 | return catalog; 47 | } 48 | 49 | public String getTable() { 50 | return table; 51 | } 52 | 53 | public String getName() { 54 | return name; 55 | } 56 | 57 | public String getLabel() { 58 | return label; 59 | } 60 | 61 | public int getType() { 62 | return type; 63 | } 64 | 65 | public CatalogTableName getSchemaTableName() { 66 | return new CatalogTableName(catalog, table); 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) return true; 72 | if (o == null || getClass() != o.getClass()) return false; 73 | DataColumn that = (DataColumn) o; 74 | return type == that.type && 75 | Objects.equals(catalog, that.catalog) && 76 | Objects.equals(table, that.table) && 77 | Objects.equals(name, that.name) && 78 | Objects.equals(label, that.label); 79 | } 80 | 81 | @Override 82 | public int hashCode() { 83 | return Objects.hash(catalog, table, name, label, type); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return String.format("%s(%s, %s, %s, %s, %d)", 89 | getClass().getSimpleName(), name, catalog, table, label, type); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/DriverPolicy.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | import java.util.Properties; 4 | 5 | public class DriverPolicy { 6 | 7 | private static final int DEFAULT_RECORD_SET_QUEUE_CAPACITY = 256; 8 | private static final int DEFAULT_RECORD_SET_TIMEOUT_MS = 1000; 9 | private static final int DEFAULT_METADATA_CACHE_TTL_SECONDS = 3600; 10 | private static final int DEFAULT_SCHEMA_BUILDER_MAX_RECORDS = 1000; 11 | 12 | private final int recordSetQueueCapacity; 13 | private final int recordSetTimeoutMs; 14 | private final int metadataCacheTtlSeconds; 15 | private final int schemaBuilderMaxRecords; 16 | private final int txnTimeoutSeconds; 17 | private final boolean showRecordMetadata; 18 | 19 | public DriverPolicy(Properties properties) { 20 | recordSetQueueCapacity = parseInt(properties.getProperty("recordSetQueueCapacity"), 21 | DEFAULT_RECORD_SET_QUEUE_CAPACITY); 22 | recordSetTimeoutMs = parseInt(properties.getProperty("recordSetTimeoutMs"), 23 | DEFAULT_RECORD_SET_TIMEOUT_MS); 24 | metadataCacheTtlSeconds = parseInt(properties.getProperty("metadataCacheTtlSeconds"), 25 | DEFAULT_METADATA_CACHE_TTL_SECONDS); 26 | schemaBuilderMaxRecords = parseInt(properties.getProperty("schemaBuilderMaxRecords"), 27 | DEFAULT_SCHEMA_BUILDER_MAX_RECORDS); 28 | txnTimeoutSeconds = parseInt(properties.getProperty("txnTimeoutSeconds"), 0); 29 | showRecordMetadata = Boolean.parseBoolean(properties.getProperty("showRecordMetadata")); 30 | } 31 | 32 | public int getRecordSetQueueCapacity() { 33 | return recordSetQueueCapacity; 34 | } 35 | 36 | public int getRecordSetTimeoutMs() { 37 | return recordSetTimeoutMs; 38 | } 39 | 40 | public int getMetadataCacheTtlSeconds() { 41 | return metadataCacheTtlSeconds; 42 | } 43 | 44 | public int getSchemaBuilderMaxRecords() { 45 | return schemaBuilderMaxRecords; 46 | } 47 | 48 | public int getTxnTimeoutSeconds() { 49 | return txnTimeoutSeconds; 50 | } 51 | 52 | public boolean getShowRecordMetadata() { 53 | return showRecordMetadata; 54 | } 55 | 56 | private int parseInt(String value, int defaultValue) { 57 | if (value != null) { 58 | return Integer.parseInt(value); 59 | } 60 | return defaultValue; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/Pair.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | public class Pair { 4 | 5 | private final L left; 6 | private final R right; 7 | 8 | public Pair(L left, R right) { 9 | this.left = left; 10 | this.right = right; 11 | } 12 | 13 | public static Pair of(L left, R right) { 14 | return new Pair<>(left, right); 15 | } 16 | 17 | public L getLeft() { 18 | return left; 19 | } 20 | 21 | public R getRight() { 22 | return right; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/model/QueryType.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.model; 2 | 3 | public enum QueryType { 4 | SHOW_CATALOGS, 5 | SHOW_SCHEMAS, 6 | SHOW_TABLES, 7 | SHOW_COLUMNS, 8 | DROP_SCHEMA, 9 | DROP_TABLE, 10 | CREATE_INDEX, 11 | DROP_INDEX, 12 | SELECT, 13 | INSERT, 14 | UPDATE, 15 | DELETE, 16 | UNKNOWN 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/Operator.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import org.apache.calcite.sql.SqlKind; 5 | import org.apache.calcite.sql.SqlOperator; 6 | 7 | import static com.aerospike.jdbc.predicate.OperatorBinary.*; 8 | import static com.aerospike.jdbc.predicate.OperatorUnary.NOT; 9 | import static com.aerospike.jdbc.predicate.OperatorVarArgs.IN; 10 | 11 | public interface Operator { 12 | 13 | Exp exp(Exp... expressions); 14 | 15 | static Operator parsed(SqlOperator op) { 16 | if (op.kind == SqlKind.EQUALS) return EQ; 17 | if (op.kind == SqlKind.LESS_THAN) return LT; 18 | if (op.kind == SqlKind.LESS_THAN_OR_EQUAL) return LE; 19 | if (op.kind == SqlKind.GREATER_THAN) return GT; 20 | if (op.kind == SqlKind.GREATER_THAN_OR_EQUAL) return GE; 21 | if (op.kind == SqlKind.NOT_EQUALS) return NE; 22 | 23 | if (op.kind == SqlKind.OR) return OR; 24 | if (op.kind == SqlKind.AND) return AND; 25 | 26 | if (op.kind == SqlKind.NOT) return NOT; 27 | 28 | if (op.kind == SqlKind.IN) return IN; 29 | 30 | throw new UnsupportedOperationException("Unsupported operator type"); 31 | } 32 | 33 | static boolean isBoolean(Operator operator) { 34 | return operator == OR || operator == AND; 35 | } 36 | 37 | static boolean isVarArgs(Operator operator) { 38 | return operator == IN; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/OperatorBinary.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.google.common.base.Preconditions; 5 | 6 | import java.util.function.BinaryOperator; 7 | 8 | public enum OperatorBinary implements Operator { 9 | 10 | EQ(Exp::eq), 11 | LT(Exp::lt), 12 | LE(Exp::le), 13 | GT(Exp::gt), 14 | GE(Exp::ge), 15 | NE((exp1, exp2) -> Exp.not(Exp.eq(exp1, exp2))), 16 | 17 | OR(Exp::or), 18 | AND(Exp::and); 19 | 20 | private final BinaryOperator func; 21 | 22 | OperatorBinary(BinaryOperator expFunc) { 23 | this.func = expFunc; 24 | } 25 | 26 | @Override 27 | public Exp exp(Exp... expressions) { 28 | Preconditions.checkArgument(expressions.length == 2); 29 | return func.apply(expressions[0], expressions[1]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/OperatorUnary.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.google.common.base.Preconditions; 5 | 6 | import java.util.function.UnaryOperator; 7 | 8 | public enum OperatorUnary implements Operator { 9 | 10 | NOT(Exp::not); 11 | 12 | private final UnaryOperator func; 13 | 14 | OperatorUnary(UnaryOperator expFunc) { 15 | this.func = expFunc; 16 | } 17 | 18 | @Override 19 | public Exp exp(Exp... expressions) { 20 | Preconditions.checkArgument(expressions.length == 1); 21 | return func.apply(expressions[0]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/OperatorVarArgs.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | 5 | public enum OperatorVarArgs implements Operator { 6 | 7 | IN(Exp::or); 8 | 9 | private final VarArgsFunction func; 10 | 11 | OperatorVarArgs(VarArgsFunction expFunc) { 12 | this.func = expFunc; 13 | } 14 | 15 | @Override 16 | public Exp exp(Exp... expressions) { 17 | return func.apply(expressions); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicate.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public interface QueryPredicate { 12 | 13 | Exp toFilterExpression(boolean withPrimaryKey); 14 | 15 | Optional toFilter(String binName); 16 | 17 | boolean isIndexable(); 18 | 19 | List getBinNames(); 20 | 21 | default Collection getPrimaryKeys() { 22 | return Collections.emptyList(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateBase.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 9 | 10 | public abstract class QueryPredicateBase implements QueryPredicate { 11 | 12 | protected final String binName; 13 | protected final Exp.Type valueType; 14 | 15 | protected QueryPredicateBase( 16 | String binName, 17 | Exp.Type valueType 18 | ) { 19 | this.binName = binName; 20 | this.valueType = valueType; 21 | } 22 | 23 | protected static Exp.Type getValueType(Object value) { 24 | if (value == null) { 25 | return Exp.Type.NIL; 26 | } else if (value instanceof String) { 27 | return Exp.Type.STRING; 28 | } else if (value instanceof Long || value instanceof Integer 29 | || value instanceof Short || value instanceof Byte) { 30 | return Exp.Type.INT; 31 | } else if (value instanceof Double || value instanceof Float) { 32 | return Exp.Type.FLOAT; 33 | } else if (value instanceof Boolean) { 34 | return Exp.Type.BOOL; 35 | } else if (value instanceof byte[]) { 36 | return Exp.Type.BLOB; 37 | } else { 38 | return Exp.Type.STRING; 39 | } 40 | } 41 | 42 | protected Exp getValueExp(Object value) { 43 | if (value == null) { 44 | return Exp.nil(); 45 | } else if (value instanceof String) { 46 | return Exp.val((String) value); 47 | } else if (value instanceof Long) { 48 | return Exp.val((long) value); 49 | } else if (value instanceof Integer) { 50 | return Exp.val((int) value); 51 | } else if (value instanceof Short) { 52 | return Exp.val((short) value); 53 | } else if (value instanceof Byte) { 54 | return Exp.val((byte) value); 55 | } else if (value instanceof Double) { 56 | return Exp.val((double) value); 57 | } else if (value instanceof Float) { 58 | return Exp.val((float) value); 59 | } else if (value instanceof Boolean) { 60 | return Exp.val((boolean) value); 61 | } else if (value instanceof byte[]) { 62 | return Exp.val((byte[]) value); 63 | } else { 64 | return Exp.val(value.toString()); 65 | } 66 | } 67 | 68 | protected boolean isPrimaryKeyPredicate() { 69 | return binName.equals(PRIMARY_KEY_COLUMN_NAME); 70 | } 71 | 72 | protected Exp buildLeftExp() { 73 | return isPrimaryKeyPredicate() 74 | ? Exp.key(valueType) 75 | : Exp.bin(binName, valueType); 76 | } 77 | 78 | @Override 79 | public boolean isIndexable() { 80 | return true; 81 | } 82 | 83 | @Override 84 | public List getBinNames() { 85 | if (isIndexable()) { 86 | return Collections.singletonList(binName); 87 | } 88 | return Collections.emptyList(); 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return getClass().getSimpleName() + "(" + binName + ")"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateBinary.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.Optional; 9 | 10 | public class QueryPredicateBinary extends QueryPredicateBase { 11 | 12 | private final Operator operator; 13 | private final Object value; 14 | 15 | public QueryPredicateBinary(String binName, Operator operator, Object value) { 16 | super(binName, getValueType(value)); 17 | this.operator = operator; 18 | this.value = value; 19 | } 20 | 21 | @Override 22 | public Exp toFilterExpression(boolean withPrimaryKey) { 23 | if (isPrimaryKeyPredicate() && !withPrimaryKey) { 24 | return null; 25 | } 26 | return operator.exp(buildLeftExp(), getValueExp(value)); 27 | } 28 | 29 | @Override 30 | public Optional toFilter(String binName) { 31 | if (binName.equals(this.binName) && operator == OperatorBinary.EQ) { 32 | if (valueType == Exp.Type.INT) { 33 | return Optional.of(Filter.equal(binName, (long) value)); 34 | } else if (valueType == Exp.Type.STRING) { 35 | return Optional.of(Filter.equal(binName, (String) value)); 36 | } 37 | } 38 | return Optional.empty(); 39 | } 40 | 41 | @Override 42 | public Collection getPrimaryKeys() { 43 | if (isPrimaryKeyPredicate() && operator == OperatorBinary.EQ) { 44 | return Collections.singletonList(value); 45 | } 46 | return Collections.emptyList(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateBoolean.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | public class QueryPredicateBoolean implements QueryPredicate { 14 | 15 | private final QueryPredicate left; 16 | private final Operator operator; 17 | private final QueryPredicate right; 18 | 19 | public QueryPredicateBoolean( 20 | QueryPredicate left, 21 | Operator operator, 22 | QueryPredicate right 23 | ) { 24 | this.left = left; 25 | this.operator = operator; 26 | this.right = right; 27 | } 28 | 29 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 30 | private static Optional or(Optional optional, Optional fallback) { 31 | return optional.isPresent() ? optional : fallback; 32 | } 33 | 34 | @Override 35 | public Exp toFilterExpression(boolean withPrimaryKey) { 36 | Exp leftExp = left.toFilterExpression(withPrimaryKey); 37 | Exp rightExp = right.toFilterExpression(withPrimaryKey); 38 | if (leftExp == null || rightExp == null) { 39 | return leftExp == null ? rightExp : leftExp; 40 | } 41 | return operator.exp(leftExp, rightExp); 42 | } 43 | 44 | @Override 45 | public Optional toFilter(String binName) { 46 | if (isIndexable()) { 47 | // TODO: support range filters 48 | return or( 49 | left.toFilter(binName), 50 | right.toFilter(binName) 51 | ); 52 | } 53 | return Optional.empty(); 54 | } 55 | 56 | @Override 57 | public boolean isIndexable() { 58 | return operator == OperatorBinary.AND && (left.isIndexable() || right.isIndexable()); 59 | } 60 | 61 | @Override 62 | public List getBinNames() { 63 | if (isIndexable()) { 64 | return Stream.concat(left.getBinNames().stream(), right.getBinNames().stream()) 65 | .collect(Collectors.toList()); 66 | } 67 | return Collections.emptyList(); 68 | } 69 | 70 | @Override 71 | public Collection getPrimaryKeys() { 72 | if (operator == OperatorBinary.AND) { 73 | return Stream.concat(left.getPrimaryKeys().stream(), right.getPrimaryKeys().stream()) 74 | .collect(Collectors.toSet()); 75 | } 76 | return Collections.emptyList(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateIsNotNull.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 11 | 12 | public class QueryPredicateIsNotNull implements QueryPredicate { 13 | 14 | private final String binName; 15 | 16 | public QueryPredicateIsNotNull(String binName) { 17 | this.binName = binName; 18 | } 19 | 20 | @Override 21 | public Exp toFilterExpression(boolean withPrimaryKey) { 22 | return binName.equals(PRIMARY_KEY_COLUMN_NAME) 23 | ? Exp.keyExists() 24 | : Exp.binExists(binName); 25 | } 26 | 27 | @Override 28 | public Optional toFilter(String binName) { 29 | return Optional.empty(); 30 | } 31 | 32 | @Override 33 | public boolean isIndexable() { 34 | return false; 35 | } 36 | 37 | @Override 38 | public List getBinNames() { 39 | return Collections.emptyList(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateIsNull.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 11 | 12 | public class QueryPredicateIsNull implements QueryPredicate { 13 | 14 | private final String binName; 15 | 16 | public QueryPredicateIsNull(String binName) { 17 | this.binName = binName; 18 | } 19 | 20 | @Override 21 | public Exp toFilterExpression(boolean withPrimaryKey) { 22 | return Exp.not( 23 | binName.equals(PRIMARY_KEY_COLUMN_NAME) 24 | ? Exp.keyExists() 25 | : Exp.binExists(binName) 26 | ); 27 | } 28 | 29 | @Override 30 | public Optional toFilter(String binName) { 31 | return Optional.empty(); 32 | } 33 | 34 | @Override 35 | public boolean isIndexable() { 36 | return false; 37 | } 38 | 39 | @Override 40 | public List getBinNames() { 41 | return Collections.emptyList(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateLike.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | import com.aerospike.client.query.RegexFlag; 6 | 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public class QueryPredicateLike extends QueryPredicateBase { 12 | 13 | private final String expression; 14 | 15 | public QueryPredicateLike(String binName, String expression) { 16 | super(binName, Exp.Type.STRING); 17 | this.expression = expression.replace("%", ".*"); 18 | } 19 | 20 | @Override 21 | public Exp toFilterExpression(boolean withPrimaryKey) { 22 | return Exp.regexCompare( 23 | expression, 24 | RegexFlag.ICASE | RegexFlag.NEWLINE, 25 | buildLeftExp() 26 | ); 27 | } 28 | 29 | @Override 30 | public Optional toFilter(String binName) { 31 | return Optional.empty(); 32 | } 33 | 34 | @Override 35 | public boolean isIndexable() { 36 | return false; 37 | } 38 | 39 | @Override 40 | public List getBinNames() { 41 | return Collections.emptyList(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateList.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.Optional; 10 | 11 | public class QueryPredicateList extends QueryPredicateBase { 12 | 13 | private final Operator operator; 14 | private final Object[] values; 15 | 16 | public QueryPredicateList(String binName, Operator operator, Object[] values) { 17 | super(binName, getValueType(values[0])); 18 | this.operator = operator; 19 | this.values = values; 20 | } 21 | 22 | @Override 23 | public Exp toFilterExpression(boolean withPrimaryKey) { 24 | if (isPrimaryKeyPredicate() && !withPrimaryKey) { 25 | return null; 26 | } 27 | return operator.exp( 28 | Arrays.stream(values) 29 | .map(v -> Exp.eq(buildLeftExp(), getValueExp(v))) 30 | .toArray(Exp[]::new) 31 | ); 32 | } 33 | 34 | @Override 35 | public Optional toFilter(String binName) { 36 | return Optional.empty(); 37 | } 38 | 39 | @Override 40 | public boolean isIndexable() { 41 | return false; 42 | } 43 | 44 | @Override 45 | public Collection getPrimaryKeys() { 46 | if (isPrimaryKeyPredicate()) { 47 | return Arrays.asList(values); 48 | } 49 | return Collections.emptyList(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicatePrefix.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public class QueryPredicatePrefix implements QueryPredicate { 11 | 12 | private final Operator operator; 13 | private final QueryPredicate right; 14 | 15 | public QueryPredicatePrefix( 16 | Operator operator, 17 | QueryPredicate right 18 | ) { 19 | this.operator = operator; 20 | this.right = right; 21 | } 22 | 23 | @Override 24 | public Exp toFilterExpression(boolean withPrimaryKey) { 25 | Exp rightExp = right.toFilterExpression(withPrimaryKey); 26 | if (rightExp == null) { 27 | return null; 28 | } 29 | return operator.exp(rightExp); 30 | } 31 | 32 | @Override 33 | public Optional toFilter(String binName) { 34 | return Optional.empty(); 35 | } 36 | 37 | @Override 38 | public boolean isIndexable() { 39 | return false; 40 | } 41 | 42 | @Override 43 | public List getBinNames() { 44 | return Collections.emptyList(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/QueryPredicateRange.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | import com.aerospike.client.exp.Exp; 4 | import com.aerospike.client.query.Filter; 5 | import com.google.common.base.Preconditions; 6 | 7 | import java.util.Optional; 8 | 9 | public class QueryPredicateRange extends QueryPredicateBase { 10 | 11 | private final Object lowValue; 12 | private final Object highValue; 13 | 14 | public QueryPredicateRange( 15 | String binName, 16 | Object lowValue, 17 | Object highValue 18 | ) { 19 | super(binName, getValueType(lowValue)); 20 | Preconditions.checkState(getValueType(lowValue) == getValueType(highValue)); 21 | this.lowValue = lowValue; 22 | this.highValue = highValue; 23 | } 24 | 25 | @Override 26 | public Exp toFilterExpression(boolean withPrimaryKey) { 27 | // ANSI SQL defines the BETWEEN operator to be inclusive, 28 | // so both boundary values are included in the range. 29 | return Exp.and( 30 | Exp.ge(buildLeftExp(), getValueExp(lowValue)), 31 | Exp.le(buildLeftExp(), getValueExp(highValue)) 32 | ); 33 | } 34 | 35 | @Override 36 | public Optional toFilter(String binName) { 37 | if (binName.equals(this.binName) && valueType == Exp.Type.INT) { 38 | return Optional.of(Filter.range(binName, (long) lowValue, (long) highValue)); 39 | } 40 | return Optional.empty(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/predicate/VarArgsFunction.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.predicate; 2 | 3 | @FunctionalInterface 4 | interface VarArgsFunction { 5 | @SuppressWarnings("unchecked") 6 | R apply(T... args); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/BaseQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Bin; 5 | import com.aerospike.client.IAerospikeClient; 6 | import com.aerospike.client.Value; 7 | import com.aerospike.jdbc.AerospikeConnection; 8 | import com.aerospike.jdbc.AerospikeDatabaseMetadata; 9 | import com.aerospike.jdbc.model.AerospikeQuery; 10 | import com.aerospike.jdbc.model.DriverConfiguration; 11 | import com.aerospike.jdbc.sql.ListRecordSet; 12 | import com.aerospike.jdbc.util.AerospikeVersion; 13 | 14 | import java.sql.SQLException; 15 | import java.sql.Statement; 16 | import java.util.List; 17 | import java.util.concurrent.ExecutionException; 18 | import java.util.concurrent.Future; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | import static java.util.Collections.emptyList; 23 | 24 | public abstract class BaseQueryHandler implements QueryHandler { 25 | 26 | private static final Logger logger = Logger.getLogger(BaseQueryHandler.class.getName()); 27 | 28 | protected final IAerospikeClient client; 29 | protected final Statement statement; 30 | protected final PolicyBuilder policyBuilder; 31 | protected final DriverConfiguration config; 32 | protected final AerospikeVersion aerospikeVersion; 33 | protected final AerospikeDatabaseMetadata databaseMetadata; 34 | 35 | protected BaseQueryHandler(IAerospikeClient client, Statement statement) { 36 | this.client = client; 37 | this.statement = statement; 38 | policyBuilder = new PolicyBuilder(client); 39 | config = getConfiguration(); 40 | aerospikeVersion = getAerospikeVersion(); 41 | databaseMetadata = getDatabaseMetadata(); 42 | } 43 | 44 | protected Bin[] getBins(AerospikeQuery query) { 45 | List columns = query.getColumns(); 46 | Bin[] bins = new Bin[columns.size()]; 47 | for (int i = 0; i < columns.size(); i++) { 48 | bins[i] = new Bin(columns.get(i), Value.get(query.getValues().get(i))); 49 | } 50 | return bins; 51 | } 52 | 53 | protected ListRecordSet emptyRecordSet(AerospikeQuery query) { 54 | return new ListRecordSet(statement, query.getCatalog(), query.getTable(), 55 | emptyList(), emptyList()); 56 | } 57 | 58 | protected Integer getUpdateCount(Future updateCountFuture) { 59 | try { 60 | return updateCountFuture.get(); 61 | } catch (ExecutionException e) { 62 | logger.log(Level.SEVERE, "Future computation failure", e.getCause()); 63 | } catch (InterruptedException e) { 64 | logger.log(Level.SEVERE, "Thread was interrupted", e); 65 | } 66 | return 0; 67 | } 68 | 69 | protected void logAerospikeException(AerospikeException e) { 70 | logger.log(Level.SEVERE, "Aerospike operation failure", e); 71 | } 72 | 73 | private DriverConfiguration getConfiguration() { 74 | try { 75 | return ((AerospikeConnection) statement.getConnection()).getConfiguration(); 76 | } catch (SQLException e) { 77 | throw new IllegalStateException("Failed to get DriverConfiguration", e); 78 | } 79 | } 80 | 81 | private AerospikeVersion getAerospikeVersion() { 82 | try { 83 | return ((AerospikeConnection) statement.getConnection()).getAerospikeVersion(); 84 | } catch (SQLException e) { 85 | throw new IllegalStateException("Failed to get AerospikeVersion", e); 86 | } 87 | } 88 | 89 | private AerospikeDatabaseMetadata getDatabaseMetadata() { 90 | try { 91 | return (AerospikeDatabaseMetadata) statement.getConnection().getMetaData(); 92 | } catch (SQLException e) { 93 | throw new IllegalStateException("Failed to get AerospikeDatabaseMetadata", e); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/DeleteQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.IAerospikeClient; 5 | import com.aerospike.client.Key; 6 | import com.aerospike.client.Value; 7 | import com.aerospike.client.policy.ScanPolicy; 8 | import com.aerospike.client.policy.WritePolicy; 9 | import com.aerospike.jdbc.async.EventLoopProvider; 10 | import com.aerospike.jdbc.async.FutureDeleteListener; 11 | import com.aerospike.jdbc.async.RecordSetRecordSequenceListener; 12 | import com.aerospike.jdbc.model.AerospikeQuery; 13 | import com.aerospike.jdbc.model.Pair; 14 | 15 | import java.sql.ResultSet; 16 | import java.sql.Statement; 17 | import java.util.Collection; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | import java.util.logging.Logger; 20 | 21 | public class DeleteQueryHandler extends BaseQueryHandler { 22 | 23 | private static final Logger logger = Logger.getLogger(DeleteQueryHandler.class.getName()); 24 | 25 | public DeleteQueryHandler(IAerospikeClient client, Statement statement) { 26 | super(client, statement); 27 | } 28 | 29 | @Override 30 | public Pair execute(AerospikeQuery query) { 31 | Collection keyObjects = query.getPrimaryKeys(); 32 | final WritePolicy writePolicy = policyBuilder.buildWritePolicy(query); 33 | if (!keyObjects.isEmpty()) { 34 | logger.info("DELETE primary key"); 35 | FutureDeleteListener listener = new FutureDeleteListener(keyObjects.size()); 36 | for (Object keyObject : keyObjects) { 37 | Key key = new Key(query.getCatalog(), query.getSetName(), Value.get(keyObject)); 38 | try { 39 | client.delete(EventLoopProvider.getEventLoop(), listener, writePolicy, key); 40 | } catch (AerospikeException e) { 41 | logAerospikeException(e); 42 | listener.onFailure(e); 43 | } 44 | } 45 | return new Pair<>(emptyRecordSet(query), getUpdateCount(listener.getTotal())); 46 | } else { 47 | logger.info("DELETE scan"); 48 | RecordSetRecordSequenceListener listener = new RecordSetRecordSequenceListener(config.getDriverPolicy()); 49 | ScanPolicy scanPolicy = policyBuilder.buildScanPolicy(query); 50 | scanPolicy.includeBinData = false; 51 | 52 | client.scanAll(EventLoopProvider.getEventLoop(), listener, scanPolicy, query.getCatalog(), 53 | query.getSetName()); 54 | 55 | final WritePolicy deletePolicy = policyBuilder.buildDeleteWritePolicy(query); 56 | final AtomicInteger count = new AtomicInteger(); 57 | listener.getRecordSet().forEach(r -> { 58 | try { 59 | if (client.delete(deletePolicy, r.key)) 60 | count.incrementAndGet(); 61 | } catch (AerospikeException e) { 62 | logAerospikeException(e); 63 | } 64 | }); 65 | 66 | return new Pair<>(emptyRecordSet(query), count.get()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/IndexCreateHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.query.IndexType; 5 | import com.aerospike.jdbc.model.AerospikeQuery; 6 | import com.aerospike.jdbc.model.CatalogTableName; 7 | import com.aerospike.jdbc.model.Pair; 8 | 9 | import java.sql.ResultSet; 10 | import java.sql.Statement; 11 | import java.sql.Types; 12 | import java.util.logging.Logger; 13 | 14 | import static java.lang.String.format; 15 | 16 | public class IndexCreateHandler extends BaseQueryHandler { 17 | 18 | private static final Logger logger = Logger.getLogger(IndexCreateHandler.class.getName()); 19 | 20 | protected IndexCreateHandler(IAerospikeClient client, Statement statement) { 21 | super(client, statement); 22 | } 23 | 24 | @Override 25 | public Pair execute(AerospikeQuery query) { 26 | logger.info("CREATE INDEX statement"); 27 | if (query.getColumns().size() != 1) { 28 | throw new UnsupportedOperationException( 29 | format("Multi-column index is not supported, got: %s", query.getColumns())); 30 | } else { 31 | client.createIndex( 32 | null, 33 | query.getCatalog(), 34 | query.getTable(), 35 | query.getIndex(), 36 | query.getColumns().get(0), 37 | getIndexType(query)); 38 | } 39 | databaseMetadata.resetCatalogIndexes(); 40 | return new Pair<>(emptyRecordSet(query), 1); 41 | } 42 | 43 | private IndexType getIndexType(AerospikeQuery query) { 44 | final CatalogTableName catalogTableName = new CatalogTableName( 45 | query.getCatalog(), 46 | query.getTable() 47 | ); 48 | final String columnName = query.getColumns().get(0); 49 | return databaseMetadata.getSchemaBuilder().getSchema(catalogTableName).stream() 50 | .filter(dataColumn -> dataColumn.getName().equals(columnName)) 51 | .findFirst() 52 | .map(dataColumn -> { 53 | int columnType = dataColumn.getType(); 54 | switch (columnType) { 55 | case Types.VARCHAR: 56 | return IndexType.STRING; 57 | case Types.BIGINT: 58 | case Types.INTEGER: 59 | return IndexType.NUMERIC; 60 | default: 61 | throw new UnsupportedOperationException( 62 | format("Secondary index is not supported for type: %d", columnType)); 63 | } 64 | }) 65 | .orElseThrow(() -> new IllegalArgumentException(format("Column %s not found in %s", 66 | columnName, query.getTable()))); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/IndexDropHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.jdbc.model.AerospikeQuery; 5 | import com.aerospike.jdbc.model.Pair; 6 | 7 | import java.sql.ResultSet; 8 | import java.sql.Statement; 9 | import java.util.logging.Logger; 10 | 11 | public class IndexDropHandler extends BaseQueryHandler { 12 | 13 | private static final Logger logger = Logger.getLogger(IndexDropHandler.class.getName()); 14 | 15 | protected IndexDropHandler(IAerospikeClient client, Statement statement) { 16 | super(client, statement); 17 | } 18 | 19 | @Override 20 | public Pair execute(AerospikeQuery query) { 21 | logger.info("DROP INDEX statement"); 22 | client.dropIndex(null, query.getCatalog(), query.getTable(), query.getIndex()); 23 | 24 | databaseMetadata.resetCatalogIndexes(); 25 | return new Pair<>(emptyRecordSet(query), 1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/InsertQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.*; 4 | import com.aerospike.client.policy.BatchPolicy; 5 | import com.aerospike.client.policy.BatchWritePolicy; 6 | import com.aerospike.client.policy.WritePolicy; 7 | import com.aerospike.jdbc.async.EventLoopProvider; 8 | import com.aerospike.jdbc.async.FutureBatchOperateListListener; 9 | import com.aerospike.jdbc.async.FutureWriteListener; 10 | import com.aerospike.jdbc.model.AerospikeQuery; 11 | import com.aerospike.jdbc.model.Pair; 12 | 13 | import java.sql.ResultSet; 14 | import java.sql.Statement; 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.UUID; 19 | import java.util.logging.Logger; 20 | import java.util.stream.Collectors; 21 | 22 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 23 | 24 | public class InsertQueryHandler extends BaseQueryHandler { 25 | 26 | private static final Logger logger = Logger.getLogger(InsertQueryHandler.class.getName()); 27 | 28 | public InsertQueryHandler(IAerospikeClient client, Statement statement) { 29 | super(client, statement); 30 | } 31 | 32 | @Override 33 | public Pair execute(AerospikeQuery query) { 34 | if (aerospikeVersion.isBatchOpsSupported()) { 35 | logger.info("INSERT batch"); 36 | return putBatch(query); 37 | } 38 | logger.info("INSERT individual"); 39 | return putConsecutively(query); 40 | } 41 | 42 | public Pair putConsecutively(AerospikeQuery query) { 43 | List binNames = getBinNames(query); 44 | 45 | FutureWriteListener listener = new FutureWriteListener(query.getValues().size()); 46 | WritePolicy writePolicy = policyBuilder.buildCreateOnlyPolicy(query); 47 | 48 | for (Object aerospikeRecord : query.getValues()) { 49 | @SuppressWarnings("unchecked") 50 | List values = (List) aerospikeRecord; 51 | Value recordKey = extractInsertKey(query, values); 52 | Key key = new Key(query.getCatalog(), query.getSetName(), recordKey); 53 | Bin[] bins = buildBinArray(binNames, values); 54 | 55 | try { 56 | client.put(EventLoopProvider.getEventLoop(), listener, writePolicy, key, bins); 57 | } catch (AerospikeException e) { 58 | logAerospikeException(e); 59 | listener.onFailure(e); 60 | } 61 | } 62 | return new Pair<>(emptyRecordSet(query), getUpdateCount(listener.getTotal())); 63 | } 64 | 65 | public Pair putBatch(AerospikeQuery query) { 66 | List binNames = getBinNames(query); 67 | 68 | FutureBatchOperateListListener listener = new FutureBatchOperateListListener(); 69 | List batchRecords = new ArrayList<>(); 70 | BatchWritePolicy batchWritePolicy = policyBuilder.buildBatchCreateOnlyPolicy(); 71 | 72 | for (Object aerospikeRecord : query.getValues()) { 73 | @SuppressWarnings("unchecked") 74 | List values = (List) aerospikeRecord; 75 | Value recordKey = extractInsertKey(query, values); 76 | Key key = new Key(query.getCatalog(), query.getSetName(), recordKey); 77 | batchRecords.add( 78 | new BatchWrite( 79 | batchWritePolicy, 80 | key, 81 | Arrays.stream(buildBinArray(binNames, values)) 82 | .map(Operation::put) 83 | .toArray(Operation[]::new) 84 | ) 85 | ); 86 | } 87 | BatchPolicy batchPolicy = policyBuilder.buildBatchPolicyDefault(query); 88 | try { 89 | client.operate(EventLoopProvider.getEventLoop(), listener, batchPolicy, batchRecords); 90 | } catch (AerospikeException e) { 91 | // no error log as this completes the future exceptionally 92 | listener.onFailure(e); 93 | } 94 | 95 | return new Pair<>(emptyRecordSet(query), getUpdateCount(listener.getTotal())); 96 | } 97 | 98 | protected Bin[] buildBinArray(List binNames, List values) { 99 | Bin[] bins = new Bin[binNames.size()]; 100 | for (int i = 0; i < binNames.size(); i++) { 101 | bins[i] = new Bin(binNames.get(i), Value.get(values.get(i))); 102 | } 103 | return bins; 104 | } 105 | 106 | private List getBinNames(AerospikeQuery query) { 107 | return query.getColumns().stream() 108 | .filter(c -> !c.equals(PRIMARY_KEY_COLUMN_NAME)) 109 | .collect(Collectors.toList()); 110 | } 111 | 112 | private Value extractInsertKey(AerospikeQuery query, List values) { 113 | List columns = query.getColumns(); 114 | for (int i = 0; i < columns.size(); i++) { 115 | if (columns.get(i).equals(PRIMARY_KEY_COLUMN_NAME)) { 116 | Object key = values.get(i); 117 | values.remove(i); 118 | if (key == null) break; 119 | return Value.get(key); 120 | } 121 | } 122 | return Value.get(UUID.randomUUID().toString()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/PolicyBuilder.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.policy.BatchPolicy; 5 | import com.aerospike.client.policy.BatchReadPolicy; 6 | import com.aerospike.client.policy.BatchWritePolicy; 7 | import com.aerospike.client.policy.QueryPolicy; 8 | import com.aerospike.client.policy.RecordExistsAction; 9 | import com.aerospike.client.policy.ScanPolicy; 10 | import com.aerospike.client.policy.WritePolicy; 11 | import com.aerospike.jdbc.model.AerospikeQuery; 12 | 13 | import java.util.Objects; 14 | 15 | public class PolicyBuilder { 16 | 17 | protected final IAerospikeClient client; 18 | 19 | public PolicyBuilder(IAerospikeClient client) { 20 | this.client = client; 21 | } 22 | 23 | public ScanPolicy buildScanPolicy(AerospikeQuery query) { 24 | ScanPolicy scanPolicy = new ScanPolicy(client.getScanPolicyDefault()); 25 | scanPolicy.maxRecords = Objects.isNull(query.getLimit()) ? 0 : query.getLimit(); 26 | scanPolicy.filterExp = query.toFilterExpression(true); 27 | return scanPolicy; 28 | } 29 | 30 | public QueryPolicy buildQueryPolicy(AerospikeQuery query) { 31 | QueryPolicy queryPolicy = new QueryPolicy(client.getQueryPolicyDefault()); 32 | queryPolicy.filterExp = query.toFilterExpression(true); 33 | return queryPolicy; 34 | } 35 | 36 | public ScanPolicy buildScanNoBinDataPolicy(AerospikeQuery query) { 37 | ScanPolicy scanPolicy = buildScanPolicy(query); 38 | scanPolicy.includeBinData = false; 39 | return scanPolicy; 40 | } 41 | 42 | public WritePolicy buildWritePolicy(AerospikeQuery query) { 43 | WritePolicy writePolicy = new WritePolicy(client.getWritePolicyDefault()); 44 | writePolicy.filterExp = query.toFilterExpression(true); 45 | writePolicy.txn = query.getTxn(); 46 | return writePolicy; 47 | } 48 | 49 | public WritePolicy buildDeleteWritePolicy(AerospikeQuery query) { 50 | WritePolicy writePolicy = new WritePolicy(client.getWritePolicyDefault()); 51 | writePolicy.sendKey = false; 52 | writePolicy.txn = query.getTxn(); 53 | return writePolicy; 54 | } 55 | 56 | public BatchPolicy buildBatchPolicyDefault(AerospikeQuery query) { 57 | BatchPolicy batchPolicy = new BatchPolicy(client.getBatchPolicyDefault()); 58 | batchPolicy.txn = query.getTxn(); 59 | return batchPolicy; 60 | } 61 | 62 | public BatchReadPolicy buildBatchReadPolicy(AerospikeQuery query) { 63 | BatchReadPolicy batchReadPolicy = new BatchReadPolicy(); 64 | batchReadPolicy.filterExp = query.toFilterExpression(false); 65 | return batchReadPolicy; 66 | } 67 | 68 | public BatchWritePolicy buildBatchCreateOnlyPolicy() { 69 | BatchWritePolicy batchWritePolicy = new BatchWritePolicy(); 70 | batchWritePolicy.recordExistsAction = RecordExistsAction.CREATE_ONLY; 71 | batchWritePolicy.sendKey = client.getBatchPolicyDefault().sendKey; 72 | batchWritePolicy.expiration = client.getBatchWritePolicyDefault().expiration; 73 | return batchWritePolicy; 74 | } 75 | 76 | public WritePolicy buildCreateOnlyPolicy(AerospikeQuery query) { 77 | WritePolicy writePolicy = new WritePolicy(client.getWritePolicyDefault()); 78 | writePolicy.recordExistsAction = RecordExistsAction.CREATE_ONLY; 79 | writePolicy.txn = query.getTxn(); 80 | return writePolicy; 81 | } 82 | 83 | public WritePolicy buildUpdateOnlyPolicy(AerospikeQuery query) { 84 | WritePolicy writePolicy = new WritePolicy(client.getWritePolicyDefault()); 85 | writePolicy.recordExistsAction = RecordExistsAction.UPDATE_ONLY; 86 | writePolicy.txn = query.getTxn(); 87 | return writePolicy; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/QueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.jdbc.model.AerospikeQuery; 4 | import com.aerospike.jdbc.model.Pair; 5 | 6 | import java.sql.ResultSet; 7 | 8 | public interface QueryHandler { 9 | 10 | Pair execute(AerospikeQuery query); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/QueryPerformer.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.jdbc.model.AerospikeQuery; 5 | import com.aerospike.jdbc.model.Pair; 6 | 7 | import java.sql.ResultSet; 8 | import java.sql.Statement; 9 | 10 | import static com.aerospike.jdbc.util.Constants.UNSUPPORTED_QUERY_TYPE_MESSAGE; 11 | 12 | public final class QueryPerformer { 13 | 14 | private QueryPerformer() { 15 | } 16 | 17 | public static Pair executeQuery( 18 | IAerospikeClient client, 19 | Statement statement, 20 | AerospikeQuery query 21 | ) { 22 | QueryHandler queryHandler; 23 | switch (query.getQueryType()) { 24 | case SELECT: 25 | queryHandler = new SelectQueryHandler(client, statement); 26 | return queryHandler.execute(query); 27 | 28 | case INSERT: 29 | queryHandler = new InsertQueryHandler(client, statement); 30 | return queryHandler.execute(query); 31 | 32 | case UPDATE: 33 | queryHandler = new UpdateQueryHandler(client, statement); 34 | return queryHandler.execute(query); 35 | 36 | case DELETE: 37 | queryHandler = new DeleteQueryHandler(client, statement); 38 | return queryHandler.execute(query); 39 | 40 | case DROP_TABLE: 41 | queryHandler = new TruncateQueryHandler(client, statement); 42 | return queryHandler.execute(query); 43 | 44 | case CREATE_INDEX: 45 | queryHandler = new IndexCreateHandler(client, statement); 46 | return queryHandler.execute(query); 47 | 48 | case DROP_INDEX: 49 | queryHandler = new IndexDropHandler(client, statement); 50 | return queryHandler.execute(query); 51 | 52 | default: 53 | throw new UnsupportedOperationException(UNSUPPORTED_QUERY_TYPE_MESSAGE); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/TruncateQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.jdbc.model.AerospikeQuery; 5 | import com.aerospike.jdbc.model.Pair; 6 | 7 | import java.sql.ResultSet; 8 | import java.sql.Statement; 9 | import java.util.logging.Logger; 10 | 11 | public class TruncateQueryHandler extends BaseQueryHandler { 12 | 13 | private static final Logger logger = Logger.getLogger(TruncateQueryHandler.class.getName()); 14 | 15 | public TruncateQueryHandler(IAerospikeClient client, Statement statement) { 16 | super(client, statement); 17 | } 18 | 19 | @Override 20 | public Pair execute(AerospikeQuery query) { 21 | logger.info("TRUNCATE/DROP statement"); 22 | client.truncate(null, query.getCatalog(), query.getSetName(), null); 23 | 24 | return new Pair<>(emptyRecordSet(query), 1); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/query/UpdateQueryHandler.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.query; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.Bin; 5 | import com.aerospike.client.IAerospikeClient; 6 | import com.aerospike.client.Key; 7 | import com.aerospike.client.Value; 8 | import com.aerospike.client.policy.ScanPolicy; 9 | import com.aerospike.client.policy.WritePolicy; 10 | import com.aerospike.jdbc.async.EventLoopProvider; 11 | import com.aerospike.jdbc.async.FutureWriteListener; 12 | import com.aerospike.jdbc.async.RecordSetRecordSequenceListener; 13 | import com.aerospike.jdbc.model.AerospikeQuery; 14 | import com.aerospike.jdbc.model.Pair; 15 | 16 | import java.sql.ResultSet; 17 | import java.sql.Statement; 18 | import java.util.Collection; 19 | import java.util.concurrent.atomic.AtomicInteger; 20 | import java.util.logging.Logger; 21 | 22 | public class UpdateQueryHandler extends BaseQueryHandler { 23 | 24 | private static final Logger logger = Logger.getLogger(UpdateQueryHandler.class.getName()); 25 | 26 | public UpdateQueryHandler(IAerospikeClient client, Statement statement) { 27 | super(client, statement); 28 | } 29 | 30 | @Override 31 | public Pair execute(AerospikeQuery query) { 32 | Collection keyObjects = query.getPrimaryKeys(); 33 | final Bin[] bins = getBins(query); 34 | final WritePolicy writePolicy = policyBuilder.buildUpdateOnlyPolicy(query); 35 | if (!keyObjects.isEmpty()) { 36 | logger.info("UPDATE primary key"); 37 | FutureWriteListener listener = new FutureWriteListener(keyObjects.size()); 38 | for (Object keyObject : keyObjects) { 39 | Key key = new Key(query.getCatalog(), query.getSetName(), Value.get(keyObject)); 40 | try { 41 | client.put(EventLoopProvider.getEventLoop(), listener, writePolicy, key, bins); 42 | } catch (AerospikeException e) { 43 | logAerospikeException(e); 44 | listener.onFailure(e); 45 | } 46 | } 47 | return new Pair<>(emptyRecordSet(query), getUpdateCount(listener.getTotal())); 48 | } else { 49 | logger.info("UPDATE scan"); 50 | RecordSetRecordSequenceListener listener = new RecordSetRecordSequenceListener(config.getDriverPolicy()); 51 | ScanPolicy scanPolicy = policyBuilder.buildScanPolicy(query); 52 | scanPolicy.includeBinData = false; 53 | client.scanAll(EventLoopProvider.getEventLoop(), listener, scanPolicy, query.getCatalog(), 54 | query.getSetName()); 55 | 56 | final AtomicInteger count = new AtomicInteger(); 57 | listener.getRecordSet().forEach(r -> { 58 | try { 59 | client.put(writePolicy, r.key, bins); 60 | count.incrementAndGet(); 61 | } catch (AerospikeException e) { 62 | logAerospikeException(e); 63 | } 64 | }); 65 | 66 | return new Pair<>(emptyRecordSet(query), count.get()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/schema/AerospikeSchemaBuilder.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.schema; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.Value; 5 | import com.aerospike.client.policy.ScanPolicy; 6 | import com.aerospike.jdbc.model.CatalogTableName; 7 | import com.aerospike.jdbc.model.DataColumn; 8 | import com.aerospike.jdbc.model.DriverPolicy; 9 | import com.aerospike.jdbc.model.Pair; 10 | 11 | import java.sql.Types; 12 | import java.time.Duration; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.TreeMap; 18 | import java.util.logging.Logger; 19 | 20 | import static com.aerospike.jdbc.util.Constants.DEFAULT_SCHEMA_NAME; 21 | import static com.aerospike.jdbc.util.Constants.METADATA_DIGEST_COLUMN_NAME; 22 | import static com.aerospike.jdbc.util.Constants.METADATA_GEN_COLUMN_NAME; 23 | import static com.aerospike.jdbc.util.Constants.METADATA_TTL_COLUMN_NAME; 24 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 25 | 26 | public final class AerospikeSchemaBuilder { 27 | 28 | private static final Logger logger = Logger.getLogger(AerospikeSchemaBuilder.class.getName()); 29 | 30 | private final IAerospikeClient client; 31 | private final DriverPolicy driverPolicy; 32 | private final AerospikeSchemaCache schemaCache; 33 | 34 | public AerospikeSchemaBuilder(IAerospikeClient client, DriverPolicy driverPolicy) { 35 | this.client = client; 36 | this.driverPolicy = driverPolicy; 37 | schemaCache = new AerospikeSchemaCache(Duration.ofSeconds(driverPolicy.getMetadataCacheTtlSeconds())); 38 | } 39 | 40 | public List getSchema(CatalogTableName catalogTableName) { 41 | return schemaCache.get(catalogTableName).orElseGet(() -> { 42 | logger.info(() -> "Fetching CatalogTableName: " + catalogTableName); 43 | final Map columnHandles = initColumnHandles(catalogTableName); 44 | 45 | ScanPolicy policy = new ScanPolicy(client.getScanPolicyDefault()); 46 | policy.maxRecords = driverPolicy.getSchemaBuilderMaxRecords(); 47 | 48 | client.scanAll(policy, catalogTableName.getCatalogName(), toSet(catalogTableName.getTableName()), 49 | (key, rec) -> { 50 | Map bins = rec.bins; 51 | if (bins != null) { 52 | bins.forEach((k, value) -> { 53 | logger.fine(() -> String.format("Bin: %s -> %s", k, value)); 54 | int t = getBinType(value); 55 | if (k != null && t != 0) { 56 | columnHandles.put(k, new DataColumn(catalogTableName.getCatalogName(), 57 | catalogTableName.getTableName(), t, k, k)); 58 | } 59 | }); 60 | } 61 | }); 62 | 63 | List columns = new ArrayList<>(columnHandles.values()); 64 | schemaCache.put(catalogTableName, columns); 65 | return columns; 66 | }); 67 | } 68 | 69 | private Map initColumnHandles(CatalogTableName catalogTableName) { 70 | final Map columnHandles = new TreeMap<>(String::compareToIgnoreCase); 71 | final List> metadataColumns = new ArrayList<>(); 72 | 73 | // add record key column handler 74 | metadataColumns.add(Pair.of(PRIMARY_KEY_COLUMN_NAME, Types.VARCHAR)); 75 | 76 | // add record metadata column handlers 77 | if (driverPolicy.getShowRecordMetadata()) { 78 | metadataColumns.addAll(Arrays.asList( 79 | Pair.of(METADATA_DIGEST_COLUMN_NAME, Types.VARCHAR), 80 | Pair.of(METADATA_GEN_COLUMN_NAME, Types.INTEGER), 81 | Pair.of(METADATA_TTL_COLUMN_NAME, Types.INTEGER) 82 | )); 83 | } 84 | 85 | for (Pair md : metadataColumns) { 86 | columnHandles.put(md.getLeft(), 87 | new DataColumn( 88 | catalogTableName.getCatalogName(), 89 | catalogTableName.getTableName(), 90 | md.getRight(), 91 | md.getLeft(), 92 | md.getLeft())); 93 | } 94 | return columnHandles; 95 | } 96 | 97 | private String toSet(String tableName) { 98 | if (tableName.equals(DEFAULT_SCHEMA_NAME)) { 99 | return null; 100 | } 101 | return tableName; 102 | } 103 | 104 | private int getBinType(Object value) { 105 | int t = 0; 106 | if (value instanceof byte[] || value instanceof Value.BytesValue || value instanceof Value.ByteSegmentValue) { 107 | t = Types.VARBINARY; 108 | } else if (value instanceof String || value instanceof Value.StringValue) { 109 | t = Types.VARCHAR; 110 | } else if (value instanceof Integer || value instanceof Value.IntegerValue) { 111 | t = Types.INTEGER; 112 | } else if (value instanceof Long || value instanceof Value.LongValue) { 113 | t = Types.BIGINT; 114 | } else if (value instanceof Double || value instanceof Value.DoubleValue) { 115 | t = Types.DOUBLE; 116 | } else if (value instanceof Float || value instanceof Value.FloatValue) { 117 | t = Types.FLOAT; 118 | } else if (value instanceof Boolean || value instanceof Value.BooleanValue) { 119 | t = Types.BOOLEAN; 120 | } else if (value instanceof Byte || value instanceof Value.ByteValue) { 121 | t = Types.TINYINT; 122 | } else if (value instanceof Value.HLLValue) { 123 | t = Types.BLOB; 124 | } else if (value instanceof Value.GeoJSONValue) { 125 | t = Types.BLOB; 126 | } else if (value instanceof List || value instanceof Value.ListValue) { 127 | t = Types.ARRAY; 128 | } else if (value instanceof Map || value instanceof Value.MapValue) { 129 | t = Types.OTHER; 130 | } else { 131 | logger.info(() -> "Unknown bin type: " + value); 132 | } 133 | return t; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/schema/AerospikeSchemaCache.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.schema; 2 | 3 | import com.aerospike.jdbc.model.CatalogTableName; 4 | import com.aerospike.jdbc.model.DataColumn; 5 | import com.google.common.cache.Cache; 6 | import com.google.common.cache.CacheBuilder; 7 | 8 | import java.time.Duration; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | public final class AerospikeSchemaCache 13 | implements OptionalCache> { 14 | 15 | private final Cache> store; 16 | 17 | public AerospikeSchemaCache(Duration ttl) { 18 | store = CacheBuilder.newBuilder().expireAfterWrite(ttl).build(); 19 | } 20 | 21 | @Override 22 | public Optional> get(CatalogTableName catalogTableName) { 23 | return Optional.ofNullable(store.getIfPresent(catalogTableName)); 24 | } 25 | 26 | @Override 27 | public void put(CatalogTableName catalogTableName, List columns) { 28 | store.put(catalogTableName, columns); 29 | } 30 | 31 | @Override 32 | public void clear() { 33 | store.invalidateAll(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/schema/OptionalCache.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.schema; 2 | 3 | import java.util.Optional; 4 | 5 | public interface OptionalCache { 6 | 7 | Optional get(K key); 8 | 9 | void put(K key, V value); 10 | 11 | void clear(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/AerospikeRecordResultSet.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql; 2 | 3 | import com.aerospike.client.Record; 4 | import com.aerospike.client.Value; 5 | import com.aerospike.jdbc.async.RecordSet; 6 | import com.aerospike.jdbc.model.DataColumn; 7 | import com.google.common.io.BaseEncoding; 8 | 9 | import java.math.BigDecimal; 10 | import java.sql.Statement; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.logging.Logger; 15 | import java.util.stream.Collectors; 16 | 17 | import static com.aerospike.jdbc.util.Constants.METADATA_DIGEST_COLUMN_NAME; 18 | import static com.aerospike.jdbc.util.Constants.METADATA_GEN_COLUMN_NAME; 19 | import static com.aerospike.jdbc.util.Constants.METADATA_TTL_COLUMN_NAME; 20 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 21 | 22 | public class AerospikeRecordResultSet extends BaseResultSet { 23 | 24 | private static final Logger logger = Logger.getLogger(AerospikeRecordResultSet.class.getName()); 25 | 26 | private final RecordSet recordSet; 27 | private final Set columnNames; 28 | 29 | public AerospikeRecordResultSet( 30 | RecordSet recordSet, 31 | Statement statement, 32 | String catalog, 33 | String table, 34 | List columns // columns list 35 | ) { 36 | super(statement, catalog, table, columns); 37 | this.recordSet = recordSet; 38 | this.columnNames = columns.stream().map(DataColumn::getName).collect(Collectors.toSet()); 39 | } 40 | 41 | @Override 42 | protected Record getRecord() { 43 | return recordSet.getRecord(); 44 | } 45 | 46 | @Override 47 | protected boolean moveToNext() { 48 | return recordSet.next(); 49 | } 50 | 51 | @Override 52 | public Object getObject(String columnLabel) { 53 | logger.fine(() -> "getObject: " + columnLabel); 54 | Object obj = getValue(columnLabel).map(Value::getObject).orElse(null); 55 | wasNull = obj == null; 56 | return obj; 57 | } 58 | 59 | @Override 60 | public String getString(String columnLabel) { 61 | logger.fine(() -> "getString: " + columnLabel); 62 | String str = getValue(columnLabel).map(Value::toString).orElse(null); 63 | wasNull = str == null; 64 | return str; 65 | } 66 | 67 | @Override 68 | public boolean getBoolean(String columnLabel) { 69 | logger.fine(() -> "getBoolean: " + columnLabel); 70 | return getValue(columnLabel).map(Value::toString).map(Boolean::parseBoolean).orElse(false); 71 | } 72 | 73 | @Override 74 | public byte getByte(String columnLabel) { 75 | logger.fine(() -> "getByte: " + columnLabel); 76 | return (byte) getInt(columnLabel); 77 | } 78 | 79 | @Override 80 | public short getShort(String columnLabel) { 81 | logger.fine(() -> "getShort: " + columnLabel); 82 | return (short) getInt(columnLabel); 83 | } 84 | 85 | @Override 86 | public int getInt(String columnLabel) { 87 | logger.fine(() -> "getInt: " + columnLabel); 88 | return getValue(columnLabel).map(Value::toString).map(Integer::parseInt).orElse(0); 89 | } 90 | 91 | @Override 92 | public long getLong(String columnLabel) { 93 | logger.fine(() -> "getLong: " + columnLabel); 94 | return getValue(columnLabel).map(Value::toString).map(Long::parseLong).orElse(0L); 95 | } 96 | 97 | @Override 98 | public float getFloat(String columnLabel) { 99 | logger.fine(() -> "getFloat: " + columnLabel); 100 | return (float) getDouble(columnLabel); 101 | } 102 | 103 | @Override 104 | public double getDouble(String columnLabel) { 105 | logger.fine(() -> "getDouble: " + columnLabel); 106 | return getValue(columnLabel).map(Value::toString).map(Double::parseDouble).orElse(0.0d); 107 | } 108 | 109 | @Override 110 | public BigDecimal getBigDecimal(String columnLabel, int scale) { 111 | logger.fine(() -> "getBigDecimal: " + columnLabel); 112 | return BigDecimal.valueOf(getLong(columnLabel)); 113 | } 114 | 115 | @Override 116 | public byte[] getBytes(String columnLabel) { 117 | logger.fine(() -> "getBytes: " + columnLabel); 118 | return getValue(columnLabel).map(Value::getObject).map(byte[].class::cast).orElse(null); 119 | } 120 | 121 | private Optional getValue(String columnLabel) { 122 | if (!columnNames.contains(columnLabel)) { 123 | return Optional.empty(); 124 | } 125 | switch (columnLabel) { 126 | case PRIMARY_KEY_COLUMN_NAME: 127 | return Optional.ofNullable(recordSet.getKey()) 128 | .map(key -> key.userKey); 129 | case METADATA_DIGEST_COLUMN_NAME: 130 | return Optional.ofNullable(recordSet.getKey()) 131 | .map(key -> BaseEncoding.base16().lowerCase().encode(key.digest)) 132 | .map(Value::get); 133 | case METADATA_TTL_COLUMN_NAME: 134 | return Optional.ofNullable(recordSet.getRecord()) 135 | .map(rec -> rec.expiration) 136 | .map(Value::get); 137 | case METADATA_GEN_COLUMN_NAME: 138 | return Optional.ofNullable(recordSet.getRecord()) 139 | .map(rec -> rec.generation) 140 | .map(Value::get); 141 | default: // regular bin value 142 | return Optional.ofNullable(recordSet.getRecord()) 143 | .map(rec -> rec.bins.get(columnLabel)) 144 | .map(Value::get); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/IndexToLabelResultSet.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql; 2 | 3 | import java.io.InputStream; 4 | import java.io.Reader; 5 | import java.math.BigDecimal; 6 | import java.math.RoundingMode; 7 | import java.net.URL; 8 | import java.sql.*; 9 | import java.util.Calendar; 10 | import java.util.Map; 11 | 12 | public interface IndexToLabelResultSet extends ResultSet { 13 | 14 | @Override 15 | default String getString(int columnIndex) throws SQLException { 16 | return getString(getColumnLabel(columnIndex)); 17 | } 18 | 19 | @Override 20 | default boolean getBoolean(int columnIndex) throws SQLException { 21 | return getBoolean(getColumnLabel(columnIndex)); 22 | } 23 | 24 | @Override 25 | default byte getByte(int columnIndex) throws SQLException { 26 | return getByte(getColumnLabel(columnIndex)); 27 | } 28 | 29 | @Override 30 | default short getShort(int columnIndex) throws SQLException { 31 | return getShort(getColumnLabel(columnIndex)); 32 | } 33 | 34 | @Override 35 | default int getInt(int columnIndex) throws SQLException { 36 | return getInt(getColumnLabel(columnIndex)); 37 | } 38 | 39 | @Override 40 | default long getLong(int columnIndex) throws SQLException { 41 | return getLong(getColumnLabel(columnIndex)); 42 | } 43 | 44 | @Override 45 | default float getFloat(int columnIndex) throws SQLException { 46 | return getFloat(getColumnLabel(columnIndex)); 47 | } 48 | 49 | @Override 50 | default double getDouble(int columnIndex) throws SQLException { 51 | return getDouble(getColumnLabel(columnIndex)); 52 | } 53 | 54 | @Override 55 | default BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { 56 | return getBigDecimal(getColumnLabel(columnIndex)).setScale(scale, RoundingMode.FLOOR); 57 | } 58 | 59 | @Override 60 | default byte[] getBytes(int columnIndex) throws SQLException { 61 | return getBytes(getColumnLabel(columnIndex)); 62 | } 63 | 64 | @Override 65 | default Date getDate(int columnIndex) throws SQLException { 66 | return getDate(getColumnLabel(columnIndex)); 67 | } 68 | 69 | @Override 70 | default Time getTime(int columnIndex) throws SQLException { 71 | return getTime(getColumnLabel(columnIndex)); 72 | } 73 | 74 | @Override 75 | default Timestamp getTimestamp(int columnIndex) throws SQLException { 76 | return getTimestamp(getColumnLabel(columnIndex)); 77 | } 78 | 79 | @Override 80 | default InputStream getAsciiStream(int columnIndex) throws SQLException { 81 | return getAsciiStream(getColumnLabel(columnIndex)); 82 | } 83 | 84 | /** 85 | * @deprecated use getCharacterStream in place of getUnicodeStream 86 | */ 87 | @Override 88 | @Deprecated 89 | @SuppressWarnings("java:S1133") 90 | default InputStream getUnicodeStream(int columnIndex) throws SQLException { 91 | return getUnicodeStream(getColumnLabel(columnIndex)); 92 | } 93 | 94 | @Override 95 | default InputStream getBinaryStream(int columnIndex) throws SQLException { 96 | return getBinaryStream(getColumnLabel(columnIndex)); 97 | } 98 | 99 | @Override 100 | default Object getObject(int columnIndex) throws SQLException { 101 | return getObject(getColumnLabel(columnIndex)); 102 | } 103 | 104 | @Override 105 | default Reader getCharacterStream(int columnIndex) throws SQLException { 106 | return getCharacterStream(getColumnLabel(columnIndex)); 107 | } 108 | 109 | @Override 110 | default BigDecimal getBigDecimal(int columnIndex) throws SQLException { 111 | return getBigDecimal(getColumnLabel(columnIndex)); 112 | } 113 | 114 | @Override 115 | default Blob getBlob(int columnIndex) throws SQLException { 116 | return getBlob(getColumnLabel(columnIndex)); 117 | } 118 | 119 | @Override 120 | default Clob getClob(int columnIndex) throws SQLException { 121 | return getClob(getColumnLabel(columnIndex)); 122 | } 123 | 124 | @Override 125 | default Array getArray(int columnIndex) throws SQLException { 126 | return getArray(getColumnLabel(columnIndex)); 127 | } 128 | 129 | @Override 130 | default Date getDate(int columnIndex, Calendar cal) throws SQLException { 131 | return getDate(getColumnLabel(columnIndex), cal); 132 | } 133 | 134 | @Override 135 | default Time getTime(int columnIndex, Calendar cal) throws SQLException { 136 | return getTime(getColumnLabel(columnIndex), cal); 137 | } 138 | 139 | @Override 140 | default Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { 141 | return getTimestamp(getColumnLabel(columnIndex), cal); 142 | } 143 | 144 | @Override 145 | default URL getURL(int columnIndex) throws SQLException { 146 | return getURL(getColumnLabel(columnIndex)); 147 | } 148 | 149 | @Override 150 | default RowId getRowId(int columnIndex) throws SQLException { 151 | return getRowId(getColumnLabel(columnIndex)); 152 | } 153 | 154 | @Override 155 | default NClob getNClob(int columnIndex) throws SQLException { 156 | return getNClob(getColumnLabel(columnIndex)); 157 | } 158 | 159 | @Override 160 | default SQLXML getSQLXML(int columnIndex) throws SQLException { 161 | return getSQLXML(getColumnLabel(columnIndex)); 162 | } 163 | 164 | @Override 165 | default String getNString(int columnIndex) throws SQLException { 166 | return getNString(getColumnLabel(columnIndex)); 167 | } 168 | 169 | @Override 170 | default Reader getNCharacterStream(int columnIndex) throws SQLException { 171 | return getNCharacterStream(getColumnLabel(columnIndex)); 172 | } 173 | 174 | @Override 175 | default T getObject(int columnIndex, Class type) throws SQLException { 176 | return getObject(getColumnLabel(columnIndex), type); 177 | } 178 | 179 | @Override 180 | default Object getObject(int columnIndex, Map> map) throws SQLException { 181 | return getObject(getColumnLabel(columnIndex), map); 182 | } 183 | 184 | @Override 185 | default Ref getRef(int columnIndex) throws SQLException { 186 | return getRef(getColumnLabel(columnIndex)); 187 | } 188 | 189 | default String getColumnLabel(int columnIndex) throws SQLException { 190 | return getMetaData().getColumnLabel(columnIndex); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/ListRecordSet.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql; 2 | 3 | import com.aerospike.jdbc.model.DataColumn; 4 | 5 | import java.math.BigDecimal; 6 | import java.sql.SQLException; 7 | import java.sql.SQLFeatureNotSupportedException; 8 | import java.sql.Statement; 9 | import java.util.Iterator; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | import java.util.logging.Logger; 14 | import java.util.stream.IntStream; 15 | 16 | import static java.util.stream.Collectors.toMap; 17 | 18 | public class ListRecordSet extends BaseResultSet> { 19 | 20 | private static final Logger logger = Logger.getLogger(ListRecordSet.class.getName()); 21 | 22 | private final Iterator> it; 23 | private final Map nameToIndex; 24 | private List currentRecord = null; 25 | 26 | public ListRecordSet(Statement statement, String catalog, String table, List columns, 27 | Iterable> data) { 28 | super(statement, catalog, table, columns); 29 | this.it = data.iterator(); 30 | nameToIndex = IntStream.range(0, columns.size()).boxed() 31 | .collect(toMap(i -> columns.get(i).getName(), i -> i)); 32 | } 33 | 34 | @Override 35 | protected List getRecord() { 36 | return currentRecord; 37 | } 38 | 39 | protected Object getValue(String columnLabel) { 40 | return getRecord().get(nameToIndex.get(columnLabel)); 41 | } 42 | 43 | @Override 44 | public String getString(String columnLabel) { 45 | Object value = getValue(columnLabel); 46 | return Objects.isNull(value) ? "" : value.toString(); 47 | } 48 | 49 | @Override 50 | public boolean getBoolean(String columnLabel) { 51 | return Boolean.parseBoolean(getValue(columnLabel).toString()); 52 | } 53 | 54 | @Override 55 | public byte getByte(String columnLabel) { 56 | return (byte) getValue(columnLabel); 57 | } 58 | 59 | @Override 60 | public short getShort(String columnLabel) { 61 | return (short) getValue(columnLabel); 62 | } 63 | 64 | @Override 65 | public int getInt(String columnLabel) { 66 | String strVal = null; 67 | try { 68 | strVal = getValue(columnLabel).toString(); 69 | return Integer.parseInt(strVal); 70 | } catch (Exception e) { 71 | logger.warning("getInt Exception for " + columnLabel + ", " + strVal); 72 | return -1; 73 | } 74 | } 75 | 76 | @Override 77 | public long getLong(String columnLabel) { 78 | String strVal = null; 79 | try { 80 | strVal = getValue(columnLabel).toString(); 81 | return Long.parseLong(strVal); 82 | } catch (Exception e) { 83 | logger.warning("getLong Exception for " + columnLabel + ", " + strVal); 84 | return -1; 85 | } 86 | } 87 | 88 | @Override 89 | public float getFloat(String columnLabel) { 90 | return (float) getValue(columnLabel); 91 | } 92 | 93 | @Override 94 | public double getDouble(String columnLabel) { 95 | return (double) getValue(columnLabel); 96 | } 97 | 98 | @Override 99 | public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { 100 | throw new SQLFeatureNotSupportedException("getBigDecimal"); 101 | } 102 | 103 | @Override 104 | public byte[] getBytes(String columnLabel) { 105 | return (byte[]) getValue(columnLabel); 106 | } 107 | 108 | @Override 109 | public boolean isLast() { 110 | return !it.hasNext(); 111 | } 112 | 113 | @Override 114 | protected boolean moveToNext() { 115 | boolean hasNext = it.hasNext(); 116 | currentRecord = hasNext ? it.next() : null; 117 | return hasNext; 118 | } 119 | 120 | @Override 121 | protected void setCurrentRecord(List rec) { 122 | currentRecord = rec; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/SimpleParameterMetaData.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql; 2 | 3 | import com.aerospike.jdbc.model.DataColumn; 4 | import com.aerospike.jdbc.util.SqlLiterals; 5 | 6 | import java.sql.ParameterMetaData; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | public class SimpleParameterMetaData implements ParameterMetaData, SimpleWrapper { 11 | 12 | private final List columns; 13 | 14 | public SimpleParameterMetaData(List columns) { 15 | this.columns = columns; 16 | } 17 | 18 | @Override 19 | public int getParameterCount() { 20 | return columns.size(); 21 | } 22 | 23 | @Override 24 | public int isNullable(int param) { 25 | return ParameterMetaData.parameterNullable; 26 | } 27 | 28 | @Override 29 | public boolean isSigned(int param) { 30 | return false; 31 | } 32 | 33 | @Override 34 | public int getPrecision(int param) { 35 | return 0; 36 | } 37 | 38 | @Override 39 | public int getScale(int param) { 40 | return 0; 41 | } 42 | 43 | @Override 44 | public int getParameterType(int param) { 45 | return columns.get(param - 1).getType(); 46 | } 47 | 48 | @Override 49 | public String getParameterTypeName(int param) { 50 | return SqlLiterals.sqlTypeNames.get(getParameterType(param)); 51 | } 52 | 53 | @Override 54 | public String getParameterClassName(int param) { 55 | return Optional.ofNullable(SqlLiterals.sqlToJavaTypes.get(getParameterType(param))) 56 | .map(Class::getName).orElse(null); 57 | } 58 | 59 | @Override 60 | public int getParameterMode(int param) { 61 | return ParameterMetaData.parameterModeIn; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/SimpleWrapper.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql; 2 | 3 | import java.sql.SQLException; 4 | import java.sql.Wrapper; 5 | 6 | public interface SimpleWrapper extends Wrapper { 7 | 8 | @Override 9 | default T unwrap(Class iface) throws SQLException { 10 | try { 11 | // This works for classes that aren't actually wrapping anything 12 | return iface.cast(this); 13 | } catch (ClassCastException e) { 14 | throw new SQLException("Cannot unwrap " + iface, e); 15 | } 16 | } 17 | 18 | @Override 19 | default boolean isWrapperFor(Class iface) { 20 | // This works for classes that aren't actually wrapping anything 21 | return iface.isInstance(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/type/BasicArray.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql.type; 2 | 3 | import com.aerospike.jdbc.model.DataColumn; 4 | import com.aerospike.jdbc.sql.ListRecordSet; 5 | import com.aerospike.jdbc.util.SqlLiterals; 6 | 7 | import javax.sql.rowset.serial.SerialArray; 8 | import javax.sql.rowset.serial.SerialException; 9 | import java.sql.Array; 10 | import java.sql.ResultSet; 11 | import java.sql.SQLException; 12 | import java.sql.Types; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | 18 | import static java.lang.String.format; 19 | import static java.util.Arrays.asList; 20 | import static java.util.Collections.emptyMap; 21 | import static java.util.Optional.ofNullable; 22 | import static java.util.stream.Collectors.toList; 23 | 24 | public class BasicArray extends SerialArray { 25 | 26 | private final transient List columns; 27 | private String catalog; 28 | 29 | public BasicArray(Array array, Map> map) throws SQLException { 30 | super(array, map); 31 | columns = columns(array.getBaseType()); 32 | } 33 | 34 | public BasicArray(Array array) throws SQLException { 35 | super(array); 36 | columns = columns(array.getBaseType()); 37 | } 38 | 39 | public BasicArray(String catalog, String baseTypeName, Object[] elements) throws SQLException { 40 | super(new Array() { 41 | private final int baseType = ofNullable(SqlLiterals.sqlTypeByName.get(baseTypeName)) 42 | .orElseThrow(() -> new IllegalArgumentException(format("Unsupported array type %s", baseTypeName))); 43 | 44 | @Override 45 | public String getBaseTypeName() { 46 | return baseTypeName; 47 | } 48 | 49 | @Override 50 | public int getBaseType() { 51 | return baseType; 52 | } 53 | 54 | @Override 55 | public Object getArray() { 56 | return elements; 57 | } 58 | 59 | @Override 60 | public Object getArray(Map> map) { 61 | return elements; 62 | } 63 | 64 | @Override 65 | public Object getArray(long index, int count) { 66 | return elements; 67 | } 68 | 69 | @Override 70 | public Object getArray(long index, int count, Map> map) { 71 | return elements; 72 | } 73 | 74 | @Override 75 | public ResultSet getResultSet() { 76 | throw new IllegalStateException(); 77 | } 78 | 79 | @Override 80 | public ResultSet getResultSet(Map> map) { 81 | throw new IllegalStateException(); 82 | } 83 | 84 | @Override 85 | public ResultSet getResultSet(long index, int count) { 86 | throw new IllegalStateException(); 87 | } 88 | 89 | @Override 90 | public ResultSet getResultSet(long index, int count, Map> map) { 91 | throw new IllegalStateException(); 92 | } 93 | 94 | @Override 95 | public void free() { 96 | throw new IllegalStateException(); 97 | } 98 | }); 99 | this.catalog = catalog; 100 | columns = columns(getBaseType()); 101 | } 102 | 103 | private List columns(int baseType) { 104 | return asList(new DataColumn(catalog, null, "INDEX", "INDEX").withType(Types.INTEGER), 105 | new DataColumn(catalog, null, "VALUE", "VALUE").withType(baseType)); 106 | } 107 | 108 | @Override 109 | public ResultSet getResultSet(long index, int count) throws SerialException { 110 | return getResultSet(index, count, emptyMap()); 111 | } 112 | 113 | @Override 114 | public ResultSet getResultSet(Map> map) throws SerialException { 115 | return getResultSet(0, Integer.MAX_VALUE, map); 116 | } 117 | 118 | @Override 119 | public ResultSet getResultSet() throws SerialException { 120 | return getResultSet(0, Integer.MAX_VALUE, emptyMap()); 121 | } 122 | 123 | @Override 124 | public ResultSet getResultSet(long index, int count, Map> map) throws SerialException { 125 | AtomicInteger counter = new AtomicInteger((int) index); 126 | Iterable> data = Arrays.stream(((Object[]) getArray())) 127 | .skip(index) 128 | .map(e -> asList(counter.incrementAndGet(), e)) 129 | .collect(toList()); 130 | return new ListRecordSet(null, catalog, null, columns, data); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/type/ByteArrayBlob.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql.type; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.sql.Blob; 9 | import java.sql.SQLException; 10 | import java.util.Arrays; 11 | 12 | import static java.lang.String.format; 13 | 14 | public class ByteArrayBlob implements Blob { 15 | 16 | private static final byte[] EMPTY = new byte[0]; 17 | private volatile byte[] data; 18 | 19 | public ByteArrayBlob() { 20 | this(EMPTY); 21 | } 22 | 23 | public ByteArrayBlob(byte[] data) { 24 | this.data = data; 25 | } 26 | 27 | // https://stackoverflow.com/questions/21341027/find-indexof-a-byte-array-within-another-byte-array 28 | @SuppressWarnings("all") 29 | private static int indexOf(byte[] source, int sourceOffset, int sourceCount, byte[] target, 30 | int targetOffset, int targetCount, int fromIndex) { 31 | if (fromIndex >= sourceCount) { 32 | return (targetCount == 0 ? sourceCount : -1); 33 | } 34 | if (fromIndex < 0) { 35 | fromIndex = 0; 36 | } 37 | if (targetCount == 0) { 38 | return fromIndex; 39 | } 40 | 41 | byte first = target[targetOffset]; 42 | int max = sourceOffset + (sourceCount - targetCount); 43 | 44 | for (int i = sourceOffset + fromIndex; i <= max; i++) { 45 | /* Look for first character. */ 46 | if (source[i] != first) { 47 | while (++i <= max && source[i] != first) ; 48 | } 49 | 50 | /* Found first character, now look at the rest of v2 */ 51 | if (i <= max) { 52 | int j = i + 1; 53 | int end = j + targetCount - 1; 54 | for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++) ; 55 | 56 | if (j == end) { 57 | /* Found whole string. */ 58 | return i - sourceOffset; 59 | } 60 | } 61 | } 62 | return -1; 63 | } 64 | 65 | @Override 66 | public long length() { 67 | return data.length; 68 | } 69 | 70 | @Override 71 | public byte[] getBytes(long pos, int length) throws SQLException { 72 | validatePosition(pos); 73 | if (length < 0) { 74 | throw new SQLException(format("Length must be >= 0 but was %d", length)); 75 | } 76 | int from = (int) pos - 1; 77 | return Arrays.copyOfRange(data, from, from + length); 78 | } 79 | 80 | @Override 81 | public InputStream getBinaryStream() { 82 | return new ByteArrayInputStream(data); 83 | } 84 | 85 | @Override 86 | public long position(byte[] pattern, long start) { 87 | if (start > length()) { 88 | return -1; 89 | } 90 | int index = indexOf(data, (int) start - 1, data.length, pattern, 0, pattern.length, 0); 91 | return index >= 0 ? index + 1 : index; 92 | } 93 | 94 | @Override 95 | public long position(Blob pattern, long start) { 96 | return position(((ByteArrayBlob) pattern).data, start); 97 | } 98 | 99 | @Override 100 | public int setBytes(long pos, byte[] bytes) throws SQLException { 101 | validatePosition(pos); 102 | return setBytes(pos, bytes, 0, bytes.length); 103 | } 104 | 105 | @Override 106 | public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { 107 | validatePosition(pos); 108 | if (offset < 0) { 109 | throw new SQLException(format("Offset cannot be negative but was %d", offset)); 110 | } 111 | int blobOffset = (int) pos - 1; 112 | int n = blobOffset + bytes.length - offset; 113 | byte[] newData = new byte[n]; 114 | System.arraycopy(data, 0, newData, 0, blobOffset); 115 | System.arraycopy(bytes, offset, newData, blobOffset, bytes.length - offset); 116 | data = newData; 117 | return n; 118 | } 119 | 120 | @Override 121 | public OutputStream setBinaryStream(long pos) throws SQLException { 122 | validatePosition(pos); 123 | return new ByteArrayOutputStream() { 124 | @Override 125 | public void close() throws IOException { 126 | super.close(); 127 | try { 128 | setBytes(pos, toByteArray()); 129 | } catch (SQLException e) { 130 | throw new IOException(e); 131 | } 132 | } 133 | }; 134 | } 135 | 136 | @Override 137 | public void truncate(long len) throws SQLException { 138 | validatePosition(len); 139 | @SuppressWarnings("UnnecessaryLocalVariable") // otherwise the assignment is not atomic. 140 | byte[] newData = Arrays.copyOf(data, (int) len); 141 | data = newData; 142 | } 143 | 144 | @Override 145 | public void free() { 146 | data = EMPTY; 147 | } 148 | 149 | @Override 150 | public InputStream getBinaryStream(long pos, long length) throws SQLException { 151 | return new ByteArrayInputStream(getBytes(pos, (int) length)); 152 | } 153 | 154 | @Override 155 | public boolean equals(Object o) { 156 | if (this == o) return true; 157 | if (o == null || getClass() != o.getClass()) return false; 158 | return Arrays.equals(data, ((ByteArrayBlob) o).data); 159 | } 160 | 161 | @Override 162 | public int hashCode() { 163 | return Arrays.hashCode(data); 164 | } 165 | 166 | private void validatePosition(long pos) throws SQLException { 167 | if (pos > Integer.MAX_VALUE || pos < 1) { 168 | throw new SQLException(format("Position must be between 1 and %d but was %d", Integer.MAX_VALUE, pos)); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/sql/type/StringClob.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.sql.type; 2 | 3 | import java.io.*; 4 | import java.sql.Clob; 5 | import java.sql.NClob; 6 | import java.sql.SQLException; 7 | import java.util.Objects; 8 | 9 | import static java.lang.String.format; 10 | 11 | public class StringClob implements NClob { 12 | 13 | private String data; 14 | 15 | public StringClob() { 16 | this(""); 17 | } 18 | 19 | public StringClob(String data) { 20 | this.data = data; 21 | } 22 | 23 | @Override 24 | public long length() { 25 | return data.length(); 26 | } 27 | 28 | @Override 29 | public String getSubString(long pos, int length) throws SQLException { 30 | validatePosition(pos); 31 | int from = (int) pos - 1; 32 | return data.substring(from, from + length); 33 | } 34 | 35 | @Override 36 | public Reader getCharacterStream() { 37 | return new StringReader(data); 38 | } 39 | 40 | @Override 41 | public InputStream getAsciiStream() { 42 | return new ByteArrayInputStream(data.getBytes()); 43 | } 44 | 45 | @Override 46 | public long position(String searchStr, long start) throws SQLException { 47 | validatePosition(start); 48 | int from = (int) start - 1; 49 | int foundIndex = data.indexOf(searchStr, from); 50 | return foundIndex < 0 ? foundIndex : foundIndex + 1; 51 | } 52 | 53 | @Override 54 | public long position(Clob searchStr, long start) throws SQLException { 55 | return position(((StringClob) searchStr).data, start); 56 | } 57 | 58 | @Override 59 | public int setString(long pos, String str) throws SQLException { 60 | return setString(pos, str, 0, str.length()); 61 | } 62 | 63 | @Override 64 | public int setString(long pos, String str, int offset, int len) throws SQLException { 65 | validatePosition(pos); 66 | if (offset < 0) { 67 | throw new SQLException(format("Offset cannot be negative but was %d", offset)); 68 | } 69 | int till = (int) pos; 70 | data = (data.length() >= till ? data.substring(0, till) : data) + str.substring(offset, offset + len); 71 | return len - offset; 72 | } 73 | 74 | @Override 75 | public OutputStream setAsciiStream(long pos) throws SQLException { 76 | validatePosition(pos); 77 | return new ByteArrayOutputStream() { 78 | @Override 79 | public void close() throws IOException { 80 | super.close(); 81 | try { 82 | setString(pos, new String(toByteArray())); 83 | } catch (SQLException e) { 84 | throw new IOException(e); 85 | } 86 | } 87 | }; 88 | } 89 | 90 | @Override 91 | public Writer setCharacterStream(long pos) { 92 | return new StringWriter() { 93 | @Override 94 | public void close() throws IOException { 95 | super.close(); 96 | try { 97 | setString(pos, getBuffer().toString()); 98 | } catch (SQLException e) { 99 | throw new IOException(e); 100 | } 101 | } 102 | }; 103 | } 104 | 105 | @Override 106 | public void truncate(long len) { 107 | data = data.substring(0, (int) len); 108 | } 109 | 110 | @Override 111 | public void free() { 112 | data = ""; 113 | } 114 | 115 | @Override 116 | public Reader getCharacterStream(long pos, long length) throws SQLException { 117 | return new StringReader(getSubString(pos, (int) length)); 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (o == null || getClass() != o.getClass()) return false; 124 | return Objects.equals(data, ((StringClob) o).data); 125 | } 126 | 127 | @Override 128 | public int hashCode() { 129 | return Objects.hash(data); 130 | } 131 | 132 | private void validatePosition(long pos) throws SQLException { 133 | if (pos > Integer.MAX_VALUE || pos < 1) { 134 | throw new SQLException(format("Position must be between 1 and %d but was %d", Integer.MAX_VALUE, pos)); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/tls/AerospikeTLSPolicyBuilder.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.tls; 2 | 3 | import com.aerospike.client.AerospikeException; 4 | import com.aerospike.client.policy.TlsPolicy; 5 | import org.apache.http.ssl.SSLContextBuilder; 6 | import org.apache.http.ssl.SSLContexts; 7 | 8 | import javax.net.ssl.SSLContext; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.security.KeyManagementException; 12 | import java.security.KeyStoreException; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.UnrecoverableKeyException; 15 | import java.security.cert.CertificateException; 16 | import java.util.logging.Logger; 17 | 18 | import static java.util.Objects.requireNonNull; 19 | 20 | public class AerospikeTLSPolicyBuilder { 21 | 22 | private static final Logger log = Logger.getLogger(AerospikeTLSPolicyBuilder.class.getName()); 23 | 24 | private final AerospikeTLSPolicyConfig tlsPolicyConfig; 25 | 26 | public AerospikeTLSPolicyBuilder(AerospikeTLSPolicyConfig tlsPolicyConfig) { 27 | this.tlsPolicyConfig = tlsPolicyConfig; 28 | } 29 | 30 | public TlsPolicy build() { 31 | if (!tlsPolicyConfig.getEnabled()) { 32 | return null; 33 | } 34 | 35 | log.info("Init TlsPolicy"); 36 | TlsPolicy policy = new TlsPolicy(); 37 | if (tlsPolicyConfig.getKeystorePath() != null || tlsPolicyConfig.getTruststorePath() != null) { 38 | addSSLContext(policy); 39 | } 40 | if (tlsPolicyConfig.getAllowedCiphers() != null) { 41 | policy.ciphers = tlsPolicyConfig.getAllowedCiphers(); 42 | } 43 | if (tlsPolicyConfig.getAllowedProtocols() != null) { 44 | policy.protocols = tlsPolicyConfig.getAllowedProtocols(); 45 | } 46 | if (tlsPolicyConfig.getForLoginOnly() != null) { 47 | policy.forLoginOnly = tlsPolicyConfig.getForLoginOnly(); 48 | } 49 | return policy; 50 | } 51 | 52 | private void addSSLContext(TlsPolicy tlsPolicy) { 53 | tlsPolicy.context = getSSLContext(); 54 | } 55 | 56 | private SSLContext getSSLContext() { 57 | SSLContextBuilder ctxBuilder = SSLContexts.custom(); 58 | ctxBuilder.setKeyStoreType(tlsPolicyConfig.getStoreType()); 59 | if (tlsPolicyConfig.getKeystorePath() != null) { 60 | loadKeyStore(ctxBuilder); 61 | } 62 | if (tlsPolicyConfig.getTruststorePath() != null) { 63 | loadTrustStore(ctxBuilder); 64 | } 65 | 66 | try { 67 | return ctxBuilder.build(); 68 | } catch (KeyManagementException | NoSuchAlgorithmException e) { 69 | throw new AerospikeException("Failed to build SSLContext", e); 70 | } 71 | } 72 | 73 | private void loadTrustStore(SSLContextBuilder ctxBuilder) { 74 | File tsFile = new File(tlsPolicyConfig.getTruststorePath()); 75 | try { 76 | if (tlsPolicyConfig.getTruststorePassword() != null) { 77 | ctxBuilder.loadTrustMaterial(tsFile, tlsPolicyConfig.getTruststorePassword().toCharArray()); 78 | } else { 79 | ctxBuilder.loadTrustMaterial(tsFile); 80 | } 81 | } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) { 82 | throw new AerospikeException("Failed To load truststore", e); 83 | } 84 | } 85 | 86 | private void loadKeyStore(SSLContextBuilder ctxBuilder) { 87 | requireNonNull(tlsPolicyConfig.getKeystorePassword(), 88 | "If Keystore Path is provided, Keystore Password must be provided"); 89 | 90 | File ksFile = new File(tlsPolicyConfig.getKeystorePath()); 91 | try { 92 | if (tlsPolicyConfig.getKeyPassword() == null) { 93 | // If keyPass is not provided, assume it is the same as the keystore password 94 | ctxBuilder.loadKeyMaterial(ksFile, tlsPolicyConfig.getKeystorePassword().toCharArray(), 95 | tlsPolicyConfig.getKeystorePassword().toCharArray()); 96 | } else { 97 | ctxBuilder.loadKeyMaterial(ksFile, tlsPolicyConfig.getKeystorePassword().toCharArray(), 98 | tlsPolicyConfig.getKeyPassword().toCharArray()); 99 | } 100 | } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | UnrecoverableKeyException | IOException e) { 101 | throw new AerospikeException("Failed To load keystore", e); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/tls/AerospikeTLSPolicyConfig.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.tls; 2 | 3 | import java.util.Objects; 4 | import java.util.Properties; 5 | 6 | public class AerospikeTLSPolicyConfig { 7 | 8 | private static final String TLS_ENABLED = "tlsEnabled"; 9 | private static final String TLS_STORETYPE = "tlsStoreType"; 10 | private static final String TLS_KEYSTOREPATH = "tlsKeystorePath"; 11 | private static final String TLS_KEYSTOREPASSWORD = "tlsKeystorePassword"; 12 | private static final String TLS_KEYPASSWORD = "tlsKeyPassword"; 13 | private static final String TLS_TRUSTSTOREPATH = "tlsTruststorePath"; 14 | private static final String TLS_TRUSTSTOREPASSWORD = "tlsTruststorePassword"; 15 | private static final String TLS_FORLOGINONLY = "tlsForLoginOnly"; 16 | private static final String TLS_ALLOWEDCIPHERS = "tlsAllowedCiphers"; 17 | private static final String TLS_ALLOWEDPROTOCOLS = "tlsAllowedProtocols"; 18 | 19 | private boolean enabled; 20 | private String storeType = "jks"; 21 | private String keystorePath; 22 | private String keystorePassword; 23 | private String keyPassword; 24 | private String truststorePath; 25 | private String truststorePassword; 26 | private Boolean forLoginOnly; 27 | private String[] allowedCiphers; 28 | private String[] allowedProtocols; 29 | 30 | public static AerospikeTLSPolicyConfig fromProperties(Properties props) { 31 | AerospikeTLSPolicyConfig config = new AerospikeTLSPolicyConfig(); 32 | props.forEach((key, value) -> { 33 | String propKey = (String) key; 34 | switch (propKey) { 35 | case TLS_ENABLED: 36 | config.setEnabled(Boolean.parseBoolean(value.toString())); 37 | break; 38 | case TLS_STORETYPE: 39 | config.setStoreType(value.toString()); 40 | break; 41 | case TLS_KEYSTOREPATH: 42 | config.setKeystorePath(value.toString()); 43 | break; 44 | case TLS_KEYSTOREPASSWORD: 45 | config.setKeystorePassword(value.toString()); 46 | break; 47 | case TLS_KEYPASSWORD: 48 | config.setKeyPassword(value.toString()); 49 | break; 50 | case TLS_TRUSTSTOREPATH: 51 | config.setTruststorePath(value.toString()); 52 | break; 53 | case TLS_TRUSTSTOREPASSWORD: 54 | config.setTruststorePassword(value.toString()); 55 | break; 56 | case TLS_FORLOGINONLY: 57 | config.setForLoginOnly(Boolean.parseBoolean(value.toString())); 58 | break; 59 | case TLS_ALLOWEDCIPHERS: 60 | config.setAllowedCiphers(value.toString()); 61 | break; 62 | case TLS_ALLOWEDPROTOCOLS: 63 | config.setAllowedProtocols(value.toString()); 64 | break; 65 | default: 66 | break; 67 | } 68 | }); 69 | return config; 70 | } 71 | 72 | public boolean getEnabled() { 73 | return enabled; 74 | } 75 | 76 | public void setEnabled(boolean enabled) { 77 | this.enabled = enabled; 78 | } 79 | 80 | public String getStoreType() { 81 | return storeType; 82 | } 83 | 84 | public void setStoreType(String storeType) { 85 | this.storeType = storeType; 86 | } 87 | 88 | public String getKeystorePath() { 89 | return keystorePath; 90 | } 91 | 92 | public void setKeystorePath(String keystorePath) { 93 | this.keystorePath = keystorePath; 94 | } 95 | 96 | public String getKeystorePassword() { 97 | return keystorePassword; 98 | } 99 | 100 | public void setKeystorePassword(String keystorePassword) { 101 | this.keystorePassword = keystorePassword; 102 | } 103 | 104 | public String getKeyPassword() { 105 | return keyPassword; 106 | } 107 | 108 | public void setKeyPassword(String keyPassword) { 109 | this.keyPassword = keyPassword; 110 | } 111 | 112 | public String getTruststorePath() { 113 | return truststorePath; 114 | } 115 | 116 | public void setTruststorePath(String truststorePath) { 117 | this.truststorePath = truststorePath; 118 | } 119 | 120 | public String getTruststorePassword() { 121 | return truststorePassword; 122 | } 123 | 124 | public void setTruststorePassword(String truststorePassword) { 125 | this.truststorePassword = truststorePassword; 126 | } 127 | 128 | public Boolean getForLoginOnly() { 129 | return forLoginOnly; 130 | } 131 | 132 | public void setForLoginOnly(Boolean forLoginOnly) { 133 | this.forLoginOnly = forLoginOnly; 134 | } 135 | 136 | public String[] getAllowedCiphers() { 137 | return allowedCiphers; 138 | } 139 | 140 | public void setAllowedCiphers(String allowedCiphers) { 141 | if (Objects.nonNull(allowedCiphers)) { 142 | this.allowedCiphers = allowedCiphers.trim().split(","); 143 | } 144 | } 145 | 146 | public String[] getAllowedProtocols() { 147 | return allowedProtocols; 148 | } 149 | 150 | public void setAllowedProtocols(String allowedProtocols) { 151 | if (Objects.nonNull(allowedProtocols)) { 152 | this.allowedProtocols = allowedProtocols.trim().split(","); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/AerospikeClientLogger.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import com.aerospike.client.Log; 4 | 5 | import java.util.logging.Logger; 6 | 7 | import static java.lang.String.format; 8 | 9 | public class AerospikeClientLogger implements Log.Callback { 10 | 11 | private static final Logger logger = Logger.getLogger(AerospikeClientLogger.class.getName()); 12 | 13 | @Override 14 | public void log(Log.Level level, String message) { 15 | switch (level) { 16 | case DEBUG: 17 | logger.fine(message); 18 | break; 19 | 20 | case INFO: 21 | logger.info(message); 22 | break; 23 | 24 | case WARN: 25 | logger.warning(message); 26 | break; 27 | 28 | case ERROR: 29 | logger.severe(message); 30 | break; 31 | 32 | default: 33 | logger.warning(() -> format("Unexpected Aerospike client log level %s. Message: %s", 34 | level, message)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/AerospikeVersion.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.Info; 5 | 6 | import java.util.logging.Logger; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import static java.lang.String.format; 11 | 12 | public class AerospikeVersion { 13 | 14 | private static final Logger logger = Logger.getLogger(AerospikeVersion.class.getName()); 15 | 16 | private static final String S_INDEX_SUPPORT_VERSION = "6.0.0.0"; 17 | private static final String BATCH_OPS_SUPPORT_VERSION = "6.0.0.0"; 18 | private static final String S_INDEX_CARDINALITY_SUPPORT_VERSION = "6.1.0.0"; 19 | private static final Pattern versionPattern = Pattern.compile("^(\\d.){1,3}\\d(?=.*|$)"); 20 | 21 | private final IAerospikeClient client; 22 | private volatile Boolean sIndexSupported; 23 | private volatile Boolean batchOpsSupported; 24 | private volatile Boolean sIndexCardinalitySupported; 25 | 26 | public AerospikeVersion(IAerospikeClient client) { 27 | this.client = client; 28 | } 29 | 30 | public boolean isSIndexSupported() { 31 | if (sIndexSupported == null) { 32 | synchronized (this) { 33 | if (sIndexSupported == null) { 34 | String serverVersion = clearQualifier(getAerospikeServerVersion()); 35 | sIndexSupported = compareVersions(serverVersion, S_INDEX_SUPPORT_VERSION) >= 0; 36 | logger.info(() -> format("Secondary index supported: %b, for version: %s", 37 | sIndexSupported, serverVersion)); 38 | } 39 | } 40 | } 41 | return sIndexSupported; 42 | } 43 | 44 | public boolean isBatchOpsSupported() { 45 | if (batchOpsSupported == null) { 46 | synchronized (this) { 47 | if (batchOpsSupported == null) { 48 | String serverVersion = clearQualifier(getAerospikeServerVersion()); 49 | batchOpsSupported = compareVersions(serverVersion, BATCH_OPS_SUPPORT_VERSION) >= 0; 50 | logger.info(() -> format("Batch operations supported: %b, for version: %s", 51 | batchOpsSupported, serverVersion)); 52 | } 53 | } 54 | } 55 | return batchOpsSupported; 56 | } 57 | 58 | public boolean isSIndexCardinalitySupported() { 59 | if (sIndexCardinalitySupported == null) { 60 | synchronized (this) { 61 | if (sIndexCardinalitySupported == null) { 62 | String serverVersion = clearQualifier(getAerospikeServerVersion()); 63 | sIndexCardinalitySupported = compareVersions(serverVersion, S_INDEX_CARDINALITY_SUPPORT_VERSION) >= 0; 64 | logger.info(() -> format("Secondary index cardinality supported: %b, for version: %s", 65 | sIndexCardinalitySupported, serverVersion)); 66 | } 67 | } 68 | } 69 | return sIndexCardinalitySupported; 70 | } 71 | 72 | public String getAerospikeServerVersion() { 73 | String versionString = Info.request(client.getInfoPolicyDefault(), 74 | client.getCluster().getRandomNode(), "version"); 75 | return versionString.substring(versionString.lastIndexOf(' ') + 1); 76 | } 77 | 78 | private String clearQualifier(String version) { 79 | Matcher m = versionPattern.matcher(version); 80 | if (m.find()) { 81 | return m.group(0); 82 | } 83 | return version; 84 | } 85 | 86 | private int compareVersions(String version1, String version2) { 87 | int comparisonResult = 0; 88 | 89 | String[] version1Splits = version1.split("\\."); 90 | String[] version2Splits = version2.split("\\."); 91 | int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); 92 | 93 | for (int i = 0; i < maxLengthOfVersionSplits; i++) { 94 | Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; 95 | Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; 96 | int compare = v1.compareTo(v2); 97 | if (compare != 0) { 98 | comparisonResult = compare; 99 | break; 100 | } 101 | } 102 | return comparisonResult; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/AuxStatementParser.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import com.aerospike.jdbc.model.AerospikeQuery; 4 | import com.aerospike.jdbc.model.QueryType; 5 | 6 | import java.sql.SQLDataException; 7 | import java.sql.SQLException; 8 | import java.util.Arrays; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | import static com.aerospike.jdbc.util.Constants.UNSUPPORTED_QUERY_TYPE_MESSAGE; 13 | 14 | public final class AuxStatementParser { 15 | 16 | private static final Pattern truncateTablePattern; 17 | private static final Pattern createIndexPattern; 18 | private static final Pattern dropIndexPattern; 19 | 20 | static { 21 | truncateTablePattern = Pattern.compile( 22 | "truncate\\s+table\\s+\"?([^\\s^;\"]+)\"?[\\s;]*", 23 | Pattern.CASE_INSENSITIVE); 24 | createIndexPattern = Pattern.compile( 25 | "create\\s+index\\s+(\\S+)\\s+on\\s+\"?([^\\s^;\"]+)\"?\\s*\\((.+)\\)[\\s;]*", 26 | Pattern.CASE_INSENSITIVE); 27 | dropIndexPattern = Pattern.compile( 28 | "drop\\s+index\\s+(\\S+)\\s+on\\s*\"?([^\\s^;\"]+)\"?[\\s;]*", 29 | Pattern.CASE_INSENSITIVE); 30 | } 31 | 32 | private AuxStatementParser() { 33 | } 34 | 35 | /** 36 | * An auxiliary method to parse queries which are not supported by the main parser. 37 | * 38 | * @param sql the original SQL query string. 39 | * @return an {@link com.aerospike.jdbc.model.AerospikeQuery} 40 | * @throws SQLException if no match. 41 | */ 42 | public static AerospikeQuery parse(String sql) throws SQLException { 43 | Matcher m = truncateTablePattern.matcher(sql); 44 | if (m.find()) { 45 | AerospikeQuery query = new AerospikeQuery(); 46 | query.setQueryType(QueryType.DROP_TABLE); 47 | query.setTable(m.group(1)); 48 | return query; 49 | } 50 | 51 | m = createIndexPattern.matcher(sql); 52 | if (m.find()) { 53 | AerospikeQuery query = new AerospikeQuery(); 54 | query.setQueryType(QueryType.CREATE_INDEX); 55 | query.setIndex(m.group(1)); 56 | query.setTable(m.group(2)); 57 | query.setColumns(Arrays.asList(m.group(3).trim().split(","))); 58 | return query; 59 | } 60 | 61 | m = dropIndexPattern.matcher(sql); 62 | if (m.find()) { 63 | AerospikeQuery query = new AerospikeQuery(); 64 | query.setQueryType(QueryType.DROP_INDEX); 65 | query.setIndex(m.group(1)); 66 | query.setTable(m.group(2)); 67 | return query; 68 | } 69 | 70 | throw new SQLDataException(UNSUPPORTED_QUERY_TYPE_MESSAGE); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/Constants.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | public final class Constants { 4 | 5 | public static final String PRIMARY_KEY_COLUMN_NAME = "__key"; 6 | public static final String DEFAULT_SCHEMA_NAME = "__default"; 7 | 8 | public static final String METADATA_DIGEST_COLUMN_NAME = "__digest"; 9 | public static final String METADATA_TTL_COLUMN_NAME = "__ttl"; 10 | public static final String METADATA_GEN_COLUMN_NAME = "__gen"; 11 | 12 | public static final String UNSUPPORTED_QUERY_TYPE_MESSAGE = "Unsupported query type"; 13 | 14 | // Driver version 15 | public static final String DRIVER_VERSION = "1.10.1"; 16 | public static final int DRIVER_MAJOR_VERSION = 1; 17 | public static final int DRIVER_MINOR_VERSION = 10; 18 | 19 | // JDBC specification 20 | public static final String JDBC_VERSION = "4.2"; 21 | public static final int JDBC_MAJOR_VERSION = JDBC_VERSION.charAt(0) - '0'; 22 | public static final int JDBC_MINOR_VERSION = JDBC_VERSION.charAt(2) - '0'; 23 | 24 | private Constants() { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/DatabaseMetadataBuilder.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import com.aerospike.jdbc.AerospikeConnection; 4 | import com.aerospike.jdbc.AerospikeDatabaseMetadata; 5 | import com.aerospike.jdbc.model.DriverPolicy; 6 | import com.google.common.cache.Cache; 7 | import com.google.common.cache.CacheBuilder; 8 | 9 | import java.sql.SQLException; 10 | import java.time.Duration; 11 | import java.util.concurrent.ExecutionException; 12 | 13 | public class DatabaseMetadataBuilder { 14 | 15 | private final Cache metadataCache; 16 | 17 | public DatabaseMetadataBuilder(DriverPolicy driverPolicy) { 18 | metadataCache = CacheBuilder.newBuilder() 19 | .expireAfterWrite(Duration.ofSeconds(driverPolicy.getMetadataCacheTtlSeconds())) 20 | .build(); 21 | } 22 | 23 | public AerospikeDatabaseMetadata build(String url, AerospikeConnection connection) 24 | throws SQLException { 25 | try { 26 | return metadataCache.get(url, () -> new AerospikeDatabaseMetadata(url, connection)); 27 | } catch (ExecutionException e) { 28 | throw new SQLException(e); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/IOUtils.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.Reader; 7 | import java.io.StringWriter; 8 | 9 | public final class IOUtils { 10 | 11 | private IOUtils() { 12 | } 13 | 14 | public static byte[] toByteArray(InputStream is) throws IOException { 15 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 16 | int nRead; 17 | byte[] data = new byte[16 * 1024]; 18 | while ((nRead = is.read(data, 0, data.length)) != -1) { 19 | buffer.write(data, 0, nRead); 20 | } 21 | return buffer.toByteArray(); 22 | } 23 | 24 | public static String toString(Reader reader) throws IOException { 25 | StringWriter writer = new StringWriter(); 26 | int nRead; 27 | char[] data = new char[16 * 1024]; 28 | while ((nRead = reader.read(data, 0, data.length)) != -1) { 29 | writer.write(data, 0, nRead); 30 | } 31 | return writer.toString(); 32 | } 33 | 34 | public static String stripQuotes(String s) { 35 | return s == null ? null : s.replaceAll("^\"?(.*?)\"?$", "$1"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/PreparedStatement.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Collections; 6 | import java.util.Map.Entry; 7 | 8 | public class PreparedStatement { 9 | 10 | public static final char PS_PLACEHOLDER_PREFIX = '_'; 11 | 12 | private PreparedStatement() { 13 | } 14 | 15 | public static Entry parseParameters(String sql, int offset) { 16 | StringBuilder fixedIndexSqlBuf = new StringBuilder(); 17 | int count = 0; 18 | boolean intoConstant = false; 19 | for (char c : sql.toCharArray()) { 20 | if (c == '\'') { 21 | intoConstant = !intoConstant; 22 | } 23 | if (!intoConstant && c == '?') { 24 | count++; 25 | fixedIndexSqlBuf.append(PS_PLACEHOLDER_PREFIX).append(count + offset); 26 | } else { 27 | fixedIndexSqlBuf.append(c); 28 | } 29 | } 30 | return Collections.singletonMap(fixedIndexSqlBuf.toString(), count).entrySet().iterator().next(); 31 | } 32 | 33 | public static Iterable splitQueries(String sql) { 34 | Collection queries = new ArrayList<>(); 35 | StringBuilder currentQuery = new StringBuilder(); 36 | boolean intoConstant = false; 37 | 38 | for (char c : sql.toCharArray()) { 39 | if (c == '\'') { 40 | intoConstant = !intoConstant; 41 | } 42 | if (!intoConstant && c == ';') { 43 | appendNotEmpty(queries, currentQuery.toString()); 44 | currentQuery.setLength(0); 45 | } else { 46 | currentQuery.append(c); 47 | } 48 | } 49 | 50 | if (currentQuery.length() > 0 && !currentQuery.toString().trim().isEmpty()) { 51 | appendNotEmpty(queries, currentQuery.toString()); 52 | } 53 | 54 | return queries; 55 | } 56 | 57 | private static void appendNotEmpty(Collection queries, String query) { 58 | if (!query.trim().isEmpty()) { 59 | queries.add(query); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/aerospike/jdbc/util/SqlLiterals.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import java.sql.Types; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import java.util.TreeMap; 9 | 10 | import static java.sql.Types.NULL; 11 | import static java.sql.Types.OTHER; 12 | import static java.util.stream.Collectors.toMap; 13 | 14 | public class SqlLiterals { 15 | 16 | public static final Map, Integer> sqlTypes = new HashMap<>(); 17 | 18 | static { 19 | sqlTypes.put(Short.class, Types.SMALLINT); 20 | sqlTypes.put(Integer.class, Types.INTEGER); 21 | sqlTypes.put(Long.class, Types.BIGINT); 22 | sqlTypes.put(Boolean.class, Types.BOOLEAN); 23 | sqlTypes.put(Float.class, Types.FLOAT); 24 | sqlTypes.put(Double.class, Types.DOUBLE); 25 | sqlTypes.put(short.class, Types.SMALLINT); 26 | sqlTypes.put(int.class, Types.INTEGER); 27 | sqlTypes.put(long.class, Types.BIGINT); 28 | sqlTypes.put(boolean.class, Types.BOOLEAN); 29 | sqlTypes.put(float.class, Types.FLOAT); 30 | sqlTypes.put(double.class, Types.DOUBLE); 31 | sqlTypes.put(String.class, Types.VARCHAR); 32 | sqlTypes.put(byte[].class, Types.BLOB); 33 | sqlTypes.put(java.sql.Date.class, Types.DATE); 34 | sqlTypes.put(java.sql.Time.class, Types.TIME); 35 | sqlTypes.put(java.sql.Timestamp.class, Types.TIMESTAMP); 36 | sqlTypes.put(ArrayList.class, Types.ARRAY); 37 | sqlTypes.put(null, NULL); 38 | sqlTypes.put(Object.class, OTHER); 39 | } 40 | 41 | public static final Map sqlTypeNames = new HashMap<>(); 42 | 43 | static { 44 | sqlTypeNames.put(Types.SMALLINT, "short"); 45 | sqlTypeNames.put(Types.INTEGER, "integer"); 46 | sqlTypeNames.put(Types.BIGINT, "long"); 47 | sqlTypeNames.put(Types.BOOLEAN, "boolean"); 48 | sqlTypeNames.put(Types.FLOAT, "float"); 49 | sqlTypeNames.put(Types.DOUBLE, "double"); 50 | sqlTypeNames.put(Types.VARCHAR, "varchar"); 51 | sqlTypeNames.put(Types.BLOB, "blob"); 52 | sqlTypeNames.put(Types.DATE, "date"); 53 | sqlTypeNames.put(Types.TIME, "time"); 54 | sqlTypeNames.put(Types.TIMESTAMP, "timestamp"); 55 | sqlTypeNames.put(Types.ARRAY, "list"); 56 | } 57 | 58 | public static final Map sqlTypeByName = sqlTypeNames.entrySet().stream() 59 | .filter(name -> name.getKey() != Types.ARRAY) 60 | .collect(toMap(Entry::getValue, Entry::getKey, (v1, v2) -> v1, TreeMap::new)); 61 | 62 | public static final Map> sqlToJavaTypes = new HashMap<>(); 63 | 64 | static { 65 | sqlToJavaTypes.put(Types.SMALLINT, Short.class); 66 | sqlToJavaTypes.put(Types.INTEGER, Integer.class); 67 | sqlToJavaTypes.put(Types.BIGINT, Long.class); 68 | sqlToJavaTypes.put(Types.BOOLEAN, Boolean.class); 69 | sqlToJavaTypes.put(Types.FLOAT, Float.class); 70 | sqlToJavaTypes.put(Types.DOUBLE, Double.class); 71 | sqlToJavaTypes.put(Types.VARCHAR, String.class); 72 | sqlToJavaTypes.put(Types.LONGVARCHAR, String.class); 73 | sqlToJavaTypes.put(Types.BLOB, byte[].class); 74 | sqlToJavaTypes.put(Types.BINARY, byte[].class); 75 | sqlToJavaTypes.put(Types.VARBINARY, byte[].class); 76 | sqlToJavaTypes.put(Types.LONGVARBINARY, byte[].class); 77 | sqlToJavaTypes.put(Types.DATE, java.sql.Date.class); 78 | sqlToJavaTypes.put(Types.TIME, java.sql.Time.class); 79 | sqlToJavaTypes.put(Types.TIMESTAMP, java.sql.Timestamp.class); 80 | } 81 | 82 | public static int getSqlType(Object value) { 83 | return value == null ? NULL : getSqlType(value.getClass()); 84 | } 85 | 86 | public static int getSqlType(Class clazz) { 87 | return clazz == null ? NULL : sqlTypes.getOrDefault(clazz, OTHER); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/logging.properties: -------------------------------------------------------------------------------- 1 | .level = INFO -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/DatabaseMetadataTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.util.TestRecord; 4 | import com.aerospike.jdbc.util.TestUtil; 5 | import org.testng.annotations.AfterClass; 6 | import org.testng.annotations.BeforeClass; 7 | import org.testng.annotations.Test; 8 | 9 | import java.sql.DatabaseMetaData; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.util.Objects; 14 | 15 | import static com.aerospike.jdbc.util.TestConfig.NAMESPACE; 16 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 17 | import static com.aerospike.jdbc.util.TestUtil.closeQuietly; 18 | import static java.lang.String.format; 19 | import static org.testng.Assert.assertEquals; 20 | import static org.testng.Assert.assertFalse; 21 | import static org.testng.Assert.assertTrue; 22 | 23 | public class DatabaseMetadataTest extends JdbcBaseTest { 24 | 25 | private final TestRecord testRecord; 26 | 27 | DatabaseMetadataTest() { 28 | testRecord = new TestRecord("key1", true, 11100, 1, "bar"); 29 | } 30 | 31 | @BeforeClass 32 | public void setUp() throws SQLException { 33 | Objects.requireNonNull(connection, "connection is null"); 34 | PreparedStatement statement = null; 35 | int count; 36 | String query = testRecord.toInsertQuery(); 37 | try { 38 | statement = connection.prepareStatement(query); 39 | count = statement.executeUpdate(); 40 | } finally { 41 | closeQuietly(statement); 42 | } 43 | assertEquals(count, 1); 44 | } 45 | 46 | @AfterClass 47 | public void tearDown() throws SQLException { 48 | Objects.requireNonNull(connection, "connection is null"); 49 | PreparedStatement statement = null; 50 | String query = format("delete from %s", TABLE_NAME); 51 | try { 52 | statement = connection.prepareStatement(query); 53 | boolean result = statement.execute(); 54 | assertFalse(result); 55 | } finally { 56 | closeQuietly(statement); 57 | } 58 | assertTrue(statement.getUpdateCount() > 0); 59 | } 60 | 61 | @Test 62 | public void testGetTables() throws SQLException { 63 | DatabaseMetaData databaseMetaData = connection.getMetaData(); 64 | ResultSet tables = databaseMetaData.getTables(NAMESPACE, "", TABLE_NAME, null); 65 | 66 | if (tables.next()) { 67 | assertEquals(tables.getString("TABLE_NAME"), TABLE_NAME); 68 | assertFalse(tables.next()); 69 | } 70 | TestUtil.closeQuietly(tables); 71 | } 72 | 73 | @Test 74 | public void testGetSchemas() throws SQLException { 75 | ResultSet schemas = connection.getMetaData().getSchemas(); 76 | 77 | assertTrue(schemas.next()); 78 | String schemaName = schemas.getString(1); 79 | String catalogName = schemas.getString(2); 80 | assertEquals(schemas.getString("TABLE_SCHEM"), schemaName); 81 | assertEquals(schemas.getString("TABLE_CATALOG"), catalogName); 82 | assertFalse(schemas.next()); 83 | TestUtil.closeQuietly(schemas); 84 | } 85 | 86 | @Test 87 | public void testGetCatalogs() throws SQLException { 88 | ResultSet catalogs = connection.getMetaData().getCatalogs(); 89 | 90 | assertTrue(catalogs.next()); 91 | String catalogName = catalogs.getString(1); 92 | assertEquals(catalogs.getString("TABLE_CAT"), catalogName); 93 | assertFalse(catalogs.next()); 94 | TestUtil.closeQuietly(catalogs); 95 | } 96 | 97 | @Test 98 | public void testJDBCVersion() throws SQLException { 99 | DatabaseMetaData metadata = connection.getMetaData(); 100 | 101 | assertEquals(metadata.getJDBCMajorVersion(), 4); 102 | assertEquals(metadata.getJDBCMinorVersion(), 2); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/IndexQueriesTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.util.TestRecord; 4 | import org.testng.annotations.AfterClass; 5 | import org.testng.annotations.BeforeClass; 6 | import org.testng.annotations.Test; 7 | 8 | import java.sql.SQLException; 9 | import java.sql.Statement; 10 | import java.util.Objects; 11 | 12 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 13 | import static com.aerospike.jdbc.util.TestUtil.closeQuietly; 14 | import static com.aerospike.jdbc.util.TestUtil.sleep; 15 | import static java.lang.String.format; 16 | import static org.testng.Assert.assertEquals; 17 | import static org.testng.Assert.assertFalse; 18 | import static org.testng.Assert.assertThrows; 19 | 20 | public class IndexQueriesTest extends JdbcBaseTest { 21 | 22 | private final TestRecord testRecord; 23 | 24 | IndexQueriesTest() { 25 | testRecord = new TestRecord("key1", true, 11100, 1, "bar"); 26 | } 27 | 28 | @BeforeClass 29 | public void setUp() throws SQLException { 30 | Objects.requireNonNull(connection, "connection is null"); 31 | Statement statement = null; 32 | int count; 33 | String query = testRecord.toInsertQuery(); 34 | try { 35 | statement = connection.createStatement(); 36 | count = statement.executeUpdate(query); 37 | } finally { 38 | closeQuietly(statement); 39 | } 40 | assertEquals(count, 1); 41 | } 42 | 43 | @AfterClass 44 | public void tearDown() throws SQLException { 45 | Objects.requireNonNull(connection, "connection is null"); 46 | Statement statement = null; 47 | String query = format("TRUNCATE TABLE %s", TABLE_NAME); 48 | try { 49 | statement = connection.createStatement(); 50 | boolean result = statement.execute(query); 51 | sleep(100L); 52 | assertFalse(result); 53 | } finally { 54 | closeQuietly(statement); 55 | } 56 | assertEquals(statement.getUpdateCount(), 1); 57 | } 58 | 59 | @Test 60 | public void testIndexCreateSuccess() throws SQLException { 61 | Statement statement = null; 62 | int count; 63 | String query = format("CREATE INDEX str1_idx ON %s (str1);", TABLE_NAME); 64 | try { 65 | statement = connection.createStatement(); 66 | count = statement.executeUpdate(query); 67 | } finally { 68 | closeQuietly(statement); 69 | } 70 | assertEquals(count, 1); 71 | } 72 | 73 | @Test 74 | public void testIndexCreateMultiColumn() throws SQLException { 75 | String query = format("CREATE INDEX multi_idx ON %s (str1, int1)", TABLE_NAME); 76 | final Statement statement = connection.createStatement(); 77 | assertThrows(UnsupportedOperationException.class, () -> statement.executeUpdate(query)); 78 | closeQuietly(statement); 79 | } 80 | 81 | @Test 82 | public void testIndexCreateUnsupportedType() throws SQLException { 83 | String query = format("CREATE INDEX bool1_idx ON %s (bool1)", TABLE_NAME); 84 | final Statement statement = connection.createStatement(); 85 | assertThrows(UnsupportedOperationException.class, () -> statement.executeUpdate(query)); 86 | closeQuietly(statement); 87 | } 88 | 89 | @Test 90 | public void testIndexCreateNonExistentColumn() throws SQLException { 91 | String query = format("CREATE INDEX ne_idx ON %s (ne)", TABLE_NAME); 92 | final Statement statement = connection.createStatement(); 93 | assertThrows(IllegalArgumentException.class, () -> statement.executeUpdate(query)); 94 | closeQuietly(statement); 95 | } 96 | 97 | @Test 98 | public void testIndexDropSuccess() throws SQLException { 99 | Statement statement = null; 100 | int count; 101 | String query = format("DROP INDEX str1_idx ON %s;", TABLE_NAME); 102 | try { 103 | statement = connection.createStatement(); 104 | count = statement.executeUpdate(query); 105 | } finally { 106 | closeQuietly(statement); 107 | } 108 | assertEquals(count, 1); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/JdbcBaseTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import org.testng.annotations.AfterClass; 4 | import org.testng.annotations.BeforeClass; 5 | 6 | import java.sql.Connection; 7 | import java.sql.DriverManager; 8 | import java.sql.SQLException; 9 | import java.util.concurrent.Executors; 10 | import java.util.logging.Logger; 11 | 12 | import static com.aerospike.jdbc.util.TestConfig.HOSTNAME; 13 | import static com.aerospike.jdbc.util.TestConfig.NAMESPACE; 14 | import static com.aerospike.jdbc.util.TestConfig.PORT; 15 | 16 | public abstract class JdbcBaseTest { 17 | 18 | private static final Logger logger = Logger.getLogger(JdbcBaseTest.class.getName()); 19 | 20 | protected static Connection connection; 21 | 22 | @BeforeClass 23 | public static void connectionInit() throws Exception { 24 | logger.info("connectionInit"); 25 | Class.forName("com.aerospike.jdbc.AerospikeDriver").newInstance(); 26 | String url = String.format("jdbc:aerospike:%s:%d/%s?sendKey=true", HOSTNAME, PORT, NAMESPACE); 27 | connection = DriverManager.getConnection(url); 28 | connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000); 29 | } 30 | 31 | @AfterClass 32 | public static void connectionClose() throws SQLException { 33 | logger.info("connectionClose"); 34 | connection.close(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/JdbiQueriesTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import org.jdbi.v3.core.Jdbi; 4 | import org.testng.annotations.Ignore; 5 | import org.testng.annotations.Test; 6 | 7 | import java.util.Map; 8 | import java.util.UUID; 9 | 10 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 11 | import static java.lang.String.format; 12 | import static org.testng.Assert.assertEquals; 13 | 14 | public class JdbiQueriesTest extends JdbcBaseTest { 15 | 16 | @Test 17 | @Ignore 18 | public void testJdbiInsertStringLiteral() { 19 | String id = UUID.randomUUID().toString(); 20 | Jdbi jdbi = Jdbi.create(connection); 21 | jdbi.withHandle(handle -> { 22 | String query = format("INSERT INTO test.foo (%s, k1, k2, k3, k4) VALUES ('%s', 'i1', 'i2', 'i3', 'i4')", 23 | PRIMARY_KEY_COLUMN_NAME, id); 24 | int rowsInserted = handle.createUpdate(query) 25 | .execute(); 26 | 27 | assertEquals(rowsInserted, 1); 28 | 29 | query = format("SELECT * FROM test.foo WHERE %s='%s'", PRIMARY_KEY_COLUMN_NAME, id); 30 | Map result = handle.createQuery(query) 31 | .mapToMap() 32 | .first(); 33 | 34 | assertEquals(result.get(PRIMARY_KEY_COLUMN_NAME).toString(), id); 35 | assertEquals(result.get("k1"), "i1"); 36 | assertEquals(result.get("k2"), "i2"); 37 | assertEquals(result.get("k3"), "i3"); 38 | assertEquals(result.get("k4"), "i4"); 39 | 40 | return true; 41 | }); 42 | } 43 | 44 | @Test 45 | @Ignore 46 | public void testJdbiBindVariablesPositions() { 47 | String id = UUID.randomUUID().toString(); 48 | String v1 = "v1"; 49 | String v2 = "v2"; 50 | String v3 = "v3"; 51 | String v4 = "v4"; 52 | Jdbi jdbi = Jdbi.create(connection); 53 | jdbi.withHandle(handle -> { 54 | String query = format("INSERT INTO test.foo (%s, k1, k2, k3, k4) VALUES (?, ?, ?, ?, ?)", 55 | PRIMARY_KEY_COLUMN_NAME); 56 | int rowsInserted = handle.createUpdate(query) 57 | .bind(0, id) 58 | .bind(1, v1) 59 | .bind(2, v2) 60 | .bind(3, v3) 61 | .bind(4, v4) 62 | .execute(); 63 | 64 | assertEquals(rowsInserted, 1); 65 | 66 | query = format("SELECT * FROM test.foo WHERE %s=?", 67 | PRIMARY_KEY_COLUMN_NAME); 68 | Map result = handle.createQuery(query) 69 | .bind(0, id) 70 | .mapToMap() 71 | .first(); 72 | 73 | assertEquals(result.get(PRIMARY_KEY_COLUMN_NAME).toString(), id); 74 | assertEquals(result.get("k1"), v1); 75 | assertEquals(result.get("k2"), v2); 76 | assertEquals(result.get("k3"), v3); 77 | assertEquals(result.get("k4"), v4); 78 | 79 | return true; 80 | }); 81 | } 82 | 83 | @Test 84 | @Ignore 85 | public void testJdbiBindVariablesNames() { 86 | String id = UUID.randomUUID().toString(); 87 | String v1 = "v1"; 88 | String v2 = "v2"; 89 | String v3 = "v3"; 90 | String v4 = "v4"; 91 | Jdbi jdbi = Jdbi.create(connection); 92 | jdbi.withHandle(handle -> { 93 | String query = format("INSERT INTO test.foo (%s, k1, k2, k3, k4) VALUES (:id, :v1, :v2, :v3, :v4)", 94 | PRIMARY_KEY_COLUMN_NAME); 95 | int rowsInserted = handle.createUpdate(query) 96 | .bind("id", id) 97 | .bind("v1", v1) 98 | .bind("v2", v2) 99 | .bind("v3", v3) 100 | .bind("v4", v4) 101 | .execute(); 102 | 103 | assertEquals(rowsInserted, 1); 104 | 105 | query = format("SELECT * FROM test.foo WHERE %s=:id", 106 | PRIMARY_KEY_COLUMN_NAME); 107 | Map result = handle.createQuery(query) 108 | .bind("id", id) 109 | .mapToMap() 110 | .first(); 111 | 112 | assertEquals(result.get(PRIMARY_KEY_COLUMN_NAME).toString(), id); 113 | assertEquals(result.get("k1"), v1); 114 | assertEquals(result.get("k2"), v2); 115 | assertEquals(result.get("k3"), v3); 116 | assertEquals(result.get("k4"), v4); 117 | 118 | return true; 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/ParseJdbcUrlTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.client.IAerospikeClient; 4 | import com.aerospike.client.Value; 5 | import com.aerospike.client.policy.AuthMode; 6 | import com.aerospike.jdbc.model.DriverConfiguration; 7 | import org.testng.annotations.Test; 8 | 9 | import java.sql.DriverManager; 10 | import java.util.Properties; 11 | 12 | import static org.testng.Assert.assertEquals; 13 | import static org.testng.Assert.assertFalse; 14 | import static org.testng.Assert.assertNull; 15 | import static org.testng.Assert.assertTrue; 16 | 17 | public class ParseJdbcUrlTest { 18 | 19 | @Test 20 | public void testParseUrlParameters() throws Exception { 21 | Class.forName("com.aerospike.jdbc.AerospikeDriver").newInstance(); 22 | String url = String.format( 23 | "jdbc:aerospike:%s:%d/%s?%s=%d&%s=%d&%s=%b&%s=%s&%s=%d&%s=%b", 24 | "localhost", 3000, "test", 25 | "timeout", 512, 26 | "totalTimeout", 2000, 27 | "useServicesAlternate", true, 28 | "authMode", "external_insecure", 29 | "recordSetTimeoutMs", 5000, 30 | "useBoolBin", false 31 | ); 32 | AerospikeConnection connection = (AerospikeConnection) DriverManager.getConnection(url); 33 | Properties properties = connection.getClientInfo(); 34 | IAerospikeClient client = connection.getClient(); 35 | 36 | assertEquals(properties.getProperty("timeout"), "512"); 37 | assertEquals(properties.getProperty("totalTimeout"), "2000"); 38 | assertEquals(properties.getProperty("useServicesAlternate"), "true"); 39 | assertEquals(properties.getProperty("authMode"), "external_insecure"); 40 | assertEquals(properties.getProperty("recordSetTimeoutMs"), "5000"); 41 | assertEquals(properties.getProperty("useBoolBin"), "false"); 42 | 43 | DriverConfiguration config = connection.getConfiguration(); 44 | assertEquals(config.getClientPolicy().timeout, 512); 45 | assertEquals(client.getInfoPolicyDefault().timeout, 512); 46 | assertEquals(client.getScanPolicyDefault().recordsPerSecond, 0); 47 | assertTotalTimeoutAll(client, 2000); 48 | assertSendKeyAll(client, false); 49 | assertTrue(config.getClientPolicy().useServicesAlternate); 50 | assertEquals(config.getClientPolicy().authMode, AuthMode.EXTERNAL_INSECURE); 51 | assertEquals(config.getDriverPolicy().getRecordSetTimeoutMs(), 5000); 52 | assertFalse(Value.UseBoolBin); 53 | Value.UseBoolBin = true; 54 | 55 | Properties update = new Properties(); 56 | update.setProperty("totalTimeout", "3000"); 57 | update.setProperty("sendKey", "true"); 58 | update.setProperty("recordSetQueueCapacity", "1024"); 59 | update.setProperty("metadataCacheTtlSeconds", "7200"); 60 | update.setProperty("recordsPerSecond", "128"); 61 | update.setProperty("schemaBuilderMaxRecords", "500"); 62 | connection.setClientInfo(update); 63 | assertEquals(client.getScanPolicyDefault().recordsPerSecond, 128); 64 | assertTotalTimeoutAll(client, 3000); 65 | assertSendKeyAll(client, true); 66 | assertEquals(config.getDriverPolicy().getRecordSetQueueCapacity(), 1024); 67 | assertEquals(config.getDriverPolicy().getMetadataCacheTtlSeconds(), 7200); 68 | assertEquals(config.getDriverPolicy().getSchemaBuilderMaxRecords(), 500); 69 | 70 | connection.setClientInfo("recordSetTimeoutMs", "7000"); 71 | assertEquals(config.getDriverPolicy().getRecordSetTimeoutMs(), 7000); 72 | } 73 | 74 | @Test 75 | public void testInapplicableUrl() throws Exception { 76 | AerospikeDriver driver = (AerospikeDriver) Class.forName("com.aerospike.jdbc.AerospikeDriver") 77 | .newInstance(); 78 | String url = "jdbc:postgresql://localhost:5432/sample"; 79 | assertNull(driver.connect(url, new Properties())); 80 | } 81 | 82 | private void assertTotalTimeoutAll(IAerospikeClient client, int timeout) { 83 | assertEquals(client.getReadPolicyDefault().totalTimeout, timeout); 84 | assertEquals(client.getWritePolicyDefault().totalTimeout, timeout); 85 | assertEquals(client.getQueryPolicyDefault().totalTimeout, timeout); 86 | assertEquals(client.getScanPolicyDefault().totalTimeout, timeout); 87 | assertEquals(client.getBatchPolicyDefault().totalTimeout, timeout); 88 | } 89 | 90 | private void assertSendKeyAll(IAerospikeClient client, boolean sendKey) { 91 | assertEquals(client.getReadPolicyDefault().sendKey, sendKey); 92 | assertEquals(client.getWritePolicyDefault().sendKey, sendKey); 93 | assertEquals(client.getQueryPolicyDefault().sendKey, sendKey); 94 | assertEquals(client.getScanPolicyDefault().sendKey, sendKey); 95 | assertEquals(client.getBatchPolicyDefault().sendKey, sendKey); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/QueryCustomParserTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.model.AerospikeQuery; 4 | import com.aerospike.jdbc.model.QueryType; 5 | import com.aerospike.jdbc.util.AuxStatementParser; 6 | import org.testng.annotations.Test; 7 | 8 | import java.sql.SQLException; 9 | 10 | import static java.lang.String.format; 11 | import static org.testng.Assert.assertEquals; 12 | 13 | public class QueryCustomParserTest { 14 | 15 | private static final String tableName = "jdbc"; 16 | 17 | @Test 18 | public void testIndexCreateQuery() throws SQLException { 19 | AerospikeQuery query; 20 | 21 | String lowCaseQuery = format("create index str1_idx on %s (str1);", tableName); 22 | query = AuxStatementParser.parse(lowCaseQuery); 23 | assertIndexCreateQuery(query); 24 | 25 | String quotedTableQuery = format("create index str1_idx on \"%s\" (str1);", tableName); 26 | query = AuxStatementParser.parse(quotedTableQuery); 27 | assertIndexCreateQuery(query); 28 | 29 | String whiteSpacesQuery = format("create index str1_idx on %s( str1 ) ;", tableName); 30 | query = AuxStatementParser.parse(whiteSpacesQuery); 31 | assertIndexCreateQuery(query); 32 | } 33 | 34 | private void assertIndexCreateQuery(AerospikeQuery query) { 35 | assertEquals(query.getQueryType(), QueryType.CREATE_INDEX); 36 | assertEquals(query.getTable(), tableName); 37 | assertEquals(query.getIndex(), "str1_idx"); 38 | assertEquals(query.getColumns().get(0), "str1"); 39 | } 40 | 41 | @Test 42 | public void testIndexDropQuery() throws SQLException { 43 | AerospikeQuery query; 44 | 45 | String lowCaseQuery = format("drop index str1_idx on %s;", tableName); 46 | query = AuxStatementParser.parse(lowCaseQuery); 47 | assertIndexDropQuery(query); 48 | 49 | String quotedTableQuery = format("drop index str1_idx on \"%s\";", tableName); 50 | query = AuxStatementParser.parse(quotedTableQuery); 51 | assertIndexDropQuery(query); 52 | 53 | String whiteSpacesQuery = format("drop index str1_idx on %s ;", tableName); 54 | query = AuxStatementParser.parse(whiteSpacesQuery); 55 | assertIndexDropQuery(query); 56 | } 57 | 58 | private void assertIndexDropQuery(AerospikeQuery query) { 59 | assertEquals(query.getQueryType(), QueryType.DROP_INDEX); 60 | assertEquals(query.getTable(), tableName); 61 | assertEquals(query.getIndex(), "str1_idx"); 62 | } 63 | 64 | @Test 65 | public void testTruncateTableQuery() throws SQLException { 66 | AerospikeQuery query; 67 | 68 | String lowCaseQuery = format("truncate table %s", tableName); 69 | query = AuxStatementParser.parse(lowCaseQuery); 70 | assertTruncateTableQuery(query); 71 | 72 | String quotedTableQuery = format("truncate table \"%s\"", tableName); 73 | query = AuxStatementParser.parse(quotedTableQuery); 74 | assertTruncateTableQuery(query); 75 | 76 | String whiteSpacesQuery = format("truncate table %s ;", tableName); 77 | query = AuxStatementParser.parse(whiteSpacesQuery); 78 | assertTruncateTableQuery(query); 79 | } 80 | 81 | private void assertTruncateTableQuery(AerospikeQuery query) { 82 | assertEquals(query.getQueryType(), QueryType.DROP_TABLE); 83 | assertEquals(query.getTable(), tableName); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/QuerySendKeyFalseTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.util.TestRecord; 4 | import org.testng.annotations.AfterClass; 5 | import org.testng.annotations.AfterMethod; 6 | import org.testng.annotations.BeforeClass; 7 | import org.testng.annotations.BeforeMethod; 8 | import org.testng.annotations.Test; 9 | 10 | import java.sql.Connection; 11 | import java.sql.DriverManager; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.sql.Statement; 15 | import java.util.Objects; 16 | import java.util.concurrent.Executors; 17 | import java.util.logging.Logger; 18 | 19 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 20 | import static com.aerospike.jdbc.util.TestConfig.HOSTNAME; 21 | import static com.aerospike.jdbc.util.TestConfig.NAMESPACE; 22 | import static com.aerospike.jdbc.util.TestConfig.PORT; 23 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 24 | import static com.aerospike.jdbc.util.TestUtil.closeQuietly; 25 | import static java.lang.String.format; 26 | import static org.testng.Assert.assertEquals; 27 | import static org.testng.Assert.assertFalse; 28 | import static org.testng.Assert.assertTrue; 29 | 30 | public class QuerySendKeyFalseTest { 31 | 32 | private static final Logger logger = Logger.getLogger(QuerySendKeyFalseTest.class.getName()); 33 | private static Connection connection; 34 | 35 | private final TestRecord testRecord; 36 | 37 | QuerySendKeyFalseTest() { 38 | testRecord = new TestRecord("key1", true, 11100, 1, "bar"); 39 | } 40 | 41 | @BeforeClass 42 | public static void connectionInit() throws Exception { 43 | logger.info("connectionInit"); 44 | Class.forName("com.aerospike.jdbc.AerospikeDriver").newInstance(); 45 | String url = String.format("jdbc:aerospike:%s:%d/%s?sendKey=false", HOSTNAME, PORT, NAMESPACE); 46 | connection = DriverManager.getConnection(url); 47 | connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000); 48 | } 49 | 50 | @AfterClass 51 | public static void connectionClose() throws SQLException { 52 | logger.info("connectionClose"); 53 | connection.close(); 54 | } 55 | 56 | @BeforeMethod 57 | public void setUp() throws SQLException { 58 | Objects.requireNonNull(connection, "connection is null"); 59 | Statement statement = null; 60 | int count; 61 | String query = testRecord.toInsertQuery(); 62 | try { 63 | statement = connection.createStatement(); 64 | count = statement.executeUpdate(query); 65 | } finally { 66 | closeQuietly(statement); 67 | } 68 | assertEquals(count, 1); 69 | } 70 | 71 | @AfterMethod 72 | public void tearDown() throws SQLException { 73 | Objects.requireNonNull(connection, "connection is null"); 74 | Statement statement = null; 75 | String query = format("DELETE FROM %s", TABLE_NAME); 76 | try { 77 | statement = connection.createStatement(); 78 | boolean result = statement.execute(query); 79 | assertFalse(result); 80 | } finally { 81 | closeQuietly(statement); 82 | } 83 | assertTrue(statement.getUpdateCount() > 0); 84 | } 85 | 86 | @Test 87 | public void testSelectByPrimaryKeyQuery() throws SQLException { 88 | Statement statement = null; 89 | ResultSet resultSet = null; 90 | String query = format("SELECT * FROM %s WHERE %s='%s' AND int1=%d", 91 | TABLE_NAME, PRIMARY_KEY_COLUMN_NAME, testRecord.getPrimaryKey(), testRecord.getInt1()); 92 | int total = 0; 93 | try { 94 | statement = connection.createStatement(); 95 | resultSet = statement.executeQuery(query); 96 | while (resultSet.next()) { 97 | testRecord.assertResultSet(resultSet); 98 | 99 | total++; 100 | } 101 | assertEquals(total, 1); 102 | } finally { 103 | closeQuietly(statement); 104 | closeQuietly(resultSet); 105 | } 106 | } 107 | 108 | @Test 109 | public void testRecordFoundPrimaryKeyNull() throws SQLException { 110 | Statement statement = null; 111 | ResultSet resultSet = null; 112 | String query = format("SELECT * FROM %s WHERE int1=%d", TABLE_NAME, testRecord.getInt1()); 113 | int total = 0; 114 | try { 115 | statement = connection.createStatement(); 116 | resultSet = statement.executeQuery(query); 117 | while (resultSet.next()) { 118 | testRecord.assertResultSet(resultSet, false); 119 | 120 | total++; 121 | } 122 | assertEquals(total, 1); 123 | } finally { 124 | closeQuietly(statement); 125 | closeQuietly(resultSet); 126 | } 127 | } 128 | 129 | @Test 130 | public void testSelectByPrimaryKeyQueryFilteredOut() throws SQLException { 131 | Statement statement = null; 132 | ResultSet resultSet = null; 133 | String query = format("SELECT * FROM %s WHERE %s='%s' AND int1=%d", 134 | TABLE_NAME, PRIMARY_KEY_COLUMN_NAME, testRecord.getPrimaryKey(), 10); 135 | try { 136 | statement = connection.createStatement(); 137 | resultSet = statement.executeQuery(query); 138 | 139 | assertFalse(resultSet.next()); 140 | } finally { 141 | closeQuietly(statement); 142 | closeQuietly(resultSet); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/RecordMetadataTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.util.TestRecord; 4 | import org.testng.annotations.AfterClass; 5 | import org.testng.annotations.AfterMethod; 6 | import org.testng.annotations.BeforeClass; 7 | import org.testng.annotations.BeforeMethod; 8 | import org.testng.annotations.Test; 9 | 10 | import java.sql.Connection; 11 | import java.sql.DriverManager; 12 | import java.sql.ResultSet; 13 | import java.sql.SQLException; 14 | import java.sql.Statement; 15 | import java.util.Objects; 16 | import java.util.concurrent.Executors; 17 | import java.util.logging.Logger; 18 | 19 | import static com.aerospike.jdbc.util.Constants.METADATA_DIGEST_COLUMN_NAME; 20 | import static com.aerospike.jdbc.util.Constants.METADATA_GEN_COLUMN_NAME; 21 | import static com.aerospike.jdbc.util.Constants.METADATA_TTL_COLUMN_NAME; 22 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 23 | import static com.aerospike.jdbc.util.TestConfig.HOSTNAME; 24 | import static com.aerospike.jdbc.util.TestConfig.NAMESPACE; 25 | import static com.aerospike.jdbc.util.TestConfig.PORT; 26 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 27 | import static com.aerospike.jdbc.util.TestUtil.closeQuietly; 28 | import static java.lang.String.format; 29 | import static org.testng.Assert.assertEquals; 30 | import static org.testng.Assert.assertFalse; 31 | import static org.testng.Assert.assertNull; 32 | import static org.testng.Assert.assertTrue; 33 | 34 | public class RecordMetadataTest { 35 | 36 | private static final Logger logger = Logger.getLogger(RecordMetadataTest.class.getName()); 37 | private static Connection connection; 38 | 39 | private final TestRecord testRecord; 40 | 41 | RecordMetadataTest() { 42 | testRecord = new TestRecord("key1", true, 11100, 1, "bar"); 43 | } 44 | 45 | @BeforeClass 46 | public static void connectionInit() throws Exception { 47 | logger.info("connectionInit"); 48 | Class.forName("com.aerospike.jdbc.AerospikeDriver").newInstance(); 49 | String url = String.format("jdbc:aerospike:%s:%d/%s?sendKey=true&showRecordMetadata=true", 50 | HOSTNAME, PORT, NAMESPACE); 51 | connection = DriverManager.getConnection(url); 52 | connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000); 53 | } 54 | 55 | @AfterClass 56 | public static void connectionClose() throws SQLException { 57 | logger.info("connectionClose"); 58 | connection.close(); 59 | } 60 | 61 | @BeforeMethod 62 | public void setUp() throws SQLException { 63 | Objects.requireNonNull(connection, "connection is null"); 64 | Statement statement = null; 65 | int count; 66 | String query = testRecord.toInsertQuery(); 67 | try { 68 | statement = connection.createStatement(); 69 | count = statement.executeUpdate(query); 70 | } finally { 71 | closeQuietly(statement); 72 | } 73 | assertEquals(count, 1); 74 | } 75 | 76 | @AfterMethod 77 | public void tearDown() throws SQLException { 78 | Objects.requireNonNull(connection, "connection is null"); 79 | Statement statement = null; 80 | String query = format("DELETE FROM %s", TABLE_NAME); 81 | try { 82 | statement = connection.createStatement(); 83 | boolean result = statement.execute(query); 84 | assertFalse(result); 85 | } finally { 86 | closeQuietly(statement); 87 | } 88 | assertTrue(statement.getUpdateCount() > 0); 89 | } 90 | 91 | @Test 92 | public void testSelectAllColumns() throws SQLException { 93 | Statement statement = null; 94 | ResultSet resultSet = null; 95 | String query = format("SELECT * FROM %s LIMIT 10", TABLE_NAME); 96 | try { 97 | statement = connection.createStatement(); 98 | resultSet = statement.executeQuery(query); 99 | assertTrue(resultSet.next()); 100 | assertEquals(resultSet.getString(METADATA_DIGEST_COLUMN_NAME), "212ddf97ff3fe0f6dec5e1626d92a635a55171c2"); 101 | assertEquals(resultSet.getInt(METADATA_GEN_COLUMN_NAME), 1); 102 | assertFalse(resultSet.next()); 103 | } finally { 104 | closeQuietly(statement); 105 | closeQuietly(resultSet); 106 | } 107 | } 108 | 109 | @Test 110 | public void testSelectMetadataColumns() throws SQLException { 111 | Statement statement = null; 112 | ResultSet resultSet = null; 113 | String query = format("SELECT %s, %s, int1 FROM %s WHERE %s='key1'", METADATA_GEN_COLUMN_NAME, 114 | METADATA_TTL_COLUMN_NAME, TABLE_NAME, PRIMARY_KEY_COLUMN_NAME); 115 | try { 116 | statement = connection.createStatement(); 117 | resultSet = statement.executeQuery(query); 118 | assertTrue(resultSet.next()); 119 | assertNull(resultSet.getObject(METADATA_DIGEST_COLUMN_NAME)); 120 | assertEquals(resultSet.getInt(METADATA_GEN_COLUMN_NAME), 1); 121 | assertEquals(resultSet.getInt("int1"), 11100); 122 | assertFalse(resultSet.next()); 123 | } finally { 124 | closeQuietly(statement); 125 | closeQuietly(resultSet); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/TransactionTest.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc; 2 | 3 | import com.aerospike.jdbc.util.TestRecord; 4 | import org.testng.annotations.AfterClass; 5 | import org.testng.annotations.AfterMethod; 6 | import org.testng.annotations.BeforeClass; 7 | import org.testng.annotations.BeforeMethod; 8 | import org.testng.annotations.Ignore; 9 | import org.testng.annotations.Test; 10 | 11 | import java.sql.Connection; 12 | import java.sql.DriverManager; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | import java.sql.Statement; 16 | import java.util.Objects; 17 | import java.util.concurrent.Executors; 18 | import java.util.logging.Logger; 19 | 20 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 21 | import static com.aerospike.jdbc.util.TestConfig.HOSTNAME; 22 | import static com.aerospike.jdbc.util.TestConfig.NAMESPACE; 23 | import static com.aerospike.jdbc.util.TestConfig.PORT; 24 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 25 | import static com.aerospike.jdbc.util.TestUtil.closeQuietly; 26 | import static java.lang.String.format; 27 | import static org.testng.Assert.assertEquals; 28 | import static org.testng.Assert.assertFalse; 29 | import static org.testng.Assert.assertTrue; 30 | 31 | @Ignore 32 | public class TransactionTest { 33 | 34 | private static final Logger logger = Logger.getLogger(TransactionTest.class.getName()); 35 | private static Connection connection; 36 | 37 | private final TestRecord testRecord; 38 | private final TestRecord testRecord2; 39 | 40 | TransactionTest() { 41 | testRecord = new TestRecord("key1", true, 11100, 1, "bar"); 42 | testRecord2 = new TestRecord("key2", false, 11101, 2, "foo"); 43 | } 44 | 45 | @BeforeClass 46 | public static void connectionInit() throws Exception { 47 | logger.info("connectionInit"); 48 | Class.forName("com.aerospike.jdbc.AerospikeDriver").newInstance(); 49 | String url = String.format("jdbc:aerospike:%s:%d/%s?sendKey=true&user=%s&password=%s&durableDelete=true", 50 | HOSTNAME, PORT, NAMESPACE, "", ""); 51 | connection = DriverManager.getConnection(url); 52 | connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000); 53 | } 54 | 55 | @AfterClass 56 | public static void connectionClose() throws SQLException { 57 | logger.info("connectionClose"); 58 | connection.close(); 59 | } 60 | 61 | @BeforeMethod 62 | public void setUp() throws SQLException { 63 | Objects.requireNonNull(connection, "connection is null"); 64 | Statement statement = null; 65 | int count; 66 | String query = testRecord.toInsertQuery(); 67 | try { 68 | statement = connection.createStatement(); 69 | count = statement.executeUpdate(query); 70 | } finally { 71 | closeQuietly(statement); 72 | } 73 | assertEquals(count, 1); 74 | } 75 | 76 | @AfterMethod 77 | public void tearDown() throws SQLException { 78 | Objects.requireNonNull(connection, "connection is null"); 79 | Statement statement = null; 80 | String query = format("DELETE FROM %s", TABLE_NAME); 81 | try { 82 | statement = connection.createStatement(); 83 | boolean result = statement.execute(query); 84 | assertFalse(result); 85 | } finally { 86 | closeQuietly(statement); 87 | } 88 | assertTrue(statement.getUpdateCount() > 0); 89 | } 90 | 91 | @Test 92 | public void testTransactionCommit() throws SQLException { 93 | connection.setAutoCommit(false); 94 | Statement statement = null; 95 | String query = testRecord2.toInsertQuery(); 96 | try { 97 | statement = connection.createStatement(); 98 | statement.executeUpdate(query); 99 | } finally { 100 | closeQuietly(statement); 101 | } 102 | try { 103 | statement = connection.createStatement(); 104 | statement.executeUpdate(format("DELETE FROM %s WHERE __key='key1'", TABLE_NAME)); 105 | } finally { 106 | closeQuietly(statement); 107 | } 108 | connection.commit(); 109 | 110 | ResultSet resultSet = null; 111 | query = format("SELECT * FROM %s LIMIT 10", TABLE_NAME); 112 | try { 113 | statement = connection.createStatement(); 114 | resultSet = statement.executeQuery(query); 115 | assertTrue(resultSet.next()); 116 | assertEquals(resultSet.getString(PRIMARY_KEY_COLUMN_NAME), "key2"); 117 | assertFalse(resultSet.next()); 118 | } finally { 119 | closeQuietly(statement); 120 | closeQuietly(resultSet); 121 | } 122 | connection.setAutoCommit(true); 123 | } 124 | 125 | @Test 126 | public void testTransactionRollback() throws SQLException { 127 | connection.setAutoCommit(false); 128 | Statement statement = null; 129 | String query = testRecord2.toInsertQuery(); 130 | try { 131 | statement = connection.createStatement(); 132 | statement.executeUpdate(query); 133 | } finally { 134 | closeQuietly(statement); 135 | } 136 | try { 137 | statement = connection.createStatement(); 138 | statement.executeUpdate(format("DELETE FROM %s WHERE __key='key1'", TABLE_NAME)); 139 | } finally { 140 | closeQuietly(statement); 141 | } 142 | connection.rollback(); 143 | 144 | ResultSet resultSet = null; 145 | query = format("SELECT * FROM %s LIMIT 10", TABLE_NAME); 146 | try { 147 | statement = connection.createStatement(); 148 | resultSet = statement.executeQuery(query); 149 | assertTrue(resultSet.next()); 150 | assertEquals(resultSet.getString(PRIMARY_KEY_COLUMN_NAME), "key1"); 151 | assertFalse(resultSet.next()); 152 | } finally { 153 | closeQuietly(statement); 154 | closeQuietly(resultSet); 155 | } 156 | connection.setAutoCommit(true); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/util/TestConfig.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | public class TestConfig { 4 | 5 | public static final String NAMESPACE = "test"; 6 | public static final String TABLE_NAME = "jdbc"; 7 | 8 | public static final String HOSTNAME = "localhost"; 9 | public static final int PORT = 3000; 10 | 11 | private TestConfig() { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/util/TestRecord.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | import static com.aerospike.jdbc.util.Constants.PRIMARY_KEY_COLUMN_NAME; 7 | import static com.aerospike.jdbc.util.TestConfig.TABLE_NAME; 8 | import static java.lang.String.format; 9 | import static org.testng.Assert.assertEquals; 10 | 11 | public class TestRecord { 12 | 13 | private final String primaryKey; 14 | private final boolean bool1; 15 | private final int int1; 16 | private final int int2; 17 | private final String str1; 18 | private final byte[] val1; 19 | private final Object val2; 20 | 21 | public TestRecord( 22 | String primaryKey, 23 | boolean bool1, 24 | int int1, 25 | int int2, 26 | String str1 27 | ) { 28 | this.primaryKey = primaryKey; 29 | this.bool1 = bool1; 30 | this.int1 = int1; 31 | this.int2 = int2; 32 | this.str1 = str1; 33 | this.val1 = null; 34 | this.val2 = null; 35 | } 36 | 37 | public TestRecord( 38 | String primaryKey, 39 | boolean bool1, 40 | int int1, 41 | int int2, 42 | String str1, 43 | byte[] val1, 44 | Object val2 45 | ) { 46 | this.primaryKey = primaryKey; 47 | this.bool1 = bool1; 48 | this.int1 = int1; 49 | this.int2 = int2; 50 | this.str1 = str1; 51 | this.val1 = val1; 52 | this.val2 = val2; 53 | } 54 | 55 | public String getPrimaryKey() { 56 | return primaryKey; 57 | } 58 | 59 | public boolean getBool1() { 60 | return bool1; 61 | } 62 | 63 | public byte[] getVal1() { 64 | return val1; 65 | } 66 | 67 | public int getInt1() { 68 | return int1; 69 | } 70 | 71 | public int getInt2() { 72 | return int2; 73 | } 74 | 75 | public String getStr1() { 76 | return str1; 77 | } 78 | 79 | public String toInsertQuery() { 80 | return format( 81 | "INSERT INTO %s (%s, bool1, int1, int2, str1) VALUES (%s, %b, %d, %d, %s)", 82 | TABLE_NAME, 83 | PRIMARY_KEY_COLUMN_NAME, 84 | primaryKey, 85 | bool1, 86 | int1, 87 | int2, 88 | str1 89 | ); 90 | } 91 | 92 | public String toPreparedInsertQuery() { 93 | return format( 94 | "INSERT INTO %s (%s, bool1, int1, int2, str1, val1, val2) VALUES (%s, %b, %d, %d, %s, ?, ?)", 95 | TABLE_NAME, 96 | PRIMARY_KEY_COLUMN_NAME, 97 | primaryKey, 98 | bool1, 99 | int1, 100 | int2, 101 | str1 102 | ); 103 | } 104 | 105 | public void assertResultSet(ResultSet resultSet) throws SQLException { 106 | assertResultSet(resultSet, true); 107 | } 108 | 109 | public void assertResultSet(ResultSet resultSet, boolean sendKey) throws SQLException { 110 | assertEquals(resultSet.getString(PRIMARY_KEY_COLUMN_NAME), sendKey ? primaryKey : null); 111 | assertEquals(resultSet.getBoolean("bool1"), bool1); 112 | assertEquals(resultSet.getInt("int1"), int1); 113 | assertEquals(resultSet.getInt("int2"), int2); 114 | assertEquals(resultSet.getString("str1"), str1); 115 | 116 | assertEquals(resultSet.getString(1), sendKey ? primaryKey : null); 117 | assertEquals(resultSet.getBoolean(2), bool1); 118 | assertEquals(resultSet.getInt(3), int1); 119 | assertEquals(resultSet.getInt(4), int2); 120 | assertEquals(resultSet.getString(5), str1); 121 | } 122 | 123 | public void assertPreparedResultSet(ResultSet resultSet) throws SQLException { 124 | assertResultSet(resultSet); 125 | assertEquals(resultSet.getBytes("val1"), val1); 126 | assertEquals(resultSet.getBytes(6), val1); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/com/aerospike/jdbc/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.aerospike.jdbc.util; 2 | 3 | import java.sql.Connection; 4 | import java.sql.ResultSet; 5 | import java.sql.ResultSetMetaData; 6 | import java.sql.SQLException; 7 | import java.sql.Statement; 8 | 9 | public final class TestUtil { 10 | 11 | private TestUtil() { 12 | } 13 | 14 | public static void printResultSet(ResultSet rs) throws SQLException { 15 | ResultSetMetaData resultSetMetaData = rs.getMetaData(); 16 | for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { 17 | if (i != 1) { 18 | System.out.print(", "); 19 | } 20 | System.out.print(resultSetMetaData.getColumnName(i)); 21 | } 22 | System.out.println(); 23 | while (rs.next()) { 24 | for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { 25 | if (i != 1) { 26 | System.out.print(", "); 27 | } 28 | System.out.print(rs.getString(i)); 29 | } 30 | System.out.println(); 31 | } 32 | } 33 | 34 | public static void closeQuietly(AutoCloseable resource) { 35 | if (resource != null) { 36 | try { 37 | resource.close(); 38 | } catch (Exception ignore) { 39 | } 40 | } 41 | } 42 | 43 | public static boolean executeQuery(Connection conn, String sql) throws SQLException { 44 | Statement statement = conn.createStatement(); 45 | ResultSet resultSet = statement.executeQuery(sql); 46 | boolean hasNext = resultSet.next(); 47 | resultSet.close(); 48 | statement.close(); 49 | return hasNext; 50 | } 51 | 52 | @SuppressWarnings("all") 53 | public static void sleep(long millis) { 54 | try { 55 | Thread.sleep(millis); 56 | } catch (InterruptedException e) { 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/resources/testng.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------