├── project ├── build.properties ├── Configs.scala ├── PackagingTypePlugin.scala ├── plugins.sbt └── Tests.scala ├── .gitignore ├── .travis.yml ├── src ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── mmolimar │ │ └── ksql │ │ └── jdbc │ │ ├── WrapperNotSupported.scala │ │ ├── package.scala │ │ ├── resultset │ │ ├── ResultSetMetaData.scala │ │ ├── KsqlResultSetMetaData.scala │ │ ├── KsqlResultSet.scala │ │ └── ResultSet.scala │ │ ├── KsqlDriver.scala │ │ ├── Exceptions.scala │ │ ├── KsqlConnection.scala │ │ ├── Headers.scala │ │ └── KsqlStatement.scala ├── it │ ├── resources │ │ └── log4j.properties │ └── scala │ │ ├── io │ │ └── confluent │ │ │ └── ksql │ │ │ └── rest │ │ │ └── server │ │ │ └── mock.scala │ │ └── com │ │ └── github │ │ └── mmolimar │ │ └── ksql │ │ └── jdbc │ │ ├── embedded │ │ ├── EmbeddedZookeeperServer.scala │ │ ├── EmbeddedKsqlEngine.scala │ │ ├── EmbeddedKafkaConnect.scala │ │ └── EmbeddedKafkaCluster.scala │ │ └── KsqlDriverIntegrationTest.scala └── test │ └── scala │ ├── io │ └── confluent │ │ └── ksql │ │ └── rest │ │ └── client │ │ └── MockableKsqlRestClient.scala │ └── com │ └── github │ └── mmolimar │ └── ksql │ └── jdbc │ ├── KsqlConnectionSpec.scala │ ├── resultset │ ├── KsqlResultSetMetaDataSpec.scala │ └── KsqlResultSetSpec.scala │ ├── utils │ └── TestUtils.scala │ ├── KsqlDriverSpec.scala │ ├── KsqlDatabaseMetaDataSpec.scala │ └── KsqlStatementSpec.scala ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.6 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | project/target 3 | project/project 4 | .idea/ 5 | .idea_modules/ 6 | *.iml 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /project/Configs.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Configs { 4 | val IntegrationTest = config("it") extend (Test) 5 | val all: Seq[Configuration] = Seq(IntegrationTest) 6 | } 7 | -------------------------------------------------------------------------------- /project/PackagingTypePlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object PackagingTypePlugin extends AutoPlugin { 4 | override val buildSettings = { 5 | sys.props += "packaging.type" -> "jar" 6 | Nil 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.12.10 4 | jdk: 5 | - openjdk8 6 | script: 7 | - sbt clean coverage test it:test coverageReport && sbt coverageAggregate 8 | after_success: 9 | - sbt coveralls 10 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 4 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10") 6 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/WrapperNotSupported.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.Wrapper 4 | 5 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 6 | 7 | trait WrapperNotSupported extends Wrapper { 8 | 9 | override def unwrap[T](iface: Class[T]): T = throw NotSupported("unknown") 10 | 11 | override def isWrapperFor(iface: Class[_]): Boolean = throw NotSupported("unknown") 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/it/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.stdout.layout.ConversionPattern=[%d] %p %c{1}:%L - %m%n 6 | 7 | log4j.logger.kafka=ERROR, stdout 8 | log4j.logger.org.apache.zookeeper=ERROR, stdout 9 | log4j.logger.org.apache.kafka=ERROR, stdout 10 | log4j.logger.org.I0Itec.zkclient=ERROR, stdout 11 | log4j.logger.org.reflections=ERROR, stdout 12 | -------------------------------------------------------------------------------- /src/test/scala/io/confluent/ksql/rest/client/MockableKsqlRestClient.scala: -------------------------------------------------------------------------------- 1 | package io.confluent.ksql.rest.client 2 | 3 | import java.util.Collections.emptyMap 4 | import java.util.Optional 5 | 6 | import io.confluent.ksql.properties.LocalProperties 7 | 8 | class MockableKsqlRestClient extends KsqlRestClient( 9 | new KsqlClient( 10 | emptyMap[String, String], 11 | Optional.empty[BasicCredentials], 12 | new LocalProperties(emptyMap[String, Any]) 13 | ), 14 | "http://0.0.0.0", 15 | new LocalProperties(emptyMap[String, Any]) 16 | ) 17 | -------------------------------------------------------------------------------- /project/Tests.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt.{Def, _} 3 | 4 | object Tests { 5 | 6 | import Configs._ 7 | 8 | private lazy val testSettings = Seq( 9 | fork in Test := false, 10 | parallelExecution in Test := false 11 | ) 12 | private lazy val itSettings = inConfig(IntegrationTest)(Defaults.testSettings) ++ Seq( 13 | fork in IntegrationTest := false, 14 | parallelExecution in IntegrationTest := false, 15 | scalaSource in IntegrationTest := baseDirectory.value / "src/it/scala" 16 | ) 17 | private lazy val testAll = TaskKey[Unit]("testAll", "Executes unit and integration tests.") 18 | 19 | lazy val settings: Seq[Def.Setting[_]] = testSettings ++ itSettings ++ Seq( 20 | testAll := (test in Test).dependsOn(test in IntegrationTest).value 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/it/scala/io/confluent/ksql/rest/server/mock.scala: -------------------------------------------------------------------------------- 1 | package io.confluent.ksql.rest.server 2 | 3 | import java.util.function.{Function => JFunction, Supplier => JSupplier} 4 | 5 | import io.confluent.ksql.version.metrics.VersionCheckerAgent 6 | 7 | import scala.language.implicitConversions 8 | 9 | object mock { 10 | 11 | implicit def toJavaSupplier[A](f: () => A): JSupplier[A] = new JSupplier[A] { 12 | override def get: A = f() 13 | } 14 | 15 | implicit def toJavaFunction[A, B](f: A => B): JFunction[A, B] = (a: A) => f(a) 16 | 17 | def ksqlRestApplication(config: KsqlRestConfig, versionCheckerAgent: VersionCheckerAgent): KsqlRestApplication = { 18 | KsqlRestApplication.buildApplication(config, (_: JSupplier[java.lang.Boolean]) => versionCheckerAgent) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql 2 | 3 | import java.sql.ResultSet 4 | 5 | import scala.language.implicitConversions 6 | 7 | package object jdbc { 8 | 9 | object implicits { 10 | 11 | implicit class ResultSetStream(resultSet: ResultSet) { 12 | 13 | def toStream: Stream[ResultSet] = new Iterator[ResultSet] { 14 | 15 | def hasNext(): Boolean = resultSet.next 16 | 17 | def next(): ResultSet = resultSet 18 | 19 | }.toStream 20 | } 21 | 22 | implicit def toIndexedMap(headers: List[HeaderField]): Map[Int, HeaderField] = { 23 | headers.zipWithIndex.map { case (header, index) => 24 | HeaderField(header.name, header.label, header.jdbcType, header.length, index + 1) 25 | }.map(h => h.index -> h).toMap 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/it/scala/com/github/mmolimar/ksql/jdbc/embedded/EmbeddedZookeeperServer.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.embedded 2 | 3 | import java.io.{File, IOException} 4 | import java.net.InetSocketAddress 5 | 6 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils 7 | import kafka.utils.Logging 8 | import org.apache.zookeeper.server.{ServerCnxnFactory, ZooKeeperServer} 9 | 10 | class EmbeddedZookeeperServer(private val port: Int = TestUtils.getAvailablePort, 11 | private val tickTime: Int = 500) extends Logging { 12 | 13 | private val snapshotDir: File = TestUtils.makeTempDir("snapshot") 14 | private val logDir: File = TestUtils.makeTempDir("log") 15 | private val zookeeper: ZooKeeperServer = new ZooKeeperServer(snapshotDir, logDir, tickTime) 16 | private val factory: ServerCnxnFactory = ServerCnxnFactory.createFactory(new InetSocketAddress("localhost", port), 0) 17 | 18 | @throws[IOException] 19 | def startup(): Unit = { 20 | info("Starting up embedded Zookeeper") 21 | 22 | factory.startup(zookeeper) 23 | 24 | info("Started embedded Zookeeper: " + getConnection) 25 | } 26 | 27 | def shutdown(): Unit = { 28 | info("Shutting down embedded Zookeeper") 29 | 30 | TestUtils.swallow(zookeeper.shutdown()) 31 | TestUtils.swallow(factory.shutdown()) 32 | 33 | TestUtils.deleteFile(snapshotDir) 34 | TestUtils.deleteFile(logDir) 35 | 36 | info("Shutted down embedded Zookeeper") 37 | } 38 | 39 | def getPort: Int = port 40 | 41 | def getConnection: String = "localhost:" + getPort 42 | 43 | override def toString: String = { 44 | val sb: StringBuilder = new StringBuilder("Zookeeper{") 45 | sb.append("connection=").append(getConnection) 46 | sb.append('}') 47 | 48 | sb.toString 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/it/scala/com/github/mmolimar/ksql/jdbc/embedded/EmbeddedKsqlEngine.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.embedded 2 | 3 | import java.io.IOException 4 | 5 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils 6 | import io.confluent.ksql.rest.server.{KsqlRestApplication, KsqlRestConfig} 7 | import io.confluent.ksql.version.metrics.VersionCheckerAgent 8 | import io.confluent.rest.RestConfig 9 | import kafka.utils.Logging 10 | import org.apache.kafka.clients.producer.ProducerConfig 11 | import org.scalamock.scalatest.MockFactory 12 | import io.confluent.ksql.util.KsqlConfig 13 | 14 | import scala.collection.JavaConverters._ 15 | 16 | class EmbeddedKsqlEngine(port: Int = TestUtils.getAvailablePort, brokerList: String, connectUrl: String) extends Logging with MockFactory { 17 | 18 | private val config = new KsqlRestConfig(Map( 19 | RestConfig.LISTENERS_CONFIG -> s"http://localhost:$port", 20 | ProducerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokerList, 21 | KsqlConfig.CONNECT_URL_PROPERTY -> connectUrl, 22 | "ksql.service.id" -> "ksql-jdbc", 23 | "ksql.streams.auto.offset.reset" -> "latest", 24 | "ksql.command.topic.suffix" -> "commands" 25 | ).asJava) 26 | 27 | lazy val ksqlEngine: KsqlRestApplication = { 28 | import io.confluent.ksql.rest.server.mock.ksqlRestApplication 29 | 30 | val versionCheckerAgent = mock[VersionCheckerAgent] 31 | (versionCheckerAgent.start _).expects(*, *).returns((): Unit).anyNumberOfTimes 32 | (versionCheckerAgent.updateLastRequestTime _).expects().returns((): Unit).anyNumberOfTimes 33 | ksqlRestApplication(config, versionCheckerAgent) 34 | } 35 | 36 | @throws[IOException] 37 | def startup(): Unit = { 38 | info("Starting up embedded KSQL engine") 39 | 40 | ksqlEngine.start() 41 | 42 | info("Started embedded Zookeeper: " + getConnection) 43 | } 44 | 45 | def shutdown(): Unit = { 46 | info("Shutting down embedded KSQL engine") 47 | 48 | TestUtils.swallow(ksqlEngine.stop()) 49 | 50 | info("Stopped embedded KSQL engine") 51 | } 52 | 53 | def getPort: Int = port 54 | 55 | def getConnection: String = "localhost:" + getPort 56 | 57 | override def toString: String = { 58 | val sb: StringBuilder = new StringBuilder("KSQL{") 59 | sb.append("connection=").append(getConnection) 60 | sb.append('}') 61 | 62 | sb.toString 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/resultset/ResultSetMetaData.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.sql.ResultSetMetaData 4 | 5 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 6 | import com.github.mmolimar.ksql.jdbc.{NotSupported, WrapperNotSupported} 7 | 8 | 9 | private[resultset] class ResultSetMetaDataNotSupported extends ResultSetMetaData with WrapperNotSupported { 10 | 11 | override def getCatalogName(column: Int): String = throw NotSupported("getCatalogName") 12 | 13 | override def getColumnClassName(column: Int): String = throw NotSupported("getColumnClassName") 14 | 15 | override def getColumnCount: Int = throw NotSupported("getColumnCount") 16 | 17 | override def getColumnDisplaySize(column: Int): Int = throw NotSupported("getColumnDisplaySize") 18 | 19 | override def getColumnLabel(column: Int): String = throw NotSupported("getColumnLabel") 20 | 21 | override def getColumnName(column: Int): String = throw NotSupported("getColumnName") 22 | 23 | override def getColumnTypeName(column: Int): String = throw NotSupported("getColumnTypeName") 24 | 25 | override def getColumnType(column: Int): Int = throw NotSupported("getColumnType") 26 | 27 | override def getPrecision(column: Int): Int = throw NotSupported("getPrecision") 28 | 29 | override def getSchemaName(column: Int): String = throw NotSupported("getSchemaName") 30 | 31 | override def getScale(column: Int): Int = throw NotSupported("getScale") 32 | 33 | override def getTableName(column: Int): String = throw NotSupported("getTableName") 34 | 35 | override def isAutoIncrement(column: Int): Boolean = throw NotSupported("isAutoIncrement") 36 | 37 | override def isCaseSensitive(column: Int): Boolean = throw NotSupported("isCaseSensitive") 38 | 39 | override def isCurrency(column: Int): Boolean = throw NotSupported("isCurrency") 40 | 41 | override def isDefinitelyWritable(column: Int): Boolean = throw NotSupported("isDefinitelyWritable") 42 | 43 | override def isNullable(column: Int): Int = throw NotSupported("isNullable") 44 | 45 | override def isReadOnly(column: Int): Boolean = throw NotSupported("isReadOnly") 46 | 47 | override def isSearchable(column: Int): Boolean = throw NotSupported("isSearchable") 48 | 49 | override def isSigned(column: Int): Boolean = throw NotSupported("isSigned") 50 | 51 | override def isWritable(column: Int): Boolean = throw NotSupported("isWritable") 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/KsqlDriver.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.{Connection, Driver, DriverPropertyInfo} 4 | import java.util.Properties 5 | import java.util.logging.Logger 6 | 7 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 8 | 9 | import scala.util.matching.Regex 10 | 11 | object KsqlDriver { 12 | 13 | val ksqlName = "ksqlDB" 14 | val ksqlPrefix = "jdbc:ksql://" 15 | 16 | val driverName = "ksqlDB JDBC driver" 17 | val driverMajorVersion = 1 18 | val driverMinorVersion = 2 19 | val driverVersion = s"$driverMajorVersion.$driverMinorVersion" 20 | 21 | val jdbcMajorVersion = 4 22 | val jdbcMinorVersion = 1 23 | 24 | val ksqlMajorVersion = 5 25 | val ksqlMinorVersion = 4 26 | val ksqlMicroVersion = 0 27 | val ksqlVersion = s"$ksqlMajorVersion.$ksqlMinorVersion.$ksqlMicroVersion" 28 | 29 | private val ksqlUserPassRegex = "((.+):(.+)@){0,1}" 30 | private val ksqlServerRegex = "([A-Za-z0-9._%+-]+):([0-9]{1,5})" 31 | private val ksqlPropsRegex = "(\\?([A-Za-z0-9._-]+=[A-Za-z0-9._-]+(&[A-Za-z0-9._-]+=[A-Za-z0-9._-]+)*)){0,1}" 32 | 33 | val urlRegex: Regex = s"$ksqlPrefix$ksqlUserPassRegex$ksqlServerRegex$ksqlPropsRegex\\z".r 34 | 35 | def parseUrl(url: String): KsqlConnectionValues = url match { 36 | case urlRegex(_, username, password, ksqlServer, port, _, props, _) => 37 | KsqlConnectionValues( 38 | ksqlServer, 39 | port.toInt, 40 | Option(username), 41 | Option(password), 42 | Option(props).map(_.split("&").map(_.split("=")).map(p => p(0) -> p(1)).toMap).getOrElse(Map.empty) 43 | ) 44 | case _ => throw InvalidUrl(url) 45 | } 46 | } 47 | 48 | class KsqlDriver extends Driver { 49 | 50 | override def acceptsURL(url: String): Boolean = Option(url).exists(_.startsWith(KsqlDriver.ksqlPrefix)) 51 | 52 | override def jdbcCompliant: Boolean = false 53 | 54 | override def getPropertyInfo(url: String, info: Properties): scala.Array[DriverPropertyInfo] = scala.Array.empty 55 | 56 | override def getMinorVersion: Int = KsqlDriver.driverMinorVersion 57 | 58 | override def getMajorVersion: Int = KsqlDriver.driverMajorVersion 59 | 60 | override def getParentLogger: Logger = throw NotSupported("getParentLogger") 61 | 62 | override def connect(url: String, properties: Properties): Connection = { 63 | if (!acceptsURL(url)) throw InvalidUrl(url) 64 | 65 | val connection = buildConnection(KsqlDriver.parseUrl(url), properties) 66 | connection.validate() 67 | connection 68 | } 69 | 70 | private[jdbc] def buildConnection(values: KsqlConnectionValues, properties: Properties): KsqlConnection = { 71 | new KsqlConnection(values, properties) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ksqlDB JDBC Driver [![Build Status](https://travis-ci.org/mmolimar/ksql-jdbc-driver.svg?branch=master)](https://travis-ci.org/mmolimar/ksql-jdbc-driver)[![Coverage Status](https://coveralls.io/repos/github/mmolimar/ksql-jdbc-driver/badge.svg?branch=master)](https://coveralls.io/github/mmolimar/ksql-jdbc-driver?branch=master) 2 | 3 | **ksql-jdbc-driver** is a Type 4 Java Database Connectivity (JDBC) driver that provides standard access to 4 | Apache Kafka via JDBC API. 5 | 6 | The driver connects to the [ksqlDB engine](https://ksqldb.io/) then, the engine translates those requests 7 | to Kafka requests. 8 | 9 | ## Getting started 10 | 11 | ### Building from source ### 12 | 13 | Just clone the ``ksql-jdbc-driver`` repo and package it: 14 | 15 | ``git clone https://github.com/mmolimar/ksql-jdbc-driver.git && cd ksql-jdbc-driver`` 16 | 17 | ``sbt clean package`` 18 | 19 | If you want to build a fat jar containing both classes and dependencies -for instance, to use it in a 20 | JDBC client such as [SQuirrel SQL](http://squirrel-sql.sourceforge.net/) or whichever-, type the following: 21 | 22 | ``sbt clean assembly`` 23 | 24 | ### Running tests ### 25 | 26 | To run unit and integration tests, execute the following: 27 | 28 | ``sbt test it:test`` 29 | 30 | #### Coverage ### 31 | 32 | To know the test coverage of the driver: 33 | 34 | ``sbt clean coverage test it:test coverageReport`` 35 | 36 | ## Usage 37 | 38 | As expected, the driver can be used as we are used to. So, in your application, register the driver (depending on 39 | your JVM), for example: 40 | 41 | * ``java.sql.DriverManager.registerDriver(new com.github.mmolimar.ksql.jdbc.KsqlDriver)`` 42 | 43 | or 44 | 45 | * ``Class.forName("com.github.mmolimar.ksql.jdbc.KsqlDriver")`` 46 | 47 | ### Connection URL 48 | 49 | The URL has the form ``jdbc:ksql://[:@]:[?=&=...]`` 50 | 51 | where: 52 | 53 | * **\:\**: optional username and password to log into ksqlDB. 54 | * **\**: represents the ksqlDB engine host. 55 | * **\**: ksqlDB engine port. 56 | * **\**: are the custom client properties (optionals). Available properties: 57 | * ``secured``: sets if the ksqlDB connection is secured or not. It's a boolean (``true``|``false``) and its default 58 | value is ``false``. 59 | * ``properties``: enables to set in ksqlDB extra properties from the JDBC URL. It's a boolean (``true``|``false``) 60 | and its default value is ``false``. 61 | * ``timeout``: sets the max wait time between each message when receiving them. It's a long and its default 62 | value is ``0`` which means that is infinite. 63 | 64 | ## TODO's 65 | 66 | - [ ] Standalone mode: connecting directly to Kafka brokers. 67 | - [ ] Make the driver more compliant with the JDBC spec. 68 | 69 | ## Contribute 70 | 71 | - Source Code: https://github.com/mmolimar/ksql-jdbc-driver 72 | - Issue Tracker: https://github.com/mmolimar/ksql-jdbc-driver/issues 73 | 74 | ## License 75 | 76 | Released under the Apache License, version 2.0. 77 | -------------------------------------------------------------------------------- /src/it/scala/com/github/mmolimar/ksql/jdbc/embedded/EmbeddedKafkaConnect.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.embedded 2 | 3 | import java.util 4 | 5 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils 6 | import kafka.utils.Logging 7 | import org.apache.kafka.common.utils.Time 8 | import org.apache.kafka.connect.connector.policy.ConnectorClientConfigOverridePolicy 9 | import org.apache.kafka.connect.runtime.isolation.Plugins 10 | import org.apache.kafka.connect.runtime.rest.RestServer 11 | import org.apache.kafka.connect.runtime.standalone.{StandaloneConfig, StandaloneHerder} 12 | import org.apache.kafka.connect.runtime.{Connect, Worker, WorkerConfig} 13 | import org.apache.kafka.connect.storage.FileOffsetBackingStore 14 | import org.apache.kafka.connect.util.ConnectUtils 15 | 16 | import scala.collection.JavaConverters._ 17 | import scala.reflect.io.File 18 | 19 | class EmbeddedKafkaConnect(brokerList: String, port: Int = TestUtils.getAvailablePort) extends Logging { 20 | 21 | private val workerProps: util.Map[String, String] = Map[String, String]( 22 | WorkerConfig.LISTENERS_CONFIG -> s"http://localhost:$port", 23 | WorkerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokerList, 24 | WorkerConfig.KEY_CONVERTER_CLASS_CONFIG -> "org.apache.kafka.connect.converters.ByteArrayConverter", 25 | WorkerConfig.VALUE_CONVERTER_CLASS_CONFIG -> "org.apache.kafka.connect.converters.ByteArrayConverter", 26 | StandaloneConfig.OFFSET_STORAGE_FILE_FILENAME_CONFIG -> File.makeTemp(prefix = "connect.offsets").jfile.getAbsolutePath 27 | ).asJava 28 | 29 | private lazy val kafkaConnect: Connect = buildConnect 30 | 31 | def startup(): Unit = { 32 | info("Starting up embedded Kafka connect") 33 | 34 | kafkaConnect.start() 35 | 36 | info(s"Started embedded Kafka connect on port: $port") 37 | } 38 | 39 | def shutdown(): Unit = { 40 | info("Shutting down embedded Kafka Connect") 41 | 42 | TestUtils.swallow(kafkaConnect.stop()) 43 | 44 | info("Stopped embedded Kafka Connect") 45 | } 46 | 47 | private def buildConnect: Connect = { 48 | val config = new StandaloneConfig(workerProps) 49 | val kafkaClusterId = ConnectUtils.lookupKafkaClusterId(config) 50 | 51 | val rest = new RestServer(config) 52 | rest.initializeServer() 53 | 54 | val advertisedUrl = rest.advertisedUrl 55 | val workerId = advertisedUrl.getHost + ":" + advertisedUrl.getPort 56 | val plugins = new Plugins(workerProps) 57 | val connectorClientConfigOverridePolicy = plugins.newPlugin( 58 | config.getString(WorkerConfig.CONNECTOR_CLIENT_POLICY_CLASS_CONFIG), config, classOf[ConnectorClientConfigOverridePolicy]) 59 | val worker = new Worker(workerId, Time.SYSTEM, plugins, config, new FileOffsetBackingStore, connectorClientConfigOverridePolicy) 60 | val herder = new StandaloneHerder(worker, kafkaClusterId, connectorClientConfigOverridePolicy) 61 | 62 | new Connect(herder, rest) 63 | } 64 | 65 | def getPort: Int = port 66 | 67 | def getWorker: String = s"localhost:$port" 68 | 69 | def getUrl: String = s"http://localhost:$port" 70 | 71 | override def toString: String = { 72 | val sb: StringBuilder = StringBuilder.newBuilder 73 | sb.append("KafkaConnect{") 74 | sb.append("port=").append(port) 75 | sb.append('}') 76 | 77 | sb.toString 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/resultset/KsqlResultSetMetaData.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.sql.{ResultSetMetaData, Types} 4 | 5 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 6 | import com.github.mmolimar.ksql.jdbc.implicits.toIndexedMap 7 | import com.github.mmolimar.ksql.jdbc.{HeaderField, InvalidColumn} 8 | import io.confluent.ksql.schema.ksql.{SqlBaseType => KsqlType} 9 | 10 | 11 | class KsqlResultSetMetaData(private[jdbc] val columns: List[HeaderField]) extends ResultSetMetaDataNotSupported { 12 | 13 | private val fieldByIndex: Map[Int, HeaderField] = columns 14 | 15 | private def getField(index: Int): HeaderField = fieldByIndex.getOrElse(index, 16 | throw InvalidColumn(s"Column with index '$index' does not exist.")) 17 | 18 | override def getColumnClassName(column: Int): String = { 19 | getField(column).jdbcType match { 20 | case Types.INTEGER => classOf[java.lang.Integer] 21 | case Types.BIGINT => classOf[java.lang.Long] 22 | case Types.DOUBLE => classOf[java.lang.Double] 23 | case Types.DECIMAL => classOf[java.math.BigDecimal] 24 | case Types.BOOLEAN => classOf[java.lang.Boolean] 25 | case Types.VARCHAR => classOf[java.lang.String] 26 | case Types.JAVA_OBJECT => classOf[java.util.Map[AnyRef, AnyRef]] 27 | case Types.ARRAY => classOf[java.sql.Array] 28 | case Types.STRUCT => classOf[java.sql.Struct] 29 | case _ => classOf[java.lang.String] 30 | } 31 | }.getName 32 | 33 | override def getColumnCount: Int = columns.size 34 | 35 | override def getColumnDisplaySize(column: Int): Int = getField(column).length 36 | 37 | override def getColumnLabel(column: Int): String = getField(column).label 38 | 39 | override def getColumnName(column: Int): String = getField(column).name 40 | 41 | override def getColumnTypeName(column: Int): String = { 42 | getField(column).jdbcType match { 43 | case Types.INTEGER => KsqlType.INTEGER 44 | case Types.BIGINT => KsqlType.BIGINT 45 | case Types.DOUBLE => KsqlType.DOUBLE 46 | case Types.DECIMAL => KsqlType.DECIMAL 47 | case Types.BOOLEAN => KsqlType.BOOLEAN 48 | case Types.VARCHAR => KsqlType.STRING 49 | case Types.JAVA_OBJECT => KsqlType.MAP 50 | case Types.ARRAY => KsqlType.ARRAY 51 | case Types.STRUCT => KsqlType.STRUCT 52 | case _ => KsqlType.STRING 53 | } 54 | }.name 55 | 56 | override def getColumnType(column: Int): Int = getField(column).jdbcType 57 | 58 | override def getPrecision(column: Int): Int = getField(column).jdbcType match { 59 | case Types.DOUBLE => -1 60 | case _ => 0 61 | } 62 | 63 | override def getScale(column: Int): Int = getField(column).jdbcType match { 64 | case Types.DOUBLE => -1 65 | case _ => 0 66 | } 67 | 68 | override def isCaseSensitive(column: Int): Boolean = getField(column).jdbcType match { 69 | case Types.VARCHAR => true 70 | case _ => false 71 | } 72 | 73 | override def isNullable(column: Int): Int = ResultSetMetaData.columnNullableUnknown 74 | 75 | override def isAutoIncrement(column: Int): Boolean = false 76 | 77 | override def isCurrency(column: Int): Boolean = false 78 | 79 | override def isSearchable(column: Int): Boolean = true 80 | 81 | override def isReadOnly(column: Int): Boolean = false 82 | 83 | override def isWritable(column: Int): Boolean = !isReadOnly(column) 84 | 85 | override def isDefinitelyWritable(column: Int): Boolean = isWritable(column) 86 | 87 | override def isSigned(column: Int): Boolean = getField(column).jdbcType match { 88 | case Types.TINYINT | Types.SMALLINT | Types.INTEGER | 89 | Types.BIGINT | Types.FLOAT | Types.DOUBLE | Types.DECIMAL => true 90 | case _ => false 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/it/scala/com/github/mmolimar/ksql/jdbc/embedded/EmbeddedKafkaCluster.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.embedded 2 | 3 | import java.io.File 4 | import java.util.Properties 5 | 6 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils 7 | import kafka.server.{KafkaConfig, KafkaServer} 8 | import kafka.utils.Logging 9 | import kafka.zk.AdminZkClient 10 | 11 | import scala.collection.Seq 12 | 13 | class EmbeddedKafkaCluster(zkConnection: String, 14 | ports: Seq[Int] = Seq(TestUtils.getAvailablePort), 15 | baseProps: Properties = new Properties) extends Logging { 16 | 17 | private val actualPorts: Seq[Int] = ports.map(resolvePort) 18 | 19 | private var brokers: Seq[KafkaServer] = Seq.empty 20 | private var logDirs: Seq[File] = Seq.empty 21 | 22 | private lazy val zkClient = TestUtils.buildZkClient(zkConnection) 23 | private lazy val adminZkClient = new AdminZkClient(zkClient) 24 | 25 | def startup(): Unit = { 26 | info("Starting up embedded Kafka brokers") 27 | 28 | for ((port, i) <- actualPorts.zipWithIndex) { 29 | val logDir: File = TestUtils.makeTempDir("kafka-local") 30 | 31 | val properties: Properties = new Properties(baseProps) 32 | properties.setProperty(KafkaConfig.ZkConnectProp, zkConnection) 33 | properties.setProperty(KafkaConfig.ZkSyncTimeMsProp, i.toString) 34 | properties.setProperty(KafkaConfig.BrokerIdProp, (i + 1).toString) 35 | properties.setProperty(KafkaConfig.HostNameProp, "localhost") 36 | properties.setProperty(KafkaConfig.AdvertisedHostNameProp, "localhost") 37 | properties.setProperty(KafkaConfig.PortProp, port.toString) 38 | properties.setProperty(KafkaConfig.AdvertisedPortProp, port.toString) 39 | properties.setProperty(KafkaConfig.LogDirProp, logDir.getAbsolutePath) 40 | properties.setProperty(KafkaConfig.NumPartitionsProp, 1.toString) 41 | properties.setProperty(KafkaConfig.AutoCreateTopicsEnableProp, true.toString) 42 | properties.setProperty(KafkaConfig.DeleteTopicEnableProp, true.toString) 43 | properties.setProperty(KafkaConfig.LogFlushIntervalMessagesProp, 1.toString) 44 | properties.setProperty(KafkaConfig.OffsetsTopicReplicationFactorProp, 1.toString) 45 | 46 | info(s"Local directory for broker ID ${i + 1} is ${logDir.getAbsolutePath}") 47 | 48 | brokers :+= startBroker(properties) 49 | logDirs :+= logDir 50 | } 51 | 52 | info(s"Started embedded Kafka brokers: $getBrokerList") 53 | } 54 | 55 | def shutdown(): Unit = { 56 | brokers.foreach(broker => TestUtils.swallow(broker.shutdown)) 57 | logDirs.foreach(logDir => TestUtils.swallow(TestUtils.deleteFile(logDir))) 58 | } 59 | 60 | def getPorts: Seq[Int] = actualPorts 61 | 62 | def getBrokerList: String = actualPorts.map("localhost:" + _).mkString(",") 63 | 64 | def createTopic(topic: String, numPartitions: Int = 1, replicationFactor: Int = 1): Unit = { 65 | info(s"Creating topic $topic") 66 | adminZkClient.createTopic(topic, numPartitions, replicationFactor) 67 | } 68 | 69 | def deleteTopic(topic: String) { 70 | info(s"Deleting topic $topic") 71 | adminZkClient.deleteTopic(topic) 72 | } 73 | 74 | def deleteTopics(topics: Seq[String]): Unit = topics.foreach(deleteTopic) 75 | 76 | def existTopic(topic: String): Boolean = zkClient.topicExists(topic) 77 | 78 | def listTopics: Set[String] = zkClient.getAllTopicsInCluster 79 | 80 | private def resolvePort(port: Int) = if (port <= 0) TestUtils.getAvailablePort else port 81 | 82 | private def startBroker(props: Properties): KafkaServer = { 83 | val server = new KafkaServer(new KafkaConfig(props)) 84 | server.startup 85 | server 86 | } 87 | 88 | override def toString: String = { 89 | val sb: StringBuilder = StringBuilder.newBuilder 90 | sb.append("Kafka{") 91 | sb.append("brokerList='").append(getBrokerList).append('\'') 92 | sb.append('}') 93 | 94 | sb.toString 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/KsqlConnectionSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.{Connection, SQLException, SQLFeatureNotSupportedException} 4 | import java.util.{Collections, Properties} 5 | 6 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils._ 7 | import io.confluent.ksql.rest.client.{KsqlRestClient, MockableKsqlRestClient, RestResponse} 8 | import io.confluent.ksql.rest.entity._ 9 | import org.eclipse.jetty.http.HttpStatus.Code 10 | import org.scalamock.scalatest.MockFactory 11 | import org.scalatest.matchers.should.Matchers 12 | import org.scalatest.wordspec.AnyWordSpec 13 | 14 | class KsqlConnectionSpec extends AnyWordSpec with Matchers with MockFactory { 15 | 16 | "A KsqlConnection" when { 17 | 18 | "validating specs" should { 19 | val values = KsqlConnectionValues("localhost", 8080, None, None, Map.empty[String, String]) 20 | val mockKsqlRestClient = mock[MockableKsqlRestClient] 21 | val ksqlConnection = new KsqlConnection(values, new Properties) { 22 | override def init: KsqlRestClient = mockKsqlRestClient 23 | } 24 | 25 | "throw not supported exception if not supported" in { 26 | val methods = implementedMethods[KsqlConnection] 27 | reflectMethods[KsqlConnection](methods = methods, implemented = false, obj = ksqlConnection) 28 | .foreach(method => { 29 | assertThrows[SQLFeatureNotSupportedException] { 30 | method() 31 | } 32 | }) 33 | } 34 | 35 | "work if implemented" in { 36 | assertThrows[SQLException] { 37 | ksqlConnection.isClosed 38 | } 39 | ksqlConnection.getTransactionIsolation should be(Connection.TRANSACTION_NONE) 40 | ksqlConnection.setClientInfo(new Properties) 41 | 42 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 43 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, new KsqlEntityList)) 44 | ksqlConnection.setClientInfo("", "") 45 | assertThrows[SQLException] { 46 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 47 | .returns(RestResponse.erroneous(Code.INTERNAL_SERVER_ERROR, new KsqlErrorMessage(-1, "", Collections.emptyList[String]))) 48 | ksqlConnection.setClientInfo("", "") 49 | } 50 | 51 | ksqlConnection.isReadOnly should be(false) 52 | 53 | (mockKsqlRestClient.makeStatusRequest _: () => RestResponse[CommandStatuses]).expects 54 | .returns(RestResponse.successful[CommandStatuses] 55 | (Code.OK, new CommandStatuses(Collections.emptyMap[CommandId, CommandStatus.Status]))) 56 | ksqlConnection.isValid(0) should be(true) 57 | 58 | Option(ksqlConnection.getMetaData) should not be None 59 | 60 | Option(ksqlConnection.createStatement) should not be None 61 | assertThrows[SQLFeatureNotSupportedException] { 62 | ksqlConnection.createStatement(-1, -1) 63 | } 64 | ksqlConnection.setAutoCommit(true) 65 | ksqlConnection.setAutoCommit(false) 66 | ksqlConnection.getAutoCommit should be(false) 67 | ksqlConnection.getSchema should be(None.orNull) 68 | ksqlConnection.getWarnings should be(None.orNull) 69 | ksqlConnection.getCatalog should be(None.orNull) 70 | ksqlConnection.setCatalog("test") 71 | ksqlConnection.getCatalog should be(None.orNull) 72 | 73 | (mockKsqlRestClient.close _).expects 74 | ksqlConnection.close() 75 | ksqlConnection.isClosed should be(true) 76 | ksqlConnection.commit() 77 | } 78 | } 79 | } 80 | 81 | "A ConnectionNotSupported" when { 82 | 83 | "validating specs" should { 84 | 85 | "throw not supported exception if not supported" in { 86 | 87 | val resultSet = new ConnectionNotSupported 88 | reflectMethods[ConnectionNotSupported](methods = Seq.empty, implemented = false, obj = resultSet) 89 | .foreach(method => { 90 | assertThrows[SQLFeatureNotSupportedException] { 91 | method() 92 | } 93 | }) 94 | } 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/Exceptions.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.{SQLException, SQLFeatureNotSupportedException} 4 | 5 | import io.confluent.ksql.rest.entity.KsqlErrorMessage 6 | 7 | import scala.language.implicitConversions 8 | 9 | sealed trait KsqlException { 10 | 11 | def message: String 12 | 13 | def cause: Throwable 14 | 15 | } 16 | 17 | case class InvalidUrl(url: String, override val cause: Throwable = None.orNull) extends KsqlException { 18 | override def message = s"URL with value $url is not valid. It must match the regex '${KsqlDriver.urlRegex}'." 19 | } 20 | 21 | case class CannotConnect(url: String, msg: String, override val cause: Throwable = None.orNull) extends KsqlException { 22 | override def message = s"Cannot connect to this URL $url. Error message: $msg" 23 | } 24 | 25 | case class NotConnected(url: String, override val cause: Throwable = None.orNull) extends KsqlException { 26 | override def message = s"Not connected to database: '$url'" 27 | } 28 | 29 | case class InvalidProperty(name: String, override val cause: Throwable = None.orNull) extends KsqlException { 30 | override def message = s"Invalid property '$name'." 31 | } 32 | 33 | case class NotSupported(feature: String, override val cause: Throwable = None.orNull) extends KsqlException { 34 | override val message = s"Feature not supported: $feature." 35 | } 36 | 37 | case class InvalidValue(prop: String, value: String, override val cause: Throwable = None.orNull) extends KsqlException { 38 | override val message = s"value '$value' is not valid for property: '$prop'." 39 | } 40 | 41 | case class AlreadyClosed(override val message: String = "Already closed.", 42 | override val cause: Throwable = None.orNull) extends KsqlException 43 | 44 | class KsqlError(val prefix: String, val ksqlMessage: Option[KsqlErrorMessage], val cause: Throwable) extends KsqlException { 45 | override def message: String = 46 | s"$prefix.${ksqlMessage.map(msg => s" Error code [${msg.getErrorCode}]. Message: ${msg.getMessage}").getOrElse("")}" 47 | } 48 | 49 | case class KsqlQueryError(override val prefix: String = "Error executing query.", 50 | override val ksqlMessage: Option[KsqlErrorMessage] = None, 51 | override val cause: Throwable = None.orNull) extends KsqlError(prefix, ksqlMessage, cause) 52 | 53 | case class KsqlCommandError(override val prefix: String = "Error executing command.", 54 | override val ksqlMessage: Option[KsqlErrorMessage] = None, 55 | override val cause: Throwable = None.orNull) extends KsqlError(prefix, ksqlMessage, cause) 56 | 57 | case class KsqlEntityListError(override val prefix: String = "Invalid KSQL entity list.", 58 | override val ksqlMessage: Option[KsqlErrorMessage] = None, 59 | override val cause: Throwable = None.orNull) extends KsqlError(prefix, ksqlMessage, cause) 60 | 61 | case class InvalidColumn(override val message: String = "Invalid column.", 62 | override val cause: Throwable = None.orNull) extends KsqlException 63 | 64 | case class EmptyRow(override val message: String = "Current row is empty.", 65 | override val cause: Throwable = None.orNull) extends KsqlException 66 | 67 | case class ResultSetError(override val message: String = "Error accessing to the result set.", 68 | override val cause: Throwable = None.orNull) extends KsqlException 69 | 70 | case class UnknownTableType(override val message: String = "Table type does not exist.", 71 | override val cause: Throwable = None.orNull) extends KsqlException 72 | 73 | case class UnknownCatalog(override val message: String = "Catalog does not exist.", 74 | override val cause: Throwable = None.orNull) extends KsqlException 75 | 76 | case class UnknownSchema(override val message: String = "Schema does not exist.", 77 | override val cause: Throwable = None.orNull) extends KsqlException 78 | 79 | object Exceptions { 80 | 81 | implicit def wrapException(error: KsqlException): SQLException = { 82 | error match { 83 | case ns: NotSupported => new SQLFeatureNotSupportedException(ns.message) 84 | case e => new SQLException(e.message, e.cause) 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/resultset/KsqlResultSet.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.io.{Closeable, InputStream} 4 | import java.sql.{ResultSet, ResultSetMetaData} 5 | import java.util.{NoSuchElementException, Scanner, Iterator => JIterator} 6 | 7 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 8 | import com.github.mmolimar.ksql.jdbc.{EmptyRow, HeaderField} 9 | import io.confluent.ksql.GenericRow 10 | import io.confluent.ksql.rest.client.QueryStream 11 | import io.confluent.ksql.rest.entity.StreamedRow 12 | 13 | import scala.collection.JavaConverters._ 14 | import scala.concurrent.ExecutionContext.Implicits.global 15 | import scala.concurrent.duration._ 16 | import scala.concurrent.{Await, Future, TimeoutException} 17 | import scala.language.postfixOps 18 | import scala.util.{Failure, Success, Try} 19 | 20 | 21 | class IteratorResultSet[T <: Any](private val metadata: ResultSetMetaData, private val maxRows: Long, 22 | private[jdbc] val rows: Iterator[Seq[T]]) 23 | extends AbstractResultSet(metadata, maxRows, rows) { 24 | 25 | def this(columns: List[HeaderField], maxRows: Long, rows: Iterator[Seq[T]]) = 26 | this(new KsqlResultSetMetaData(columns), maxRows, rows) 27 | 28 | override protected def getValue[V <: AnyRef](columnIndex: Int): V = currentRow.get(columnIndex - 1).asInstanceOf[V] 29 | 30 | override protected def getColumnBounds: (Int, Int) = (1, currentRow.getOrElse(Seq.empty).size) 31 | 32 | override protected def closeInherit(): Unit = {} 33 | 34 | } 35 | 36 | trait KsqlStream extends Closeable with JIterator[StreamedRow] 37 | 38 | private[jdbc] class KsqlQueryStream(stream: QueryStream) extends KsqlStream { 39 | 40 | override def close(): Unit = stream.close() 41 | 42 | override def hasNext: Boolean = stream.hasNext 43 | 44 | override def next: StreamedRow = stream.next 45 | 46 | } 47 | 48 | private[jdbc] class KsqlInputStream(stream: InputStream) extends KsqlStream { 49 | private var isClosed = false 50 | private lazy val scanner = new Scanner(stream) 51 | 52 | override def close(): Unit = { 53 | isClosed = true 54 | scanner.close() 55 | } 56 | 57 | override def hasNext: Boolean = { 58 | if (isClosed) throw new IllegalStateException("Cannot call hasNext() when stream is closed.") 59 | scanner.hasNextLine 60 | } 61 | 62 | override def next: StreamedRow = { 63 | if (!hasNext) throw new NoSuchElementException 64 | StreamedRow.row(new GenericRow(scanner.nextLine)) 65 | } 66 | 67 | } 68 | 69 | class StreamedResultSet(private[jdbc] val metadata: ResultSetMetaData, 70 | private[jdbc] val stream: KsqlStream, private[resultset] val maxRows: Long, val timeout: Long = 0) 71 | extends AbstractResultSet[StreamedRow](metadata, maxRows, stream.asScala) { 72 | 73 | private val waitDuration = if (timeout > 0) timeout millis else Duration.Inf 74 | 75 | private var maxBound = 0 76 | 77 | protected override def nextResult: Boolean = { 78 | def hasNext = if (stream.hasNext) { 79 | stream.next match { 80 | case record if record.getHeader.isPresent && !record.getRow.isPresent => 81 | maxBound = record.getHeader.get.getSchema.columns.size 82 | next 83 | case record if record.getRow.isPresent => 84 | maxBound = record.getRow.get.getColumns.size 85 | currentRow = Some(record) 86 | true 87 | case _ => false 88 | } 89 | } else { 90 | false 91 | } 92 | 93 | Try(Await.result(Future(hasNext), waitDuration)) match { 94 | case Success(r) => r 95 | case Failure(_: TimeoutException) => false 96 | case Failure(e) => throw e 97 | } 98 | } 99 | 100 | override protected def closeInherit(): Unit = stream.close() 101 | 102 | override protected def getColumnBounds: (Int, Int) = (1, maxBound) 103 | 104 | override protected def getValue[T](columnIndex: Int): T = { 105 | currentRow.filter(_.getRow.isPresent).map(_.getRow.get.getColumnValue[T](columnIndex - 1)) 106 | .getOrElse(throw EmptyRow()) 107 | } 108 | 109 | override def getConcurrency: Int = ResultSet.CONCUR_READ_ONLY 110 | 111 | override def isAfterLast: Boolean = false 112 | 113 | override def isBeforeFirst: Boolean = false 114 | 115 | override def isFirst: Boolean = currentRow.isEmpty 116 | 117 | override def isLast: Boolean = !stream.hasNext 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/resultset/KsqlResultSetMetaDataSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.sql.{ResultSetMetaData, SQLFeatureNotSupportedException, Types} 4 | 5 | import com.github.mmolimar.ksql.jdbc.HeaderField 6 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils._ 7 | import io.confluent.ksql.schema.ksql.{SqlBaseType => KsqlType} 8 | import org.scalamock.scalatest.MockFactory 9 | import org.scalatest.OneInstancePerTest 10 | import org.scalatest.matchers.should.Matchers 11 | import org.scalatest.wordspec.AnyWordSpec 12 | 13 | class KsqlResultSetMetaDataSpec extends AnyWordSpec with Matchers with MockFactory with OneInstancePerTest { 14 | 15 | "A KsqlResultSetMetaData" when { 16 | 17 | "validating specs" should { 18 | 19 | val resultSet = new KsqlResultSetMetaData( 20 | List( 21 | HeaderField("field1", Types.INTEGER, 8), 22 | HeaderField("field2", Types.BIGINT, 16), 23 | HeaderField("field3", Types.DOUBLE, 32), 24 | HeaderField("field4", Types.DECIMAL, 32), 25 | HeaderField("field5", Types.BOOLEAN, 5), 26 | HeaderField("field6", Types.VARCHAR, 128), 27 | HeaderField("field7", Types.JAVA_OBJECT, 255), 28 | HeaderField("field8", Types.ARRAY, 255), 29 | HeaderField("field9", Types.STRUCT, 512), 30 | HeaderField("field10", -999, 9) 31 | )) 32 | 33 | "throw not supported exception if not supported" in { 34 | 35 | val methods = implementedMethods[KsqlResultSetMetaData] 36 | reflectMethods[KsqlResultSetMetaData](methods, implemented = false, resultSet) 37 | .foreach(method => { 38 | assertThrows[SQLFeatureNotSupportedException] { 39 | method() 40 | } 41 | }) 42 | } 43 | 44 | "work if implemented" in { 45 | 46 | resultSet.getColumnLabel(3) should be("FIELD3") 47 | resultSet.getColumnName(3) should be("field3") 48 | resultSet.getColumnTypeName(3) should be("DOUBLE") 49 | 50 | resultSet.getColumnClassName(1) should be("java.lang.Integer") 51 | resultSet.getColumnType(1) should be(java.sql.Types.INTEGER) 52 | resultSet.getColumnTypeName(1) should be(KsqlType.INTEGER.name) 53 | resultSet.getColumnDisplaySize(1) should be(8) 54 | 55 | resultSet.getColumnClassName(2) should be("java.lang.Long") 56 | resultSet.getColumnType(2) should be(java.sql.Types.BIGINT) 57 | resultSet.getColumnTypeName(2) should be(KsqlType.BIGINT.name) 58 | resultSet.getColumnDisplaySize(2) should be(16) 59 | 60 | resultSet.getColumnClassName(3) should be("java.lang.Double") 61 | resultSet.getColumnType(3) should be(java.sql.Types.DOUBLE) 62 | resultSet.getColumnTypeName(3) should be(KsqlType.DOUBLE.name) 63 | resultSet.getColumnDisplaySize(3) should be(32) 64 | 65 | resultSet.getColumnClassName(4) should be("java.math.BigDecimal") 66 | resultSet.getColumnType(4) should be(java.sql.Types.DECIMAL) 67 | resultSet.getColumnTypeName(4) should be(KsqlType.DECIMAL.name) 68 | resultSet.getColumnDisplaySize(4) should be(32) 69 | 70 | resultSet.getColumnClassName(5) should be("java.lang.Boolean") 71 | resultSet.getColumnType(5) should be(java.sql.Types.BOOLEAN) 72 | resultSet.getColumnTypeName(5) should be(KsqlType.BOOLEAN.name) 73 | resultSet.getColumnDisplaySize(5) should be(5) 74 | 75 | resultSet.getColumnClassName(6) should be("java.lang.String") 76 | resultSet.getColumnType(6) should be(java.sql.Types.VARCHAR) 77 | resultSet.getColumnTypeName(6) should be(KsqlType.STRING.name) 78 | resultSet.getColumnDisplaySize(6) should be(128) 79 | 80 | resultSet.getColumnClassName(7) should be("java.util.Map") 81 | resultSet.getColumnType(7) should be(java.sql.Types.JAVA_OBJECT) 82 | resultSet.getColumnTypeName(7) should be(KsqlType.MAP.name) 83 | resultSet.getColumnDisplaySize(7) should be(255) 84 | 85 | resultSet.getColumnClassName(8) should be("java.sql.Array") 86 | resultSet.getColumnType(8) should be(java.sql.Types.ARRAY) 87 | resultSet.getColumnTypeName(8) should be(KsqlType.ARRAY.name) 88 | resultSet.getColumnDisplaySize(8) should be(255) 89 | 90 | resultSet.getColumnClassName(9) should be("java.sql.Struct") 91 | resultSet.getColumnType(9) should be(java.sql.Types.STRUCT) 92 | resultSet.getColumnTypeName(9) should be(KsqlType.STRUCT.name) 93 | resultSet.getColumnDisplaySize(9) should be(512) 94 | 95 | resultSet.getColumnClassName(10) should be("java.lang.String") 96 | resultSet.getColumnType(10) should be(-999) 97 | resultSet.getColumnTypeName(10) should be(KsqlType.STRING.name) 98 | resultSet.getColumnDisplaySize(10) should be(9) 99 | 100 | resultSet.getColumnType(3) should be(java.sql.Types.DOUBLE) 101 | resultSet.getColumnCount should be(10) 102 | resultSet.getPrecision(3) should be(-1) 103 | resultSet.getPrecision(2) should be(0) 104 | resultSet.getScale(3) should be(-1) 105 | resultSet.getScale(4) should be(0) 106 | 107 | resultSet.isCaseSensitive(2) should be(false) 108 | resultSet.isCaseSensitive(6) should be(true) 109 | resultSet.isNullable(1) should be(ResultSetMetaData.columnNullableUnknown) 110 | resultSet.isCurrency(6) should be(false) 111 | resultSet.isAutoIncrement(6) should be(false) 112 | resultSet.isSearchable(6) should be(true) 113 | resultSet.isReadOnly(6) should be(false) 114 | resultSet.isWritable(6) should be(true) 115 | resultSet.isDefinitelyWritable(6) should be(true) 116 | resultSet.isSigned(2) should be(true) 117 | resultSet.isSigned(6) should be(false) 118 | } 119 | } 120 | } 121 | 122 | "A ResultSetMetaDataNotSupported" when { 123 | 124 | "validating specs" should { 125 | 126 | "throw not supported exception if not supported" in { 127 | 128 | val resultSet = new ResultSetMetaDataNotSupported 129 | reflectMethods[ResultSetMetaDataNotSupported](Seq.empty, implemented = false, resultSet) 130 | .foreach(method => { 131 | assertThrows[SQLFeatureNotSupportedException] { 132 | method() 133 | } 134 | }) 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/utils/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.utils 2 | 3 | import java.io.{File, FileNotFoundException, IOException} 4 | import java.lang.reflect.InvocationTargetException 5 | import java.net.{InetSocketAddress, ServerSocket} 6 | import java.nio.channels.ServerSocketChannel 7 | import java.util 8 | import java.util.{Properties, Random, UUID} 9 | 10 | import io.confluent.ksql.rest.client.QueryStream 11 | import javax.ws.rs.core.Response 12 | import kafka.utils.Logging 13 | import kafka.zk.KafkaZkClient 14 | import org.apache.kafka.clients.admin.{AdminClient, AdminClientConfig} 15 | import org.apache.kafka.clients.consumer.{ConsumerConfig, KafkaConsumer} 16 | import org.apache.kafka.clients.producer.{KafkaProducer, ProducerConfig} 17 | import org.apache.kafka.common.serialization.{ByteArrayDeserializer, ByteArraySerializer} 18 | import org.apache.kafka.common.utils.Time 19 | 20 | import scala.reflect.runtime.universe._ 21 | import scala.reflect.{ClassTag, _} 22 | 23 | object TestUtils extends Logging { 24 | 25 | private val RANDOM: Random = new Random 26 | 27 | def makeTempDir(dirPrefix: String): File = { 28 | val file: File = new File(System.getProperty("java.io.tmpdir"), dirPrefix + RANDOM.nextInt(10000000)) 29 | if (!file.mkdirs) throw new RuntimeException("could not create temp directory: " + file.getAbsolutePath) 30 | file.deleteOnExit() 31 | file 32 | } 33 | 34 | def getAvailablePort: Int = { 35 | var socket: ServerSocket = null 36 | try { 37 | socket = new ServerSocket(0) 38 | socket.getLocalPort 39 | } catch { 40 | case e: IOException => throw new IllegalStateException("Cannot find available port: " + e.getMessage, e) 41 | } 42 | finally socket.close() 43 | } 44 | 45 | def waitTillAvailable(host: String, port: Int, maxWaitMs: Int): Unit = { 46 | val defaultWait: Int = 100 47 | var currentWait: Int = 0 48 | try 49 | while (isPortAvailable(host, port) && currentWait < maxWaitMs) { 50 | Thread.sleep(defaultWait) 51 | currentWait += defaultWait 52 | } 53 | catch { 54 | case ie: InterruptedException => throw new RuntimeException(ie) 55 | } 56 | } 57 | 58 | def isPortAvailable(host: String, port: Int): Boolean = { 59 | var ss: ServerSocketChannel = null 60 | try { 61 | ss = ServerSocketChannel.open 62 | ss.socket.setReuseAddress(false) 63 | ss.socket.bind(new InetSocketAddress(host, port)) 64 | true 65 | } catch { 66 | case _: IOException => false 67 | } 68 | finally if (Option(ss).isDefined) ss.close() 69 | } 70 | 71 | def buildProducer(brokerList: String, compression: String = "none"): KafkaProducer[Array[Byte], Array[Byte]] = { 72 | val props = new Properties 73 | props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList) 74 | props.put(ProducerConfig.ACKS_CONFIG, "all") 75 | props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compression) 76 | props.put(ProducerConfig.LINGER_MS_CONFIG, "0") //ensure writes are synchronous 77 | props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, Long.MaxValue.toString) 78 | props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer") 79 | props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer") 80 | 81 | new KafkaProducer(props, new ByteArraySerializer, new ByteArraySerializer) 82 | } 83 | 84 | def buildConsumer(brokerList: String, groupId: String = "test-group"): KafkaConsumer[Array[Byte], Array[Byte]] = { 85 | val props = new Properties 86 | props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList) 87 | props.put(ConsumerConfig.CLIENT_ID_CONFIG, "test-client-" + UUID.randomUUID) 88 | props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId) 89 | props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true") 90 | props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") 91 | props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer") 92 | props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer") 93 | props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "0") //ensure we have no temporal batching 94 | 95 | new KafkaConsumer(props, new ByteArrayDeserializer, new ByteArrayDeserializer) 96 | } 97 | 98 | def buildAdminClient(brokerList: String): AdminClient = { 99 | val config = new util.HashMap[String, Object] 100 | config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, brokerList) 101 | 102 | AdminClient.create(config) 103 | } 104 | 105 | def buildZkClient(zkConnection: String): KafkaZkClient = 106 | KafkaZkClient(connectString = zkConnection, isSecure = false, sessionTimeoutMs = 6000, 107 | connectionTimeoutMs = 10000, maxInFlightRequests = Int.MaxValue, time = Time.SYSTEM) 108 | 109 | @throws[FileNotFoundException] 110 | def deleteFile(path: File): Boolean = { 111 | if (!path.exists) throw new FileNotFoundException(path.getAbsolutePath) 112 | var ret: Boolean = true 113 | if (path.isDirectory) for (f <- path.listFiles) { 114 | ret = ret && deleteFile(f) 115 | } 116 | ret && path.delete 117 | } 118 | 119 | def randomString(length: Int = 10, numbers: Boolean = false): String = { 120 | val str = scala.util.Random.alphanumeric.take(length).mkString 121 | if (!numbers) str.replaceAll("[0-9]", "") else str 122 | } 123 | 124 | def swallow(action: => Unit) { 125 | try { 126 | action 127 | } catch { 128 | case e: Throwable => logger.warn(e.getMessage, e) 129 | } 130 | } 131 | 132 | def mockQueryStream(mockResponse: Response): QueryStream = { 133 | classOf[QueryStream].getDeclaredConstructors 134 | .filter(_.getParameterCount == 1) 135 | .map(c => { 136 | c.setAccessible(true) 137 | c 138 | }).head.newInstance(mockResponse).asInstanceOf[QueryStream] 139 | } 140 | 141 | def implementedMethods[T <: AnyRef](implicit ct: ClassTag[T]): Seq[String] = { 142 | ct.runtimeClass.getMethods.filter(_.getDeclaringClass == ct.runtimeClass).map(_.getName) 143 | } 144 | 145 | def reflectMethods[T <: AnyRef](methods: Seq[String], implemented: Boolean, obj: T) 146 | (implicit tt: TypeTag[T], ct: ClassTag[T]): Seq[() => Any] = { 147 | val ksqlPackage = "com.github.mmolimar.ksql" 148 | val declarations = for { 149 | baseClass <- typeTag.tpe.baseClasses 150 | if baseClass.fullName.startsWith(ksqlPackage) 151 | } yield baseClass.typeSignature.decls 152 | 153 | declarations.flatten 154 | .filter(_.overrides.nonEmpty) 155 | .filter(ms => methods.contains(ms.name.toString) == implemented) 156 | .map(_.asMethod) 157 | .filter(!_.isProtected) 158 | .map(m => { 159 | val args = new Array[AnyRef](if (m.paramLists.isEmpty) 0 else m.paramLists.head.size) 160 | if (m.paramLists.nonEmpty) 161 | for ((paramType, index) <- m.paramLists.head.zipWithIndex) { 162 | args(index) = paramType.info.typeSymbol match { 163 | case tof if tof == typeOf[Byte].typeSymbol => Byte.box(0) 164 | case tof if tof == typeOf[Boolean].typeSymbol => Boolean.box(false) 165 | case tof if tof == typeOf[Short].typeSymbol => Short.box(0) 166 | case tof if tof == typeOf[Int].typeSymbol => Int.box(0) 167 | case tof if tof == typeOf[Double].typeSymbol => Double.box(0) 168 | case tof if tof == typeOf[Long].typeSymbol => Long.box(0) 169 | case tof if tof == typeOf[Float].typeSymbol => Float.box(0) 170 | case tof if tof == typeOf[String].typeSymbol => "" 171 | case _ => null 172 | } 173 | } 174 | 175 | val mirror = runtimeMirror(classTag[T].runtimeClass.getClassLoader).reflect(obj) 176 | val method = mirror.reflectMethod(m) 177 | () => 178 | try { 179 | method(args: _*) 180 | } catch { 181 | case t: InvocationTargetException => throw t.getCause 182 | } 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/KsqlDriverSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.{SQLException, SQLFeatureNotSupportedException} 4 | import java.util.Properties 5 | 6 | import io.confluent.ksql.rest.client.{KsqlRestClient, MockableKsqlRestClient, RestResponse} 7 | import io.confluent.ksql.rest.entity.{KsqlErrorMessage, ServerInfo} 8 | import org.eclipse.jetty.http.HttpStatus.Code 9 | import org.scalamock.scalatest.MockFactory 10 | import org.scalatest.matchers.should.Matchers 11 | import org.scalatest.wordspec.AnyWordSpec 12 | 13 | import scala.collection.JavaConverters._ 14 | 15 | class KsqlDriverSpec extends AnyWordSpec with Matchers with MockFactory { 16 | 17 | "A KsqlDriver" when { 18 | val driver = new KsqlDriver 19 | 20 | "validating specs" should { 21 | "not be JDBC compliant" in { 22 | driver.jdbcCompliant should be(false) 23 | } 24 | "have a major and minor version" in { 25 | driver.getMinorVersion should be(2) 26 | driver.getMajorVersion should be(1) 27 | } 28 | "have no properties" in { 29 | driver.getPropertyInfo("", new Properties).length should be(0) 30 | } 31 | "throw an exception when getting parent logger" in { 32 | assertThrows[SQLFeatureNotSupportedException] { 33 | driver.getParentLogger 34 | } 35 | } 36 | "throw an exception when connecting to an invalid URL" in { 37 | assertThrows[SQLException] { 38 | driver.connect("invalid", new Properties) 39 | } 40 | assertThrows[SQLException] { 41 | driver.connect("jdbc:ksql://localhost:9999999", new Properties) 42 | } 43 | } 44 | } 45 | 46 | "connecting to an URL" should { 47 | val mockKsqlRestClient = mock[MockableKsqlRestClient] 48 | val driver = new KsqlDriver { 49 | override private[jdbc] def buildConnection(values: KsqlConnectionValues, properties: Properties) = { 50 | new KsqlConnection(values, new Properties) { 51 | override def init: KsqlRestClient = mockKsqlRestClient 52 | } 53 | } 54 | } 55 | "throw an exception if cannot connect to the URL" in { 56 | assertThrows[SQLException] { 57 | (mockKsqlRestClient.getServerInfo _).expects() 58 | .throws(new Exception("error")) 59 | .once 60 | driver.connect("jdbc:ksql://localhost:9999", new Properties) 61 | } 62 | } 63 | "throw an exception if there is an error in the response" in { 64 | assertThrows[SQLException] { 65 | (mockKsqlRestClient.getServerInfo _).expects() 66 | .returns(RestResponse.erroneous(Code.INTERNAL_SERVER_ERROR, new KsqlErrorMessage(-1, "error message", List.empty.asJava))) 67 | .once 68 | driver.connect("jdbc:ksql://localhost:9999", new Properties) 69 | } 70 | } 71 | "connect properly if the response is successful" in { 72 | (mockKsqlRestClient.getServerInfo _).expects() 73 | .returns(RestResponse.successful[ServerInfo](Code.OK, new ServerInfo("v1", "id1", "svc1"))) 74 | .once 75 | val connection = driver.connect("jdbc:ksql://localhost:9999", new Properties) 76 | connection.isClosed should be(false) 77 | } 78 | } 79 | 80 | "accepting an URL" should { 81 | "return false if invalid" in { 82 | driver.acceptsURL(null) should be(false) 83 | driver.acceptsURL("") should be(false) 84 | driver.acceptsURL("jdbc:invalid://ksql-server:8080") should be(false) 85 | } 86 | "return true if valid" in { 87 | driver.acceptsURL("jdbc:ksql://ksql-server:8080") should be(true) 88 | driver.acceptsURL("jdbc:ksql://") should be(true) 89 | } 90 | } 91 | 92 | "parsing an URL" should { 93 | "throw an SQLException if invalid" in { 94 | assertThrows[SQLException] { 95 | KsqlDriver.parseUrl(null) 96 | } 97 | assertThrows[SQLException] { 98 | KsqlDriver.parseUrl("") 99 | } 100 | assertThrows[SQLException] { 101 | KsqlDriver.parseUrl("jdbc:invalid://ksql-server:8080") 102 | } 103 | assertThrows[SQLException] { 104 | KsqlDriver.parseUrl("jdbc:ksql://user@ksql-server:8080") 105 | } 106 | } 107 | "return the URL parsed properly" in { 108 | val ksqlUserPass = "usr:pass" 109 | val ksqlServer = "ksql-server" 110 | val ksqlPort = 8080 111 | val ksqlUrl = s"http://$ksqlServer:$ksqlPort" 112 | val ksqlUrlSecured = s"https://$ksqlServer:$ksqlPort" 113 | 114 | var url = s"jdbc:ksql://$ksqlServer:$ksqlPort" 115 | var connectionValues = KsqlDriver.parseUrl(url) 116 | connectionValues.ksqlServer should be(ksqlServer) 117 | connectionValues.port should be(ksqlPort) 118 | connectionValues.config.isEmpty should be(true) 119 | connectionValues.ksqlUrl should be(ksqlUrl) 120 | connectionValues.jdbcUrl should be(url) 121 | connectionValues.isSecured should be(false) 122 | connectionValues.properties should be(false) 123 | connectionValues.timeout should be(0) 124 | connectionValues.username should be(None) 125 | connectionValues.password should be(None) 126 | 127 | url = s"jdbc:ksql://$ksqlUserPass@$ksqlServer:$ksqlPort" 128 | connectionValues = KsqlDriver.parseUrl(url) 129 | connectionValues.ksqlServer should be(ksqlServer) 130 | connectionValues.port should be(ksqlPort) 131 | connectionValues.config.isEmpty should be(true) 132 | connectionValues.ksqlUrl should be(ksqlUrl) 133 | connectionValues.jdbcUrl should be(url) 134 | connectionValues.isSecured should be(false) 135 | connectionValues.properties should be(false) 136 | connectionValues.timeout should be(0) 137 | connectionValues.username should be(Some("usr")) 138 | connectionValues.password should be(Some("pass")) 139 | 140 | url = s"jdbc:ksql://$ksqlServer:$ksqlPort?prop1=value1" 141 | connectionValues = KsqlDriver.parseUrl(url) 142 | connectionValues.ksqlServer should be(ksqlServer) 143 | connectionValues.port should be(ksqlPort) 144 | connectionValues.config.size should be(1) 145 | connectionValues.config("prop1") should be("value1") 146 | connectionValues.ksqlUrl should be(ksqlUrl) 147 | connectionValues.jdbcUrl should be(url) 148 | connectionValues.isSecured should be(false) 149 | connectionValues.properties should be(false) 150 | connectionValues.timeout should be(0) 151 | connectionValues.username should be(None) 152 | connectionValues.password should be(None) 153 | 154 | url = s"jdbc:ksql://$ksqlUserPass@$ksqlServer:$ksqlPort?prop1=value1&secured=true&prop2=value2" 155 | connectionValues = KsqlDriver.parseUrl(url) 156 | connectionValues.ksqlServer should be(ksqlServer) 157 | connectionValues.port should be(ksqlPort) 158 | connectionValues.config.size should be(3) 159 | connectionValues.config("prop1") should be("value1") 160 | connectionValues.config("prop2") should be("value2") 161 | connectionValues.config("secured") should be("true") 162 | connectionValues.ksqlUrl should be(ksqlUrlSecured) 163 | connectionValues.jdbcUrl should be(url) 164 | connectionValues.isSecured should be(true) 165 | connectionValues.properties should be(false) 166 | connectionValues.timeout should be(0) 167 | connectionValues.username should be(Some("usr")) 168 | connectionValues.password should be(Some("pass")) 169 | 170 | url = s"jdbc:ksql://$ksqlServer:$ksqlPort?prop1=value1&timeout=100&prop2=value2" 171 | connectionValues = KsqlDriver.parseUrl(url) 172 | connectionValues.ksqlServer should be(ksqlServer) 173 | connectionValues.port should be(ksqlPort) 174 | connectionValues.config.size should be(3) 175 | connectionValues.config("prop1") should be("value1") 176 | connectionValues.config("prop2") should be("value2") 177 | connectionValues.config("timeout") should be("100") 178 | connectionValues.ksqlUrl should be(ksqlUrl) 179 | connectionValues.jdbcUrl should be(url) 180 | connectionValues.isSecured should be(false) 181 | connectionValues.properties should be(false) 182 | connectionValues.timeout should be(100) 183 | connectionValues.username should be(None) 184 | connectionValues.password should be(None) 185 | 186 | url = s"jdbc:ksql://$ksqlServer:$ksqlPort?prop1=value1&properties=true&prop2=value2" 187 | connectionValues = KsqlDriver.parseUrl(url) 188 | connectionValues.ksqlServer should be(ksqlServer) 189 | connectionValues.port should be(ksqlPort) 190 | connectionValues.config.size should be(3) 191 | connectionValues.config("prop1") should be("value1") 192 | connectionValues.config("prop2") should be("value2") 193 | connectionValues.config("properties") should be("true") 194 | connectionValues.ksqlUrl should be(ksqlUrl) 195 | connectionValues.jdbcUrl should be(url) 196 | connectionValues.isSecured should be(false) 197 | connectionValues.properties should be(true) 198 | connectionValues.timeout should be(0) 199 | connectionValues.username should be(None) 200 | connectionValues.password should be(None) 201 | 202 | url = s"jdbc:ksql://$ksqlUserPass@$ksqlServer:$ksqlPort?timeout=100&secured=true&properties=true&prop1=value1" 203 | connectionValues = KsqlDriver.parseUrl(url) 204 | connectionValues.ksqlServer should be(ksqlServer) 205 | connectionValues.port should be(ksqlPort) 206 | connectionValues.config.size should be(4) 207 | connectionValues.config("prop1") should be("value1") 208 | connectionValues.config("timeout") should be("100") 209 | connectionValues.config("secured") should be("true") 210 | connectionValues.config("properties") should be("true") 211 | connectionValues.ksqlUrl should be(ksqlUrlSecured) 212 | connectionValues.jdbcUrl should be(url) 213 | connectionValues.isSecured should be(true) 214 | connectionValues.properties should be(true) 215 | connectionValues.timeout should be(100) 216 | connectionValues.username should be(Some("usr")) 217 | connectionValues.password should be(Some("pass")) 218 | } 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/KsqlConnection.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql._ 4 | import java.util 5 | import java.util.concurrent.Executor 6 | import java.util.{Collections, Optional, Properties} 7 | 8 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 9 | import io.confluent.ksql.rest.client.{BasicCredentials, KsqlRestClient, RestResponse} 10 | import io.confluent.ksql.rest.entity.KsqlEntityList 11 | 12 | import scala.collection.JavaConverters._ 13 | import scala.util.{Failure, Success, Try} 14 | 15 | case class KsqlConnectionValues(ksqlServer: String, 16 | port: Int, 17 | username: Option[String], 18 | password: Option[String], 19 | config: Map[String, String]) { 20 | 21 | def ksqlUrl: String = { 22 | val protocol = if (isSecured) "https://" else "http://" 23 | protocol + ksqlServer + ":" + port 24 | } 25 | 26 | def jdbcUrl: String = { 27 | val ksqlUserPass = username.flatMap(usr => password.map(pass => s"$usr:$pass@")).getOrElse("") 28 | val suffix = if (config.isEmpty) "" else "?" 29 | s"${KsqlDriver.ksqlPrefix}$ksqlUserPass$ksqlServer:$port$suffix${ 30 | config.map(c => s"${c._1}=${c._2}").mkString("&") 31 | }" 32 | } 33 | 34 | def isSecured: Boolean = config.getOrElse("secured", "false").toBoolean 35 | 36 | def properties: Boolean = config.getOrElse("properties", "false").toBoolean 37 | 38 | def timeout: Long = config.getOrElse("timeout", "0").toLong 39 | 40 | } 41 | 42 | class ConnectionNotSupported extends Connection with WrapperNotSupported { 43 | 44 | override def commit(): Unit = throw NotSupported("commit") 45 | 46 | override def getHoldability: Int = throw NotSupported("getHoldability") 47 | 48 | override def setCatalog(catalog: String): Unit = throw NotSupported("setCatalog") 49 | 50 | override def setHoldability(holdability: Int): Unit = throw NotSupported("setHoldability") 51 | 52 | override def prepareStatement(sql: String): PreparedStatement = throw NotSupported("prepareStatement") 53 | 54 | override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int): PreparedStatement = 55 | throw NotSupported("prepareStatement") 56 | 57 | override def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int, 58 | resultSetHoldability: Int): PreparedStatement = throw NotSupported("prepareStatement") 59 | 60 | override def prepareStatement(sql: String, autoGeneratedKeys: Int): PreparedStatement = 61 | throw NotSupported("prepareStatement") 62 | 63 | override def prepareStatement(sql: String, columnIndexes: scala.Array[Int]): PreparedStatement = 64 | throw NotSupported("prepareStatement") 65 | 66 | override def prepareStatement(sql: String, columnNames: scala.Array[String]): PreparedStatement = 67 | throw NotSupported("prepareStatement") 68 | 69 | override def createClob: Clob = throw NotSupported("createClob") 70 | 71 | override def setSchema(schema: String): Unit = throw NotSupported("setSchema") 72 | 73 | override def setClientInfo(name: String, value: String): Unit = throw NotSupported("setClientInfo") 74 | 75 | override def setClientInfo(properties: Properties): Unit = throw NotSupported("setClientInfo") 76 | 77 | override def createSQLXML: SQLXML = throw NotSupported("createSQLXML") 78 | 79 | override def getCatalog: String = throw NotSupported("getCatalog") 80 | 81 | override def createBlob: Blob = throw NotSupported("createBlob") 82 | 83 | override def createStatement: Statement = throw NotSupported("createStatement") 84 | 85 | override def createStatement(resultSetType: Int, resultSetConcurrency: Int): Statement = 86 | throw NotSupported("createStatement") 87 | 88 | override def createStatement(resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): Statement = 89 | throw NotSupported("createStatement") 90 | 91 | override def abort(executor: Executor): Unit = throw NotSupported("abort") 92 | 93 | override def setAutoCommit(autoCommit: Boolean): Unit = throw NotSupported("setAutoCommit") 94 | 95 | override def getMetaData: DatabaseMetaData = throw NotSupported("getMetaData") 96 | 97 | override def setReadOnly(readOnly: Boolean): Unit = throw NotSupported("setReadOnly") 98 | 99 | override def prepareCall(sql: String): CallableStatement = throw NotSupported("prepareCall") 100 | 101 | override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int): CallableStatement = 102 | throw NotSupported("prepareCall") 103 | 104 | override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int, 105 | resultSetHoldability: Int): CallableStatement = throw NotSupported("prepareCall") 106 | 107 | override def setTransactionIsolation(level: Int): Unit = throw NotSupported("setTransactionIsolation") 108 | 109 | override def getWarnings: SQLWarning = throw NotSupported("getWarnings") 110 | 111 | override def releaseSavepoint(savepoint: Savepoint): Unit = throw NotSupported("releaseSavepoint") 112 | 113 | override def nativeSQL(sql: String): String = throw NotSupported("nativeSQL") 114 | 115 | override def isReadOnly: Boolean = throw NotSupported("isReadOnly") 116 | 117 | override def createArrayOf(typeName: String, elements: scala.Array[AnyRef]): Array = 118 | throw NotSupported("createArrayOf") 119 | 120 | override def setSavepoint(): Savepoint = throw NotSupported("setSavepoint") 121 | 122 | override def setSavepoint(name: String): Savepoint = throw NotSupported("setSavepoint") 123 | 124 | override def close(): Unit = throw NotSupported("close") 125 | 126 | override def createNClob: NClob = throw NotSupported("createNClob") 127 | 128 | override def rollback(): Unit = throw NotSupported("rollback") 129 | 130 | override def rollback(savepoint: Savepoint): Unit = throw NotSupported("rollback") 131 | 132 | override def setNetworkTimeout(executor: Executor, milliseconds: Int): Unit = throw NotSupported("setNetworkTimeout") 133 | 134 | override def setTypeMap(map: util.Map[String, Class[_]]): Unit = throw NotSupported("setTypeMap") 135 | 136 | override def isValid(timeout: Int): Boolean = throw NotSupported("isValid") 137 | 138 | override def getAutoCommit: Boolean = throw NotSupported("getAutoCommit") 139 | 140 | override def clearWarnings(): Unit = throw NotSupported("clearWarnings") 141 | 142 | override def getSchema: String = throw NotSupported("getSchema") 143 | 144 | override def getNetworkTimeout: Int = throw NotSupported("getNetworkTimeout") 145 | 146 | override def isClosed: Boolean = throw NotSupported("isClosed") 147 | 148 | override def getTransactionIsolation: Int = throw NotSupported("getTransactionIsolation") 149 | 150 | override def createStruct(typeName: String, attributes: scala.Array[AnyRef]): Struct = throw NotSupported("createStruct") 151 | 152 | override def getClientInfo(name: String): String = throw NotSupported("getClientInfo") 153 | 154 | override def getClientInfo: Properties = throw NotSupported("getClientInfo") 155 | 156 | override def getTypeMap: util.Map[String, Class[_]] = throw NotSupported("getTypeMap") 157 | 158 | } 159 | 160 | class KsqlConnection(private[jdbc] val values: KsqlConnectionValues, properties: Properties) 161 | extends ConnectionNotSupported { 162 | 163 | private val ksqlClient = init 164 | private var connected: Option[Boolean] = None 165 | 166 | private[jdbc] def init: KsqlRestClient = { 167 | val (localProps, clientProps) = if (values.properties) { 168 | val local = properties.asScala.toMap[String, AnyRef].filterNot(_._1.toLowerCase.startsWith("ssl.")).asJava 169 | val client = properties.asScala.toMap[String, String].filter(_._1.toLowerCase.startsWith("ssl.")).asJava 170 | (local, client) 171 | } else { 172 | (Collections.emptyMap[String, AnyRef], Collections.emptyMap[String, String]) 173 | } 174 | val credentials = values.username 175 | .flatMap(user => values.password.map(pass => Optional.of(BasicCredentials.of(user, pass)))) 176 | .getOrElse(Optional.empty[BasicCredentials]) 177 | KsqlRestClient.create(values.ksqlUrl, localProps, clientProps, credentials) 178 | } 179 | 180 | private[jdbc] def validate(): Unit = { 181 | Try(ksqlClient.getServerInfo) match { 182 | case Success(response) if response.isErroneous => 183 | throw CannotConnect(values.ksqlServer, response.getErrorMessage.getMessage) 184 | case Failure(e) => throw CannotConnect(values.ksqlServer, e.getMessage) 185 | case _ => connected = Some(true) 186 | } 187 | } 188 | 189 | private[jdbc] def executeKsqlCommand(ksql: String): RestResponse[KsqlEntityList] = ksqlClient.makeKsqlRequest(ksql) 190 | 191 | override def setAutoCommit(autoCommit: Boolean): Unit = {} 192 | 193 | override def getTransactionIsolation: Int = Connection.TRANSACTION_NONE 194 | 195 | override def getMetaData: DatabaseMetaData = new KsqlDatabaseMetaData(this) 196 | 197 | override def createStatement: Statement = createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) 198 | 199 | override def createStatement(resultSetType: Int, resultSetConcurrency: Int): Statement = { 200 | createStatement(resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT) 201 | } 202 | 203 | override def createStatement(resultSetType: Int, resultSetConcurrency: Int, resultSetHoldability: Int): Statement = { 204 | if (resultSetType != ResultSet.TYPE_FORWARD_ONLY || 205 | resultSetConcurrency != ResultSet.CONCUR_READ_ONLY || 206 | resultSetHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) { 207 | throw NotSupported("ResultSetType, ResultSetConcurrency and ResultSetHoldability must be" + 208 | " TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, HOLD_CURSORS_OVER_COMMIT respectively.") 209 | } 210 | new KsqlStatement(ksqlClient, values.timeout) 211 | } 212 | 213 | override def setClientInfo(name: String, value: String): Unit = { 214 | val ksql = s"SET '${name.trim}'='${value.trim}';" 215 | if (ksqlClient.makeKsqlRequest(ksql).isErroneous) { 216 | throw InvalidProperty(name) 217 | } 218 | } 219 | 220 | override def setClientInfo(properties: Properties): Unit = { 221 | properties.asScala.foreach(entry => setClientInfo(entry._1, entry._2)) 222 | } 223 | 224 | override def isReadOnly: Boolean = false 225 | 226 | override def getCatalog: String = None.orNull 227 | 228 | override def setCatalog(catalog: String): Unit = {} 229 | 230 | override def close(): Unit = { 231 | ksqlClient.close() 232 | connected = Some(false) 233 | } 234 | 235 | override def getAutoCommit: Boolean = false 236 | 237 | override def getSchema: String = None.orNull 238 | 239 | override def isValid(timeout: Int): Boolean = ksqlClient.makeStatusRequest.isSuccessful 240 | 241 | override def isClosed: Boolean = !connected.getOrElse(throw NotConnected(values.jdbcUrl)) 242 | 243 | override def getWarnings: SQLWarning = None.orNull 244 | 245 | override def commit(): Unit = {} 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/resultset/KsqlResultSetSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.io.InputStream 4 | import java.sql._ 5 | import java.util.NoSuchElementException 6 | 7 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils._ 8 | import com.github.mmolimar.ksql.jdbc.{DatabaseMetadataHeaders, HeaderField, TableTypes} 9 | import io.confluent.ksql.GenericRow 10 | import io.confluent.ksql.rest.entity.StreamedRow 11 | import org.scalamock.scalatest.MockFactory 12 | import org.scalatest.OneInstancePerTest 13 | import org.scalatest.matchers.should.Matchers 14 | import org.scalatest.wordspec.AnyWordSpec 15 | 16 | import scala.collection.JavaConverters._ 17 | 18 | 19 | class KsqlResultSetSpec extends AnyWordSpec with Matchers with MockFactory with OneInstancePerTest { 20 | 21 | "A IteratorResultSet" when { 22 | 23 | "validating specs" should { 24 | 25 | "throw not supported exception if not supported" in { 26 | 27 | val resultSet = new IteratorResultSet[String](List.empty[HeaderField], 0, Iterator.empty) 28 | val methods = implementedMethods[IteratorResultSet[String]] ++ implementedMethods[AbstractResultSet[String]] 29 | reflectMethods[IteratorResultSet[String]](methods, implemented = false, resultSet) 30 | .foreach(method => { 31 | assertThrows[SQLFeatureNotSupportedException] { 32 | method() 33 | } 34 | }) 35 | } 36 | 37 | "work if implemented" in { 38 | 39 | val resultSet = new IteratorResultSet(DatabaseMetadataHeaders.tableTypes, 2, Iterator(Seq(TableTypes.TABLE.name), 40 | Seq(TableTypes.STREAM.name))) 41 | 42 | resultSet.wasNull should be(true) 43 | resultSet.next should be(true) 44 | 45 | resultSet.getString(1) should be(TableTypes.TABLE.name) 46 | resultSet.getString("TABLE_TYPE") should be(TableTypes.TABLE.name) 47 | resultSet.getString("table_type") should be(TableTypes.TABLE.name) 48 | resultSet.next should be(true) 49 | resultSet.getString(1) should be(TableTypes.STREAM.name) 50 | resultSet.getString("TABLE_TYPE") should be(TableTypes.STREAM.name) 51 | resultSet.getString("table_type") should be(TableTypes.STREAM.name) 52 | resultSet.wasNull should be(false) 53 | assertThrows[SQLException] { 54 | resultSet.getString("UNKNOWN") 55 | } 56 | resultSet.next should be(false) 57 | resultSet.getWarnings should be(None.orNull) 58 | resultSet.close() 59 | } 60 | } 61 | } 62 | 63 | "A StreamedResultSet" when { 64 | 65 | "validating specs" should { 66 | 67 | val resultSetMetadata = new KsqlResultSetMetaData( 68 | List( 69 | HeaderField("field1", Types.INTEGER, 16), 70 | HeaderField("field2", Types.BIGINT, 16), 71 | HeaderField("field3", Types.DOUBLE, 16), 72 | HeaderField("field4", Types.BOOLEAN, 16), 73 | HeaderField("field5", Types.VARCHAR, 16), 74 | HeaderField("field6", Types.JAVA_OBJECT, 16), 75 | HeaderField("field7", Types.ARRAY, 16), 76 | HeaderField("field8", Types.STRUCT, 16), 77 | HeaderField("field9", -999, 16) 78 | )) 79 | 80 | "throw not supported exception if not supported" in { 81 | 82 | val resultSet = new StreamedResultSet(resultSetMetadata, mock[KsqlQueryStream], 0) 83 | val methods = implementedMethods[StreamedResultSet] ++ implementedMethods[AbstractResultSet[StreamedRow]] 84 | reflectMethods[StreamedResultSet](methods, implemented = false, resultSet) 85 | .foreach(method => { 86 | assertThrows[SQLFeatureNotSupportedException] { 87 | try { 88 | method() 89 | println("") 90 | } catch { 91 | case e: Throwable => throw e 92 | } 93 | } 94 | }) 95 | } 96 | 97 | "work when reading from a query stream" in { 98 | 99 | val mockedQueryStream = mock[KsqlQueryStream] 100 | inSequence { 101 | (mockedQueryStream.hasNext _).expects.returns(true) 102 | (mockedQueryStream.hasNext _).expects.returns(true) 103 | val columnValues = Seq[AnyRef](Int.box(1), Long.box(2L), Double.box(3.3d), Boolean.box(true), 104 | "1", Map.empty, scala.Array.empty, Map.empty, None.orNull) 105 | val row = StreamedRow.row(new GenericRow(columnValues.asJava)) 106 | (mockedQueryStream.next _).expects.returns(row) 107 | (mockedQueryStream.hasNext _).expects.returns(false) 108 | (mockedQueryStream.close _).expects 109 | } 110 | 111 | val resultSet = new StreamedResultSet(resultSetMetadata, mockedQueryStream, 0) 112 | resultSet.getMetaData should be(resultSetMetadata) 113 | resultSet.isLast should be(false) 114 | resultSet.isAfterLast should be(false) 115 | resultSet.isBeforeFirst should be(false) 116 | resultSet.getConcurrency should be(ResultSet.CONCUR_READ_ONLY) 117 | resultSet.wasNull should be(true) 118 | 119 | resultSet.isFirst should be(true) 120 | resultSet.next should be(true) 121 | 122 | // just to validate proper maps in data types 123 | val expected = Seq( 124 | Seq("1", scala.Array(1.byteValue), Boolean.box(true), Byte.box(1), 125 | Short.box(1), Int.box(1), Long.box(1L), Float.box(1.0f), Double.box(1.0d)), 126 | Seq("2", scala.Array(2L.byteValue), Boolean.box(true), Byte.box(2), 127 | Short.box(2), Int.box(2), Long.box(2L), Float.box(2.0f), Double.box(2.0d)), 128 | Seq("3.3", scala.Array(3L.byteValue), Boolean.box(true), Byte.box(3), 129 | Short.box(3), Int.box(3), Long.box(3L), Float.box(3.3f), Double.box(3.3d)), 130 | Seq("true", scala.Array(1.byteValue), Boolean.box(true), Byte.box(1), 131 | Short.box(1), Int.box(1), Long.box(1L), Float.box(1.0f), Double.box(1.0d)), 132 | Seq("1", "1".getBytes, Boolean.box(false), Byte.box(1), 133 | Short.box(1), Int.box(1), Long.box(1L), Float.box(1.0f), Double.box(1.0d)) 134 | ) 135 | expected.zipWithIndex.map { case (e, index) => 136 | resultSet.getString(index + 1) should be(e.head) 137 | resultSet.getBytes(index + 1) should be(e(1)) 138 | resultSet.getBoolean(index + 1) should be(e(2)) 139 | resultSet.getByte(index + 1) should be(e(3)) 140 | resultSet.getShort(index + 1) should be(e(4)) 141 | resultSet.getInt(index + 1) should be(e(5)) 142 | resultSet.getLong(index + 1) should be(e(6)) 143 | resultSet.getFloat(index + 1) should be(e(7)) 144 | resultSet.getDouble(index + 1) should be(e(8)) 145 | resultSet.wasNull should be(false) 146 | } 147 | resultSet.getObject(1) should be(Int.box(1)) 148 | resultSet.getObject(2) should be(Long.box(2L)) 149 | resultSet.getObject(3) should be(Double.box(3.3d)) 150 | resultSet.getObject(4) should be(Boolean.box(true)) 151 | resultSet.getObject(5) should be("1") 152 | resultSet.getObject(6) should be(Map.empty) 153 | resultSet.getObject(7) should be(scala.Array.empty) 154 | resultSet.getObject(8) should be(Map.empty) 155 | 156 | resultSet.getString(9) should be(None.orNull) 157 | resultSet.getBytes(9) should be(None.orNull) 158 | resultSet.getBoolean(9) should be(Boolean.box(false)) 159 | resultSet.getByte(9) should be(Byte.box(0)) 160 | resultSet.getShort(9) should be(Short.box(0)) 161 | resultSet.getInt(9) should be(Int.box(0)) 162 | resultSet.getLong(9) should be(Long.box(0L)) 163 | resultSet.getFloat(9) should be(Float.box(0.0f)) 164 | resultSet.getDouble(9) should be(Double.box(0.0d)) 165 | resultSet.getObject(9) should be(None.orNull) 166 | 167 | assertThrows[SQLException] { 168 | resultSet.getString(1000) 169 | } 170 | assertThrows[SQLException] { 171 | resultSet.getObject("UNKNOWN") 172 | } 173 | assertThrows[SQLException] { 174 | resultSet.getString("UNKNOWN") 175 | } 176 | assertThrows[SQLException] { 177 | resultSet.getBytes("UNKNOWN") 178 | } 179 | assertThrows[SQLException] { 180 | resultSet.getBoolean("UNKNOWN") 181 | } 182 | assertThrows[SQLException] { 183 | resultSet.getByte("UNKNOWN") 184 | } 185 | assertThrows[SQLException] { 186 | resultSet.getShort("UNKNOWN") 187 | } 188 | assertThrows[SQLException] { 189 | resultSet.getInt("UNKNOWN") 190 | } 191 | assertThrows[SQLException] { 192 | resultSet.getLong("UNKNOWN") 193 | } 194 | assertThrows[SQLException] { 195 | resultSet.getFloat("UNKNOWN") 196 | } 197 | assertThrows[SQLException] { 198 | resultSet.getDouble("UNKNOWN") 199 | } 200 | 201 | resultSet.next should be(false) 202 | resultSet.isFirst should be(false) 203 | resultSet.getWarnings should be(None.orNull) 204 | resultSet.close() 205 | resultSet.close() 206 | assertThrows[SQLException] { 207 | resultSet.next 208 | } 209 | } 210 | 211 | "work when reading from an input stream" in { 212 | val ksqlInputStream = new KsqlInputStream(new InputStream { 213 | override def read: Int = -1 214 | }) 215 | ksqlInputStream.hasNext should be(false) 216 | assertThrows[NoSuchElementException] { 217 | ksqlInputStream.next 218 | } 219 | ksqlInputStream.close() 220 | assertThrows[IllegalStateException] { 221 | ksqlInputStream.hasNext 222 | } 223 | assertThrows[IllegalStateException] { 224 | ksqlInputStream.next 225 | } 226 | 227 | val mockedInputStream = mock[KsqlInputStream] 228 | inSequence { 229 | (mockedInputStream.hasNext _).expects.returns(true) 230 | (mockedInputStream.hasNext _).expects.returns(true) 231 | val columnValues = Seq[AnyRef]("test") 232 | val row = StreamedRow.row(new GenericRow(columnValues.asJava)) 233 | (mockedInputStream.next _).expects.returns(row) 234 | (mockedInputStream.hasNext _).expects.returns(false) 235 | (mockedInputStream.close _).expects 236 | } 237 | 238 | val resultSet = new StreamedResultSet(resultSetMetadata, mockedInputStream, 0) 239 | resultSet.getMetaData should be(resultSetMetadata) 240 | resultSet.isLast should be(false) 241 | resultSet.isAfterLast should be(false) 242 | resultSet.isBeforeFirst should be(false) 243 | resultSet.getConcurrency should be(ResultSet.CONCUR_READ_ONLY) 244 | resultSet.wasNull should be(true) 245 | 246 | resultSet.isFirst should be(true) 247 | resultSet.next should be(true) 248 | 249 | resultSet.getString(1) should be("test") 250 | resultSet.next should be(false) 251 | resultSet.isFirst should be(false) 252 | resultSet.getWarnings should be(None.orNull) 253 | resultSet.close() 254 | resultSet.close() 255 | assertThrows[SQLException] { 256 | resultSet.next 257 | } 258 | } 259 | } 260 | } 261 | 262 | "A ResultSetNotSupported" when { 263 | 264 | "validating specs" should { 265 | 266 | "throw not supported exception if not supported" in { 267 | 268 | val resultSet = new ResultSetNotSupported 269 | reflectMethods[ResultSetNotSupported](Seq.empty, implemented = false, resultSet) 270 | .foreach(method => { 271 | assertThrows[SQLFeatureNotSupportedException] { 272 | method() 273 | } 274 | }) 275 | } 276 | } 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/Headers.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.Types 4 | 5 | import io.confluent.ksql.schema.ksql.{SqlBaseType => KsqlType} 6 | 7 | 8 | case class HeaderField(name: String, label: String, jdbcType: Int, length: Int, index: Int) 9 | 10 | object HeaderField { 11 | 12 | def apply(name: String, jdbcType: Int, length: Int): HeaderField = { 13 | HeaderField(name, name.toUpperCase, jdbcType, length, -1) 14 | } 15 | 16 | } 17 | 18 | object DatabaseMetadataHeaders { 19 | 20 | val tableTypes = List(HeaderField("TABLE_TYPE", Types.VARCHAR, 0)) 21 | 22 | val catalogs = List(HeaderField("TABLE_CAT", Types.VARCHAR, 0)) 23 | 24 | val schemas = List( 25 | HeaderField("TABLE_SCHEM", Types.VARCHAR, 0), 26 | HeaderField("TABLE_CATALOG", Types.VARCHAR, 0) 27 | ) 28 | 29 | val superTables = List( 30 | HeaderField("TABLE_CAT", Types.VARCHAR, 0), 31 | HeaderField("TABLE_SCHEM", Types.VARCHAR, 0), 32 | HeaderField("TABLE_NAME", Types.VARCHAR, 255), 33 | HeaderField("SUPERTABLE_NAME", Types.VARCHAR, 0) 34 | ) 35 | 36 | val tables = List( 37 | HeaderField("TABLE_CAT", java.sql.Types.VARCHAR, 0), 38 | HeaderField("TABLE_SCHEM", java.sql.Types.VARCHAR, 0), 39 | HeaderField("TABLE_NAME", java.sql.Types.VARCHAR, 255), 40 | HeaderField("TABLE_TYPE", java.sql.Types.VARCHAR, 8), 41 | HeaderField("REMARKS", java.sql.Types.VARCHAR, 0), 42 | HeaderField("TYPE_CAT", java.sql.Types.VARCHAR, 0), 43 | HeaderField("TYPE_SCHEM", java.sql.Types.VARCHAR, 0), 44 | HeaderField("TYPE_NAME", java.sql.Types.VARCHAR, 0), 45 | HeaderField("SELF_REFERENCING_COL_NAME", java.sql.Types.VARCHAR, 0), 46 | HeaderField("REF_GENERATION", java.sql.Types.VARCHAR, 0) 47 | ) 48 | 49 | val columns = List( 50 | HeaderField("TABLE_CAT", Types.VARCHAR, 0), 51 | HeaderField("TABLE_SCHEM", Types.VARCHAR, 0), 52 | HeaderField("TABLE_NAME", Types.VARCHAR, 255), 53 | HeaderField("COLUMN_NAME", Types.VARCHAR, 255), 54 | HeaderField("DATA_TYPE", Types.INTEGER, 5), 55 | HeaderField("TYPE_NAME", Types.VARCHAR, 16), 56 | HeaderField("COLUMN_SIZE", Types.INTEGER, Integer.toString(Integer.MAX_VALUE).length), 57 | HeaderField("BUFFER_LENGTH", Types.INTEGER, 10), 58 | HeaderField("DECIMAL_DIGITS", Types.INTEGER, 10), 59 | HeaderField("NUM_PREC_RADIX", Types.INTEGER, 10), 60 | HeaderField("NULLABLE", Types.INTEGER, 10), 61 | HeaderField("REMARKS", Types.VARCHAR, 0), 62 | HeaderField("COLUMN_DEF", Types.VARCHAR, 0), 63 | HeaderField("SQL_DATA_TYPE", Types.INTEGER, 10), 64 | HeaderField("SQL_DATETIME_SUB", Types.INTEGER, 10), 65 | HeaderField("CHAR_OCTET_LENGTH", Types.INTEGER, Integer.toString(Integer.MAX_VALUE).length), 66 | HeaderField("ORDINAL_POSITION", Types.INTEGER, 10), 67 | HeaderField("IS_NULLABLE", Types.VARCHAR, 3), 68 | HeaderField("SCOPE_CATALOG", Types.VARCHAR, 0), 69 | HeaderField("SCOPE_SCHEMA", Types.VARCHAR, 0), 70 | HeaderField("SCOPE_TABLE", Types.VARCHAR, 0), 71 | HeaderField("SOURCE_DATA_TYPE", Types.SMALLINT, 0), 72 | HeaderField("IS_AUTOINCREMENT", Types.VARCHAR, 3), 73 | HeaderField("IS_GENERATEDCOLUMN", Types.VARCHAR, 3) 74 | ) 75 | 76 | val procedures = List( 77 | HeaderField("PROCEDURE_CAT", Types.VARCHAR, 255), 78 | HeaderField("PROCEDURE_SCHEM", Types.VARCHAR, 255), 79 | HeaderField("PROCEDURE_NAME", Types.VARCHAR, 255), 80 | HeaderField("reserved1", Types.VARCHAR, 0), 81 | HeaderField("reserved2", Types.VARCHAR, 0), 82 | HeaderField("reserved3", Types.VARCHAR, 0), 83 | HeaderField("REMARKS", Types.VARCHAR, 255), 84 | HeaderField("PROCEDURE_TYPE", Types.SMALLINT, 6), 85 | HeaderField("SPECIFIC_NAME", Types.VARCHAR, 255) 86 | ) 87 | 88 | val typeInfo = List( 89 | HeaderField("TYPE_NAME", Types.VARCHAR, 32), 90 | HeaderField("DATA_TYPE", Types.INTEGER, 5), 91 | HeaderField("PRECISION", Types.INTEGER, 10), 92 | HeaderField("LITERAL_PREFIX", Types.VARCHAR, 4), 93 | HeaderField("LITERAL_SUFFIX", Types.VARCHAR, 4), 94 | HeaderField("CREATE_PARAMS", Types.VARCHAR, 32), 95 | HeaderField("NULLABLE", Types.SMALLINT, 5), 96 | HeaderField("CASE_SENSITIVE", Types.BOOLEAN, 3), 97 | HeaderField("SEARCHABLE", Types.SMALLINT, 3), 98 | HeaderField("UNSIGNED_ATTRIBUTE", Types.BOOLEAN, 3), 99 | HeaderField("FIXED_PREC_SCALE", Types.BOOLEAN, 3), 100 | HeaderField("AUTO_INCREMENT", Types.BOOLEAN, 3), 101 | HeaderField("LOCAL_TYPE_NAME", Types.VARCHAR, 32), 102 | HeaderField("MINIMUM_SCALE", Types.SMALLINT, 5), 103 | HeaderField("MAXIMUM_SCALE", Types.SMALLINT, 5), 104 | HeaderField("SQL_DATA_TYPE", Types.INTEGER, 10), 105 | HeaderField("SQL_DATETIME_SUB", Types.INTEGER, 10), 106 | HeaderField("NUM_PREC_RADIX", Types.INTEGER, 10) 107 | ) 108 | 109 | def mapDataType(ksqlType: KsqlType): Int = ksqlType match { 110 | case KsqlType.INTEGER => Types.INTEGER 111 | case KsqlType.BIGINT => Types.BIGINT 112 | case KsqlType.DOUBLE => Types.DOUBLE 113 | case KsqlType.DECIMAL => Types.DECIMAL 114 | case KsqlType.BOOLEAN => Types.BOOLEAN 115 | case KsqlType.STRING => Types.VARCHAR 116 | case KsqlType.ARRAY => Types.ARRAY 117 | case KsqlType.MAP => Types.JAVA_OBJECT 118 | case KsqlType.STRUCT => Types.STRUCT 119 | } 120 | 121 | } 122 | 123 | object KsqlEntityHeaders { 124 | 125 | val printTopic = List( 126 | HeaderField("PRINT_TOPIC", Types.VARCHAR, 255) 127 | ) 128 | 129 | val commandStatusEntity = List( 130 | HeaderField("COMMAND_STATUS_SEQ_NUMBER", Types.BIGINT, 16), 131 | HeaderField("COMMAND_STATUS_ID_TYPE", Types.VARCHAR, 16), 132 | HeaderField("COMMAND_STATUS_ID_ENTITY", Types.VARCHAR, 32), 133 | HeaderField("COMMAND_STATUS_ID_ACTION", Types.VARCHAR, 16), 134 | HeaderField("COMMAND_STATUS_STATUS", Types.VARCHAR, 16), 135 | HeaderField("COMMAND_STATUS_MESSAGE", Types.VARCHAR, 128) 136 | ) 137 | 138 | val sourceDescriptionEntity = List( 139 | HeaderField("SOURCE_DESCRIPTION_KEY", Types.VARCHAR, 16), 140 | HeaderField("SOURCE_DESCRIPTION_NAME", Types.VARCHAR, 16), 141 | HeaderField("SOURCE_DESCRIPTION_TYPE", Types.VARCHAR, 16), 142 | HeaderField("SOURCE_DESCRIPTION_TOPIC", Types.VARCHAR, 16), 143 | HeaderField("SOURCE_DESCRIPTION_FORMAT", Types.VARCHAR, 16), 144 | HeaderField("SOURCE_DESCRIPTION_FIELDS", Types.VARCHAR, 128), 145 | HeaderField("SOURCE_DESCRIPTION_PARTITIONS", Types.INTEGER, 8), 146 | HeaderField("SOURCE_DESCRIPTION_REPLICATION", Types.INTEGER, 8), 147 | HeaderField("SOURCE_DESCRIPTION_STATISTICS", Types.VARCHAR, 128), 148 | HeaderField("SOURCE_DESCRIPTION_ERROR_STATS", Types.VARCHAR, 128), 149 | HeaderField("SOURCE_DESCRIPTION_TIMESTAMP", Types.VARCHAR, 32) 150 | ) 151 | 152 | val connectorDescriptionEntity: List[HeaderField] = List( 153 | HeaderField("CONNECTOR_DESCRIPTION_CLASS", Types.VARCHAR, 128), 154 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_NAME", Types.VARCHAR, 16), 155 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_TYPE", Types.VARCHAR, 16), 156 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_CONNECTOR_STATE", Types.VARCHAR, 16), 157 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_CONNECTOR_TRACE", Types.VARCHAR, 128), 158 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_CONNECTOR_WORKER_ID", Types.VARCHAR, 8), 159 | HeaderField("CONNECTOR_DESCRIPTION_STATUS_TASKS_INFO", Types.VARCHAR, 512) 160 | ) ++ sourceDescriptionEntity ++ List( 161 | HeaderField("CONNECTOR_DESCRIPTION_TOPICS", Types.VARCHAR, 64) 162 | ) 163 | 164 | val connectorListEntity = List( 165 | HeaderField("CONNECTOR_NAME", Types.VARCHAR, 64), 166 | HeaderField("CONNECTOR_TYPE", Types.VARCHAR, 16), 167 | HeaderField("CONNECTOR_CLASS_NAME", Types.VARCHAR, 128) 168 | ) 169 | 170 | val createConnectorEntity = List( 171 | HeaderField("CREATE_CONNECTOR_INFO_NAME", Types.VARCHAR, 64), 172 | HeaderField("CREATE_CONNECTOR_INFO_TYPE", Types.VARCHAR, 16), 173 | HeaderField("CREATE_CONNECTOR_INFO_TASKS", Types.VARCHAR, 512), 174 | HeaderField("CREATE_CONNECTOR_INFO_CONFIGS", Types.VARCHAR, 512) 175 | ) 176 | 177 | val dropConnectorEntity = List( 178 | HeaderField("DROP_CONNECTOR_NAME", Types.VARCHAR, 64) 179 | ) 180 | 181 | val errorEntity = List( 182 | HeaderField("ERROR_MESSAGE", Types.VARCHAR, 512) 183 | ) 184 | 185 | val executionPlanEntity = List( 186 | HeaderField("EXECUTION_PLAN", Types.VARCHAR, 1024) 187 | ) 188 | 189 | val functionDescriptionListEntity = List( 190 | HeaderField("FUNCTION_DESCRIPTION_NAME", Types.VARCHAR, 32), 191 | HeaderField("FUNCTION_DESCRIPTION_TYPE", Types.VARCHAR, 16), 192 | HeaderField("FUNCTION_DESCRIPTION_DESCRIPTION", Types.VARCHAR, 128), 193 | HeaderField("FUNCTION_DESCRIPTION_PATH", Types.VARCHAR, 128), 194 | HeaderField("FUNCTION_DESCRIPTION_VERSION", Types.VARCHAR, 8), 195 | HeaderField("FUNCTION_DESCRIPTION_AUTHOR", Types.VARCHAR, 32), 196 | HeaderField("FUNCTION_DESCRIPTION_FN_DESC", Types.VARCHAR, 128), 197 | HeaderField("FUNCTION_DESCRIPTION_FN_RETURN_TYPE", Types.VARCHAR, 16), 198 | HeaderField("FUNCTION_DESCRIPTION_FN_ARGS", Types.VARCHAR, 128) 199 | ) 200 | 201 | val functionNameListEntity = List( 202 | HeaderField("FUNCTION_NAME_FN_NAME", Types.VARCHAR, 16), 203 | HeaderField("FUNCTION_NAME_FN_TYPE", Types.VARCHAR, 16) 204 | ) 205 | 206 | val kafkaTopicsListEntity = List( 207 | HeaderField("KAFKA_TOPIC_NAME", Types.VARCHAR, 16), 208 | HeaderField("KAFKA_TOPIC_REPLICA_INFO", Types.VARCHAR, 32) 209 | ) 210 | 211 | val ksqlTopicsListEntity = List( 212 | HeaderField("KSQL_TOPIC_NAME", Types.VARCHAR, 16), 213 | HeaderField("KSQL_TOPIC_KAFKA_TOPIC", Types.VARCHAR, 16), 214 | HeaderField("KSQL_TOPIC_FORMAT", Types.VARCHAR, 16) 215 | ) 216 | 217 | val kafkaTopicsListExtendedEntity = List( 218 | HeaderField("KSQL_TOPIC_NAME", Types.VARCHAR, 16), 219 | HeaderField("KSQL_TOPIC_REPLICA_INFO", Types.VARCHAR, 32), 220 | HeaderField("KSQL_TOPIC_CONSUMER_COUNT", Types.INTEGER, 8), 221 | HeaderField("KSQL_TOPIC_CONSUMER_GROUP_COUNT", Types.INTEGER, 8) 222 | ) 223 | 224 | val propertiesListEntity = List( 225 | HeaderField("PROPERTY_NAME", Types.VARCHAR, 16), 226 | HeaderField("PROPERTY_VALUE", Types.VARCHAR, 32) 227 | ) 228 | 229 | val queriesEntity = List( 230 | HeaderField("QUERY_ID", Types.VARCHAR, 16), 231 | HeaderField("QUERY_STRING", Types.VARCHAR, 128), 232 | HeaderField("QUERY_SINKS", Types.VARCHAR, 256) 233 | ) 234 | 235 | val queryDescriptionEntity = List( 236 | HeaderField("QUERY_DESCRIPTION_ID", Types.VARCHAR, 16), 237 | HeaderField("QUERY_DESCRIPTION_FIELDS", Types.VARCHAR, 128), 238 | HeaderField("QUERY_DESCRIPTION_SOURCES", Types.VARCHAR, 32), 239 | HeaderField("QUERY_DESCRIPTION_SINKS", Types.VARCHAR, 32), 240 | HeaderField("QUERY_DESCRIPTION_TOPOLOGY", Types.VARCHAR, 255), 241 | HeaderField("QUERY_DESCRIPTION_EXECUTION_PLAN", Types.VARCHAR, 255), 242 | HeaderField("QUERY_DESCRIPTION_STATE", Types.VARCHAR, 16) 243 | ) 244 | 245 | val queryDescriptionEntityList: List[HeaderField] = queryDescriptionEntity 246 | 247 | val sourceDescriptionEntityList: List[HeaderField] = sourceDescriptionEntity 248 | 249 | val streamsListEntity = List( 250 | HeaderField("STREAM_NAME", Types.VARCHAR, 16), 251 | HeaderField("STREAM_TOPIC", Types.VARCHAR, 16), 252 | HeaderField("STREAM_FORMAT", Types.VARCHAR, 16) 253 | ) 254 | 255 | val tablesListEntity = List( 256 | HeaderField("TABLE_NAME", Types.VARCHAR, 16), 257 | HeaderField("TABLE_TOPIC", Types.VARCHAR, 16), 258 | HeaderField("TABLE_FORMAT", Types.VARCHAR, 16), 259 | HeaderField("TABLE_WINDOWS", Types.BOOLEAN, 5) 260 | ) 261 | 262 | val topicDescriptionEntity = List( 263 | HeaderField("TOPIC_DESCRIPTION_NAME", Types.VARCHAR, 16), 264 | HeaderField("TOPIC_DESCRIPTION_KAFKA_TOPIC", Types.VARCHAR, 16), 265 | HeaderField("TOPIC_DESCRIPTION_FORMAT", Types.VARCHAR, 16), 266 | HeaderField("TOPIC_DESCRIPTION_SCHEMA_STRING", Types.VARCHAR, 256) 267 | ) 268 | 269 | val typesListEntity = List( 270 | HeaderField("TYPE", Types.VARCHAR, 16), 271 | HeaderField("TYPE_NAME", Types.VARCHAR, 16) 272 | ) 273 | 274 | } 275 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/KsqlDatabaseMetaDataSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.io.InputStream 4 | import java.sql.{Connection, ResultSet, RowIdLifetime, SQLException, SQLFeatureNotSupportedException} 5 | import java.util.{Collections, Properties} 6 | 7 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils._ 8 | import io.confluent.ksql.rest.client.{KsqlRestClient, MockableKsqlRestClient, QueryStream, RestResponse} 9 | import io.confluent.ksql.rest.entity._ 10 | import javax.ws.rs.core.Response 11 | import org.eclipse.jetty.http.HttpStatus.Code 12 | import org.scalamock.scalatest.MockFactory 13 | import org.scalatest.OneInstancePerTest 14 | import org.scalatest.matchers.should.Matchers 15 | import org.scalatest.wordspec.AnyWordSpec 16 | 17 | import scala.collection.JavaConverters._ 18 | 19 | 20 | class KsqlDatabaseMetaDataSpec extends AnyWordSpec with Matchers with MockFactory with OneInstancePerTest { 21 | 22 | "A KsqlDatabaseMetaData" when { 23 | 24 | val mockResponse = mock[Response] 25 | val mockKsqlRestClient = mock[MockableKsqlRestClient] 26 | 27 | val values = KsqlConnectionValues("localhost", 8080, None, None, Map.empty[String, String]) 28 | val ksqlConnection = new KsqlConnection(values, new Properties) { 29 | override def init: KsqlRestClient = mockKsqlRestClient 30 | } 31 | val metadata = new KsqlDatabaseMetaData(ksqlConnection) 32 | 33 | "validating specs" should { 34 | 35 | "throw not supported exception if not supported" in { 36 | (mockResponse.getEntity _).expects.returns(mock[InputStream]).once 37 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 38 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 39 | .anyNumberOfTimes 40 | 41 | val methods = implementedMethods[KsqlDatabaseMetaData] 42 | reflectMethods[KsqlDatabaseMetaData](methods = methods, implemented = false, obj = metadata) 43 | .foreach(method => { 44 | assertThrows[SQLFeatureNotSupportedException] { 45 | method() 46 | } 47 | }) 48 | } 49 | 50 | "work if implemented" in { 51 | val specialMethods = Set("getTables", "getColumns", "getNumericFunctions", "getStringFunctions", 52 | "getSystemFunctions", "getTimeDateFunctions") 53 | val methods = implementedMethods[KsqlDatabaseMetaData] 54 | .filterNot(specialMethods.contains) 55 | 56 | reflectMethods[KsqlDatabaseMetaData](methods = methods, implemented = true, obj = metadata) 57 | .foreach(method => { 58 | method() 59 | }) 60 | 61 | assertThrows[SQLException] { 62 | metadata.getTables("", "", "", Array[String]("test")) 63 | } 64 | 65 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 66 | .returns(RestResponse.erroneous(Code.INTERNAL_SERVER_ERROR, new KsqlErrorMessage(-1, "error message", Collections.emptyList[String]))) 67 | .once 68 | assertThrows[SQLException] { 69 | metadata.getTables("", "", "", Array[String](TableTypes.TABLE.name)) 70 | } 71 | 72 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 73 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, new KsqlEntityList)) 74 | .twice 75 | metadata.getTables("", "", "[a-z]*", 76 | Array[String](TableTypes.TABLE.name, TableTypes.STREAM.name)).next should be(false) 77 | 78 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 79 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, new KsqlEntityList)) 80 | .twice 81 | metadata.getColumns("", "", "", "").next should be(false) 82 | 83 | assertThrows[SQLException] { 84 | metadata.getColumns("test", "", "test", "test") 85 | } 86 | assertThrows[SQLException] { 87 | metadata.getColumns("", "test", "test", "test") 88 | } 89 | 90 | val fnList = new FunctionNameList( 91 | "LIST FUNCTIONS;", 92 | List( 93 | new SimpleFunctionInfo("TESTFN", FunctionType.SCALAR), 94 | new SimpleFunctionInfo("TESTDATEFN", FunctionType.SCALAR) 95 | ).asJava 96 | ) 97 | val entityListFn = new KsqlEntityList 98 | entityListFn.add(fnList) 99 | 100 | val descFn1 = new FunctionDescriptionList("DESCRIBE FUNCTION testfn;", 101 | "TESTFN", "Description", "Confluent", "version", "path", 102 | List( 103 | new FunctionInfo(List(new ArgumentInfo("arg1", "INT", "Description", false)).asJava, "BIGINT", "Description"), 104 | new FunctionInfo(List(new ArgumentInfo("arg1", "INT", "Description", false)).asJava, "STRING", "Description") 105 | ).asJava, 106 | FunctionType.SCALAR 107 | ) 108 | val descFn2 = new FunctionDescriptionList("DESCRIBE FUNCTION testdatefn;", 109 | "TESTDATEFN", "Description", "Unknown", "version", "path", 110 | List( 111 | new FunctionInfo(List(new ArgumentInfo("arg1", "INT", "Description", false)).asJava, "BIGINT", "Description") 112 | ).asJava, 113 | FunctionType.SCALAR 114 | ) 115 | val entityDescribeFn1 = new KsqlEntityList 116 | entityDescribeFn1.add(descFn1) 117 | val entityDescribeFn2 = new KsqlEntityList 118 | entityDescribeFn2.add(descFn2) 119 | 120 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects("LIST FUNCTIONS;") 121 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityListFn)) 122 | .repeat(4) 123 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects("DESCRIBE FUNCTION TESTFN;") 124 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityDescribeFn1)) 125 | .repeat(4) 126 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects("DESCRIBE FUNCTION TESTDATEFN;") 127 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityDescribeFn2)) 128 | .repeat(4) 129 | 130 | metadata.getNumericFunctions should be("TESTDATEFN,TESTFN") 131 | metadata.getStringFunctions should be("TESTFN") 132 | metadata.getSystemFunctions should be("TESTFN") 133 | metadata.getTimeDateFunctions should be("TESTDATEFN") 134 | 135 | Option(metadata.getConnection) should not be None 136 | metadata.getCatalogs.next should be(false) 137 | metadata.getCatalogTerm should be("TOPIC") 138 | metadata.getSchemaTerm should be("") 139 | metadata.getProcedureTerm should be("") 140 | metadata.getResultSetHoldability should be(ResultSet.HOLD_CURSORS_OVER_COMMIT) 141 | 142 | val tableTypes = metadata.getTableTypes 143 | tableTypes.next should be(true) 144 | tableTypes.getString(1) should be(TableTypes.TABLE.name) 145 | tableTypes.getString("TABLE_TYPE") should be(TableTypes.TABLE.name) 146 | tableTypes.getString("table_type") should be(TableTypes.TABLE.name) 147 | tableTypes.next should be(true) 148 | tableTypes.getString(1) should be(TableTypes.STREAM.name) 149 | tableTypes.getString("TABLE_TYPE") should be(TableTypes.STREAM.name) 150 | tableTypes.getString("table_type") should be(TableTypes.STREAM.name) 151 | tableTypes.next should be(false) 152 | 153 | metadata.getSchemas.next should be(false) 154 | metadata.getSchemas("", "").next should be(false) 155 | assertThrows[SQLException] { 156 | metadata.getSchemas("test", "") 157 | } 158 | assertThrows[SQLException] { 159 | metadata.getSchemas("", "test") 160 | } 161 | 162 | metadata.getSuperTables("", "", "test").next should be(false) 163 | assertThrows[SQLException] { 164 | metadata.getSuperTables("test", "", "test") 165 | } 166 | assertThrows[SQLException] { 167 | metadata.getSuperTables("", "test", "test") 168 | } 169 | } 170 | 171 | "check JDBC specs" in { 172 | 173 | metadata.getURL should be("jdbc:ksql://localhost:8080") 174 | metadata.getTypeInfo.getMetaData.getColumnCount should be(18) 175 | metadata.getSQLKeywords.split(",").length should be(30) 176 | metadata.getMaxStatements should be(0) 177 | metadata.getMaxStatementLength should be(0) 178 | metadata.getProcedures(None.orNull, None.orNull, None.orNull).next should be(false) 179 | metadata.getIdentifierQuoteString should be(" ") 180 | metadata.getSearchStringEscape should be("%") 181 | metadata.getExtraNameCharacters should be("#@") 182 | metadata.getMaxConnections should be(0) 183 | metadata.getMaxIndexLength should be(0) 184 | metadata.getMaxSchemaNameLength should be(0) 185 | metadata.getMaxTableNameLength should be(0) 186 | metadata.getMaxTablesInSelect should be(0) 187 | metadata.getMaxUserNameLength should be(0) 188 | metadata.getMaxUserNameLength should be(0) 189 | metadata.getDefaultTransactionIsolation should be(Connection.TRANSACTION_NONE) 190 | metadata.getRowIdLifetime should be(RowIdLifetime.ROWID_VALID_OTHER) 191 | metadata.getUserName should be ("") 192 | 193 | metadata.allProceduresAreCallable should be(false) 194 | metadata.allTablesAreSelectable should be(false) 195 | 196 | metadata.nullsAreSortedHigh should be(false) 197 | metadata.nullsAreSortedLow should be(false) 198 | metadata.nullsAreSortedAtStart should be(false) 199 | metadata.nullsAreSortedAtEnd should be(false) 200 | 201 | metadata.usesLocalFiles should be(true) 202 | metadata.usesLocalFilePerTable should be(true) 203 | 204 | metadata.nullPlusNonNullIsNull should be(true) 205 | 206 | metadata.storesUpperCaseIdentifiers should be(false) 207 | metadata.storesLowerCaseIdentifiers should be(false) 208 | metadata.storesMixedCaseIdentifiers should be(true) 209 | metadata.storesUpperCaseQuotedIdentifiers should be(false) 210 | metadata.storesLowerCaseQuotedIdentifiers should be(false) 211 | metadata.storesMixedCaseQuotedIdentifiers should be(true) 212 | 213 | metadata.supportsAlterTableWithAddColumn should be(false) 214 | metadata.supportsAlterTableWithDropColumn should be(false) 215 | metadata.supportsCatalogsInDataManipulation should be(false) 216 | metadata.supportsCatalogsInTableDefinitions should be(false) 217 | metadata.supportsCatalogsInProcedureCalls should be(false) 218 | metadata.supportsMultipleResultSets should be(false) 219 | metadata.supportsMultipleTransactions should be(false) 220 | metadata.supportsSchemasInDataManipulation should be(false) 221 | metadata.supportsSchemasInTableDefinitions should be(false) 222 | metadata.supportsStoredFunctionsUsingCallSyntax should be(true) 223 | metadata.supportsStoredProcedures should be(false) 224 | metadata.supportsSavepoints should be(false) 225 | metadata.supportsMixedCaseIdentifiers should be(true) 226 | metadata.supportsMixedCaseQuotedIdentifiers should be(true) 227 | metadata.supportsColumnAliasing should be(true) 228 | metadata.supportsConvert should be(false) 229 | metadata.supportsConvert(12, 15) should be(false) 230 | metadata.supportsTableCorrelationNames should be(true) 231 | metadata.supportsDifferentTableCorrelationNames should be(true) 232 | metadata.supportsExpressionsInOrderBy should be(true) 233 | metadata.supportsExtendedSQLGrammar should be(false) 234 | metadata.supportsGroupBy should be(true) 235 | metadata.supportsOrderByUnrelated should be(false) 236 | metadata.supportsGroupByUnrelated should be(true) 237 | metadata.supportsGroupByBeyondSelect should be(true) 238 | metadata.supportsLikeEscapeClause should be(true) 239 | metadata.supportsNonNullableColumns should be(true) 240 | metadata.supportsMinimumSQLGrammar should be(true) 241 | metadata.supportsCoreSQLGrammar should be(false) 242 | metadata.supportsExtendedSQLGrammar should be(false) 243 | metadata.supportsOuterJoins should be(true) 244 | metadata.supportsFullOuterJoins should be(true) 245 | metadata.supportsLimitedOuterJoins should be(true) 246 | metadata.supportsTransactions should be(false) 247 | metadata.supportsPositionedUpdate should be(false) 248 | metadata.supportsPositionedDelete should be(false) 249 | metadata.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY) should be(true) 250 | metadata.supportsUnion should be(false) 251 | metadata.supportsUnionAll should be(false) 252 | } 253 | } 254 | } 255 | 256 | "A DatabaseMetaDataNotSupported" when { 257 | 258 | "validating specs" should { 259 | 260 | "throw not supported exception if not supported" in { 261 | 262 | val metadata = new DatabaseMetaDataNotSupported 263 | reflectMethods[DatabaseMetaDataNotSupported](methods = Seq.empty, implemented = false, obj = metadata) 264 | .foreach(method => { 265 | assertThrows[SQLFeatureNotSupportedException] { 266 | method() 267 | } 268 | }) 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/test/scala/com/github/mmolimar/ksql/jdbc/KsqlStatementSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.io.{ByteArrayInputStream, InputStream} 4 | import java.sql.{ResultSet, SQLException, SQLFeatureNotSupportedException} 5 | import java.util.Collections.emptyList 6 | import java.util.Optional 7 | 8 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils._ 9 | import io.confluent.ksql.metastore.model.DataSource.DataSourceType 10 | import io.confluent.ksql.name.ColumnName 11 | import io.confluent.ksql.query.QueryId 12 | import io.confluent.ksql.rest.client.{MockableKsqlRestClient, QueryStream, RestResponse} 13 | import io.confluent.ksql.rest.entity.{ExecutionPlan, KafkaTopicsList, QueryDescriptionEntity, QueryDescriptionList, _} 14 | import io.confluent.ksql.rest.util.EntityUtil 15 | import io.confluent.ksql.schema.ksql.types.SqlTypes 16 | import io.confluent.ksql.schema.ksql.{LogicalSchema, SqlBaseType => KsqlType} 17 | import javax.ws.rs.core.Response 18 | import org.apache.kafka.connect.runtime.rest.entities.{ConnectorInfo, ConnectorStateInfo, ConnectorType} 19 | import org.eclipse.jetty.http.HttpStatus.Code 20 | import org.scalamock.scalatest.MockFactory 21 | import org.scalatest.OneInstancePerTest 22 | import org.scalatest.matchers.should.Matchers 23 | import org.scalatest.wordspec.AnyWordSpec 24 | 25 | import scala.collection.JavaConverters._ 26 | 27 | 28 | class KsqlStatementSpec extends AnyWordSpec with Matchers with MockFactory with OneInstancePerTest { 29 | 30 | "A KsqlStatement" when { 31 | 32 | val mockResponse = mock[Response] 33 | (mockResponse.getEntity _).expects.returns(mock[InputStream]).anyNumberOfTimes 34 | val mockKsqlRestClient = mock[MockableKsqlRestClient] 35 | val statement = new KsqlStatement(mockKsqlRestClient) 36 | 37 | "validating specs" should { 38 | 39 | "throw not supported exception if not supported" in { 40 | 41 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 42 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 43 | .noMoreThanOnce 44 | 45 | val methods = implementedMethods[KsqlStatement] 46 | reflectMethods[KsqlStatement](methods = methods, implemented = false, obj = statement) 47 | .foreach(method => { 48 | assertThrows[SQLFeatureNotSupportedException] { 49 | method() 50 | } 51 | }) 52 | } 53 | 54 | "work when executing queries" in { 55 | 56 | assertThrows[SQLException] { 57 | statement.getResultSet 58 | } 59 | assertThrows[SQLException] { 60 | statement.execute(null, -1) 61 | } 62 | assertThrows[SQLException] { 63 | statement.execute("", Array[Int]()) 64 | } 65 | assertThrows[SQLException] { 66 | statement.execute("invalid query", Array[String]()) 67 | } 68 | assertThrows[SQLException] { 69 | statement.execute("select * from test; select * from test;") 70 | } 71 | 72 | assertThrows[SQLException] { 73 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 74 | .returns(RestResponse.erroneous(Code.INTERNAL_SERVER_ERROR, new KsqlErrorMessage(-1, "error"))) 75 | .once 76 | statement.execute("select * from test") 77 | } 78 | 79 | assertThrows[SQLException] { 80 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 81 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 82 | .once 83 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 84 | .returns(RestResponse.erroneous(Code.INTERNAL_SERVER_ERROR, new KsqlErrorMessage(-1, "error"))) 85 | .once 86 | statement.execute("select * from test") 87 | } 88 | 89 | assertThrows[SQLException] { 90 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 91 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 92 | .once 93 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 94 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, new KsqlEntityList)) 95 | .once 96 | statement.execute("select * from test") 97 | } 98 | 99 | val queryDesc = new QueryDescription( 100 | new QueryId("id"), 101 | "select * from test;", 102 | List( 103 | new FieldInfo("field1", new SchemaInfo(KsqlType.INTEGER, List.empty.asJava, None.orNull)), 104 | new FieldInfo("field2", new SchemaInfo(KsqlType.BIGINT, List.empty.asJava, None.orNull)), 105 | new FieldInfo("field3", new SchemaInfo(KsqlType.DOUBLE, List.empty.asJava, None.orNull)), 106 | new FieldInfo("field4", new SchemaInfo(KsqlType.DECIMAL, List.empty.asJava, None.orNull)), 107 | new FieldInfo("field5", new SchemaInfo(KsqlType.BOOLEAN, List.empty.asJava, None.orNull)), 108 | new FieldInfo("field6", new SchemaInfo(KsqlType.STRING, List.empty.asJava, None.orNull)), 109 | new FieldInfo("field7", new SchemaInfo(KsqlType.MAP, List.empty.asJava, None.orNull)), 110 | new FieldInfo("field8", new SchemaInfo(KsqlType.ARRAY, List.empty.asJava, None.orNull)), 111 | new FieldInfo("field9", new SchemaInfo(KsqlType.STRUCT, List.empty.asJava, None.orNull)) 112 | 113 | ).asJava, 114 | Set("test").asJava, 115 | Set("sink1").asJava, 116 | "topologyTest", 117 | "executionPlanTest", 118 | Map.empty[String, AnyRef].asJava, 119 | Optional.empty[String] 120 | ) 121 | 122 | val entityList = new KsqlEntityList 123 | entityList.add(new QueryDescriptionEntity("select * from test;", queryDesc)) 124 | 125 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 126 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 127 | .once 128 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 129 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityList)) 130 | .once 131 | statement.execute("select * from test") should be(true) 132 | 133 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 134 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 135 | .once 136 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 137 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityList)) 138 | .once 139 | Option(statement.executeQuery("select * from test;")) should not be None 140 | 141 | statement.getMaxRows should be(0) 142 | statement.getResultSet shouldNot be(None.orNull) 143 | assertThrows[SQLException] { 144 | statement.setMaxRows(-1) 145 | } 146 | statement.setMaxRows(1) 147 | statement.getMaxRows should be(1) 148 | 149 | statement.getUpdateCount should be(-1) 150 | statement.getResultSetType should be(ResultSet.TYPE_FORWARD_ONLY) 151 | statement.getResultSetHoldability should be(ResultSet.HOLD_CURSORS_OVER_COMMIT) 152 | statement.getWarnings should be(None.orNull) 153 | 154 | assertThrows[SQLException] { 155 | (mockKsqlRestClient.makeQueryRequest _).expects(*, *) 156 | .returns(RestResponse.successful[QueryStream](Code.OK, mockQueryStream(mockResponse))) 157 | .once 158 | val multipleResults = new KsqlEntityList 159 | multipleResults.add(new QueryDescriptionEntity("select * from test;", queryDesc)) 160 | multipleResults.add(new QueryDescriptionEntity("select * from test;", queryDesc)) 161 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 162 | .returns(RestResponse.successful[KsqlEntityList](Code.ACCEPTED, multipleResults)) 163 | .once 164 | statement.execute("select * from test") 165 | } 166 | assertThrows[SQLException] { 167 | statement.getResultSet 168 | } 169 | statement.cancel() 170 | 171 | statement.isClosed should be(false) 172 | statement.close() 173 | statement.close() 174 | statement.isClosed should be(true) 175 | assertThrows[SQLException] { 176 | statement.executeQuery("select * from test;") 177 | } 178 | } 179 | 180 | "work when printing topics" in { 181 | (mockKsqlRestClient.makePrintTopicRequest _).expects(*, *) 182 | .returns(RestResponse.successful[InputStream](Code.OK, new ByteArrayInputStream("test".getBytes))) 183 | .once 184 | Option(statement.executeQuery("print 'test'")) should not be None 185 | statement.getResultSet.next should be(true) 186 | statement.getResultSet.getString(1) should be("test") 187 | } 188 | 189 | "work when executing KSQL commands" in { 190 | import KsqlEntityHeaders._ 191 | 192 | def validateCommand(entity: KsqlEntity, headers: List[HeaderField]): Unit = { 193 | val entityList = new KsqlEntityList 194 | entityList.add(entity) 195 | (mockKsqlRestClient.makeKsqlRequest(_: String)).expects(*) 196 | .returns(RestResponse.successful[KsqlEntityList](Code.OK, entityList)) 197 | .once 198 | statement.execute(entity.getStatementText) should be(true) 199 | statement.getResultSet.getMetaData.getColumnCount should be(headers.size) 200 | headers.zipWithIndex.map { case (c, index) => 201 | statement.getResultSet.getMetaData.getColumnName(index + 1) should be(c.name) 202 | statement.getResultSet.getMetaData.getColumnLabel(index + 1).toUpperCase should be(c.name) 203 | } 204 | } 205 | 206 | val schema = LogicalSchema.builder.valueColumn(ColumnName.of("key"), SqlTypes.BIGINT).build 207 | val commandStatus = new CommandStatusEntity("CREATE STREAM TEST AS SELECT * FROM test", 208 | CommandId.fromString("topic/1/create"), 209 | new CommandStatus(CommandStatus.Status.SUCCESS, "Success Message"), null) 210 | val sourceDesc = new SourceDescription( 211 | "datasource", 212 | List(new RunningQuery("read query", Set("sink1").asJava, new QueryId("readId"))).asJava, 213 | List(new RunningQuery("read query", Set("sink1").asJava, new QueryId("readId"))).asJava, 214 | EntityUtil.buildSourceSchemaEntity(schema, false), 215 | DataSourceType.KTABLE.getKsqlType, 216 | "key", 217 | "2000-01-01", 218 | "stats", 219 | "errors", 220 | true, 221 | "avro", 222 | "kadka-topic", 223 | 2, 224 | 1 225 | ) 226 | val connectorDesc = new ConnectorDescription( 227 | "DESCRIBE CONNECTOR `test-connector`", 228 | "test.Connector", 229 | new ConnectorStateInfo( 230 | "name", 231 | new ConnectorStateInfo.ConnectorState("state", "worker", "msg"), 232 | List(new ConnectorStateInfo.TaskState(0, "task", "worker", "task_msg")).asJava, 233 | ConnectorType.SOURCE 234 | ), 235 | List(sourceDesc).asJava, 236 | List("test-topic").asJava, 237 | List.empty.asJava) 238 | val connectorList = new ConnectorList( 239 | "SHOW CONNECTORS", 240 | List.empty.asJava, 241 | List(new SimpleConnectorInfo("testConnector", ConnectorType.SOURCE, "ConnectorClassname")).asJava 242 | ) 243 | val createConnector = new CreateConnectorEntity( 244 | "CREATE SOURCE CONNECTOR test WITH ('prop1'='value')", 245 | new ConnectorInfo("connector", 246 | Map.empty[String, String].asJava, 247 | List.empty.asJava, 248 | ConnectorType.SOURCE) 249 | ) 250 | val dropConnector = new DropConnectorEntity("DROP CONNECTOR `test-connector`", "TestConnector") 251 | val error = new ErrorEntity("SHOW CONNECTORS", "Error message") 252 | val executionPlan = new ExecutionPlan("DESCRIBE test") 253 | val functionDescriptionList = new FunctionDescriptionList("DESCRIBE FUNCTION test;", 254 | "TEST", "Description", "author", "version", "path", 255 | List( 256 | new FunctionInfo(List(new ArgumentInfo("arg1", "INT", "Description", false)).asJava, "BIGINT", "Description") 257 | ).asJava, 258 | FunctionType.SCALAR 259 | ) 260 | val functionNameList = new FunctionNameList( 261 | "LIST FUNCTIONS;", 262 | List(new SimpleFunctionInfo("TESTFN", FunctionType.SCALAR)).asJava 263 | ) 264 | val kafkaTopicsList = new KafkaTopicsList( 265 | "SHOW TOPICS;", 266 | List(new KafkaTopicInfo("test", List(Int.box(1)).asJava)).asJava 267 | ) 268 | val kafkaTopicsListExt = new KafkaTopicsListExtended( 269 | "LIST TOPICS EXTENDED", 270 | List(new KafkaTopicInfoExtended("test", List(Int.box(1)).asJava, 5, 10)).asJava 271 | ) 272 | val propertiesList = new PropertiesList( 273 | "list properties;", 274 | Map("key" -> "earliest").asJava, 275 | List.empty.asJava, 276 | List.empty.asJava 277 | ) 278 | val queries = new Queries( 279 | "EXPLAIN select * from test", 280 | List(new RunningQuery("select * from test;", Set("Test").asJava, new QueryId("id"))).asJava 281 | ) 282 | val queryDescription = new QueryDescriptionEntity( 283 | "EXPLAIN select * from test;", 284 | new QueryDescription( 285 | new QueryId("id"), 286 | "select * from test;", 287 | List( 288 | new FieldInfo("field1", new SchemaInfo(KsqlType.INTEGER, List.empty.asJava, None.orNull)), 289 | new FieldInfo("field2", new SchemaInfo(KsqlType.BIGINT, List.empty.asJava, None.orNull)), 290 | new FieldInfo("field3", new SchemaInfo(KsqlType.DOUBLE, List.empty.asJava, None.orNull)), 291 | new FieldInfo("field4", new SchemaInfo(KsqlType.DECIMAL, List.empty.asJava, None.orNull)), 292 | new FieldInfo("field5", new SchemaInfo(KsqlType.BOOLEAN, List.empty.asJava, None.orNull)), 293 | new FieldInfo("field6", new SchemaInfo(KsqlType.STRING, List.empty.asJava, None.orNull)), 294 | new FieldInfo("field7", new SchemaInfo(KsqlType.MAP, List.empty.asJava, None.orNull)), 295 | new FieldInfo("field8", new SchemaInfo(KsqlType.ARRAY, List.empty.asJava, None.orNull)), 296 | new FieldInfo("field9", new SchemaInfo(KsqlType.STRUCT, List.empty.asJava, None.orNull)) 297 | 298 | ).asJava, 299 | Set("test").asJava, 300 | Set("sink1").asJava, 301 | "topologyTest", 302 | "executionPlanTest", 303 | Map.empty[String, AnyRef].asJava, 304 | Optional.empty[String] 305 | ) 306 | ) 307 | val queryDescriptionList = new QueryDescriptionList( 308 | "EXPLAIN select * from test;", 309 | List(queryDescription.getQueryDescription).asJava 310 | ) 311 | val sourceDescEntity = new SourceDescriptionEntity( 312 | "DESCRIBE TEST;", 313 | sourceDesc, 314 | emptyList[KsqlWarning] 315 | ) 316 | val sourceDescList = new SourceDescriptionList( 317 | "EXPLAIN select * from test;", 318 | List(sourceDescEntity.getSourceDescription).asJava, 319 | emptyList[KsqlWarning] 320 | ) 321 | val streamsList = new StreamsList("SHOW STREAMS", List(new SourceInfo.Stream("TestStream", "TestTopic", "AVRO")).asJava) 322 | val tablesList = new TablesList("SHOW TABLES", List(new SourceInfo.Table("TestTable", "TestTopic", "JSON", false)).asJava) 323 | val topicDesc = new TopicDescription("DESCRIBE TEST", "TestTopic", "TestTopic", "AVRO", "schema") 324 | val types = new TypeList( 325 | "SHOW TYPES", 326 | Map("typeTest" -> new SchemaInfo(KsqlType.ARRAY, List.empty.asJava, None.orNull)).asJava 327 | ) 328 | 329 | val commands = Seq( 330 | (commandStatus, commandStatusEntity), 331 | (connectorDesc, connectorDescriptionEntity), 332 | (connectorList, connectorListEntity), 333 | (createConnector, createConnectorEntity), 334 | (dropConnector, dropConnectorEntity), 335 | (error, errorEntity), 336 | (executionPlan, executionPlanEntity), 337 | (functionDescriptionList, functionDescriptionListEntity), 338 | (functionNameList, functionNameListEntity), 339 | (kafkaTopicsList, kafkaTopicsListEntity), 340 | (kafkaTopicsListExt, kafkaTopicsListExtendedEntity), 341 | (propertiesList, propertiesListEntity), 342 | (queries, queriesEntity), 343 | (queryDescription, queryDescriptionEntity), 344 | (queryDescriptionList, queryDescriptionEntityList), 345 | (sourceDescEntity, sourceDescriptionEntity), 346 | (sourceDescList, sourceDescriptionEntityList), 347 | (streamsList, streamsListEntity), 348 | (tablesList, tablesListEntity), 349 | (topicDesc, topicDescriptionEntity), 350 | (types, typesListEntity) 351 | ) 352 | commands.foreach(c => validateCommand(c._1, c._2)) 353 | } 354 | } 355 | } 356 | 357 | "A StatementNotSupported" when { 358 | 359 | "validating specs" should { 360 | 361 | "throw not supported exception if not supported" in { 362 | 363 | val resultSet = new StatementNotSupported 364 | reflectMethods[StatementNotSupported](methods = Seq.empty, implemented = false, obj = resultSet) 365 | .foreach(method => { 366 | assertThrows[SQLFeatureNotSupportedException] { 367 | method() 368 | } 369 | }) 370 | } 371 | } 372 | } 373 | 374 | } 375 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/KsqlStatement.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.io.InputStream 4 | import java.sql.{Connection, ResultSet, SQLWarning, Statement, Types} 5 | 6 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 7 | import com.github.mmolimar.ksql.jdbc.resultset.{StreamedResultSet, _} 8 | import io.confluent.ksql.parser.{DefaultKsqlParser, KsqlParser} 9 | import io.confluent.ksql.rest.client.{KsqlRestClient, QueryStream, RestResponse} 10 | import io.confluent.ksql.rest.entity._ 11 | import io.confluent.ksql.schema.ksql.{SqlBaseType => KsqlType} 12 | 13 | import scala.collection.JavaConverters._ 14 | import scala.language.implicitConversions 15 | import scala.util.{Failure, Success, Try} 16 | 17 | private object KsqlStatement { 18 | 19 | private val ksqlParser: KsqlParser = new DefaultKsqlParser 20 | 21 | } 22 | 23 | class StatementNotSupported extends Statement with WrapperNotSupported { 24 | 25 | override def cancel(): Unit = throw NotSupported("cancel") 26 | 27 | override def getResultSetHoldability: Int = throw NotSupported("getResultSetHoldability") 28 | 29 | override def getMaxFieldSize: Int = throw NotSupported("getMaxFieldSize") 30 | 31 | override def getUpdateCount: Int = throw NotSupported("getUpdateCount") 32 | 33 | override def setPoolable(poolable: Boolean): Unit = throw NotSupported("setPoolable") 34 | 35 | override def getFetchSize: Int = throw NotSupported("getFetchSize") 36 | 37 | override def setQueryTimeout(seconds: Int): Unit = throw NotSupported("setQueryTimeout") 38 | 39 | override def setFetchDirection(direction: Int): Unit = throw NotSupported("setFetchDirection") 40 | 41 | override def setMaxRows(max: Int): Unit = throw NotSupported("setMaxRows") 42 | 43 | override def setCursorName(name: String): Unit = throw NotSupported("setCursorName") 44 | 45 | override def getFetchDirection: Int = throw NotSupported("getFetchDirection") 46 | 47 | override def getResultSetType: Int = throw NotSupported("getResultSetType") 48 | 49 | override def getMoreResults: Boolean = throw NotSupported("getMoreResults") 50 | 51 | override def getMoreResults(current: Int): Boolean = throw NotSupported("getMoreResults") 52 | 53 | override def addBatch(sql: String): Unit = throw NotSupported("addBatch") 54 | 55 | override def execute(sql: String): Boolean = throw NotSupported("execute") 56 | 57 | override def execute(sql: String, autoGeneratedKeys: Int): Boolean = throw NotSupported("execute") 58 | 59 | override def execute(sql: String, columnIndexes: Array[Int]): Boolean = throw NotSupported("execute") 60 | 61 | override def execute(sql: String, columnNames: Array[String]): Boolean = throw NotSupported("execute") 62 | 63 | override def executeQuery(sql: String): ResultSet = throw NotSupported("executeQuery") 64 | 65 | override def isCloseOnCompletion: Boolean = throw NotSupported("isCloseOnCompletion") 66 | 67 | override def getResultSet: ResultSet = throw NotSupported("getResultSet") 68 | 69 | override def getMaxRows: Int = throw NotSupported("getMaxRows") 70 | 71 | override def setEscapeProcessing(enable: Boolean): Unit = throw NotSupported("setEscapeProcessing") 72 | 73 | override def executeUpdate(sql: String): Int = throw NotSupported("executeUpdate") 74 | 75 | override def executeUpdate(sql: String, autoGeneratedKeys: Int): Int = throw NotSupported("executeUpdate") 76 | 77 | override def executeUpdate(sql: String, columnIndexes: Array[Int]): Int = throw NotSupported("executeUpdate") 78 | 79 | override def executeUpdate(sql: String, columnNames: Array[String]): Int = throw NotSupported("executeUpdate") 80 | 81 | override def getQueryTimeout: Int = throw NotSupported("getQueryTimeout") 82 | 83 | override def getWarnings: SQLWarning = throw NotSupported("getWarnings") 84 | 85 | override def getConnection: Connection = throw NotSupported("getConnection") 86 | 87 | override def setMaxFieldSize(max: Int): Unit = throw NotSupported("setMaxFieldSize") 88 | 89 | override def isPoolable: Boolean = throw NotSupported("isPoolable") 90 | 91 | override def clearBatch(): Unit = throw NotSupported("clearBatch") 92 | 93 | override def close(): Unit = throw NotSupported("close") 94 | 95 | override def closeOnCompletion(): Unit = throw NotSupported("closeOnCompletion") 96 | 97 | override def executeBatch: Array[Int] = throw NotSupported("executeBatch") 98 | 99 | override def getGeneratedKeys: ResultSet = throw NotSupported("getGeneratedKeys") 100 | 101 | override def setFetchSize(rows: Int): Unit = throw NotSupported("setFetchSize") 102 | 103 | override def clearWarnings(): Unit = throw NotSupported("clearWarnings") 104 | 105 | override def getResultSetConcurrency: Int = throw NotSupported("getResultSetConcurrency") 106 | 107 | override def isClosed: Boolean = throw NotSupported("isClosed") 108 | } 109 | 110 | class KsqlStatement(private val ksqlClient: KsqlRestClient, val timeout: Long = 0) extends StatementNotSupported { 111 | 112 | import KsqlStatement._ 113 | 114 | private[this] var currentResultSet: Option[ResultSet] = None 115 | private var maxRows = 0 116 | private var closed = false 117 | 118 | override def cancel(): Unit = { 119 | currentResultSet.foreach(_.close) 120 | currentResultSet = None 121 | } 122 | 123 | override def close(): Unit = { 124 | cancel() 125 | closed = true 126 | } 127 | 128 | override def isClosed: Boolean = closed 129 | 130 | override def getResultSet: ResultSet = currentResultSet.getOrElse(throw ResultSetError("Result set not initialized.")) 131 | 132 | override def getUpdateCount: Int = -1 133 | 134 | override def getMaxRows: Int = maxRows 135 | 136 | override def getMoreResults: Boolean = !isClosed && currentResultSet.isDefined && (currentResultSet.get match { 137 | case srs: StreamedResultSet => srs.hasNext 138 | case irs: IteratorResultSet[_] => irs.rows.hasNext 139 | }) 140 | 141 | override def getMoreResults(current: Int): Boolean = getMoreResults 142 | 143 | private def executeKsqlRequest(sql: String): Unit = { 144 | if (closed) throw AlreadyClosed("Statement already closed.") 145 | 146 | currentResultSet = None 147 | val fixedSql = fixSql(sql) 148 | val stmt = Try(ksqlParser.parse(fixedSql)) match { 149 | case Failure(e) => throw KsqlQueryError(s"Error parsing query '$fixedSql': ${e.getMessage}.", None, e) 150 | case Success(s) if s.isEmpty => throw KsqlQueryError("Query cannot be empty.") 151 | case Success(s) if s.size() > 1 => throw KsqlQueryError("You have to execute just one query at a time. " + 152 | s"Number of queries sent: '${s.size}'.") 153 | case Success(s) => s.get(0) 154 | } 155 | 156 | val response = stmt.getStatement.statement.getStart.getText.trim.toUpperCase match { 157 | case "SELECT" => 158 | ksqlClient.makeQueryRequest(fixedSql, None.orNull).asInstanceOf[RestResponse[AnyRef]] 159 | case "PRINT" => 160 | ksqlClient.makePrintTopicRequest(fixedSql, None.orNull).asInstanceOf[RestResponse[AnyRef]] 161 | case _ => 162 | ksqlClient.makeKsqlRequest(fixedSql).asInstanceOf[RestResponse[AnyRef]] 163 | } 164 | if (response.isErroneous) { 165 | throw KsqlQueryError(s"Error executing KSQL request: '$fixedSql'", Some(response.getErrorMessage)) 166 | } 167 | 168 | val resultSet: ResultSet = response.get match { 169 | case e: QueryStream => 170 | implicit lazy val queryDesc: QueryDescription = { 171 | val response = ksqlClient.makeKsqlRequest(s"EXPLAIN $fixedSql") 172 | if (response.isErroneous) { 173 | throw KsqlQueryError(s"Error getting metadata for query: '$fixedSql'", Some(response.getErrorMessage)) 174 | } else if (response.getResponse.size != 1) { 175 | throw KsqlEntityListError("Invalid metadata for result set.") 176 | } 177 | response.getResponse.get(0).asInstanceOf[QueryDescriptionEntity] 178 | }.getQueryDescription 179 | e 180 | case e: KsqlEntityList => e.asInstanceOf[KsqlEntityList] 181 | case e: InputStream => e.asInstanceOf[InputStream] 182 | } 183 | currentResultSet = Some(resultSet) 184 | } 185 | 186 | override def execute(sql: String): Boolean = { 187 | executeKsqlRequest(sql) 188 | currentResultSet.nonEmpty 189 | } 190 | 191 | //TODO 192 | override def execute(sql: String, autoGeneratedKeys: Int): Boolean = execute(sql) 193 | 194 | //TODO 195 | override def execute(sql: String, columnIndexes: Array[Int]): Boolean = execute(sql) 196 | 197 | //TODO 198 | override def execute(sql: String, columnNames: Array[String]): Boolean = execute(sql) 199 | 200 | override def executeQuery(sql: String): ResultSet = { 201 | executeKsqlRequest(sql) 202 | currentResultSet.getOrElse(throw ResultSetError("Result set not initialized.")) 203 | } 204 | 205 | override def getResultSetType: Int = ResultSet.TYPE_FORWARD_ONLY 206 | 207 | override def getResultSetHoldability: Int = ResultSet.HOLD_CURSORS_OVER_COMMIT 208 | 209 | override def setMaxRows(max: Int): Unit = if (max < 0) throw InvalidValue("maxRows", max.toString) else maxRows = max 210 | 211 | override def getWarnings: SQLWarning = None.orNull 212 | 213 | private def fixSql(sql: String): String = { 214 | Option(sql).filter(_.trim.nonEmpty).map(s => if (s.trim.last == ';') s else s + ";").getOrElse("") 215 | } 216 | 217 | private implicit def toResultSet(stream: QueryStream) 218 | (implicit queryDesc: QueryDescription): ResultSet = { 219 | def mapType(ksqlType: KsqlType): (Int, Int) = ksqlType match { 220 | case KsqlType.INTEGER => (Types.INTEGER, 16) 221 | case KsqlType.BIGINT => (Types.BIGINT, 32) 222 | case KsqlType.DOUBLE => (Types.DOUBLE, 32) 223 | case KsqlType.DECIMAL => (Types.DECIMAL, 32) 224 | case KsqlType.BOOLEAN => (Types.BOOLEAN, 8) 225 | case KsqlType.STRING => (Types.VARCHAR, 128) 226 | case KsqlType.MAP => (Types.JAVA_OBJECT, 255) 227 | case KsqlType.ARRAY => (Types.ARRAY, 255) 228 | case KsqlType.STRUCT => (Types.STRUCT, 255) 229 | } 230 | 231 | val columns = queryDesc.getFields.asScala.map { f => 232 | val (mappedType, length) = mapType(f.getSchema.getType) 233 | HeaderField(f.getName, mappedType, length) 234 | }.toList 235 | new StreamedResultSet(new KsqlResultSetMetaData(columns), new KsqlQueryStream(stream), maxRows, timeout) 236 | } 237 | 238 | private implicit def toResultSet(stream: InputStream): ResultSet = { 239 | new StreamedResultSet(new KsqlResultSetMetaData(KsqlEntityHeaders.printTopic), 240 | new KsqlInputStream(stream), maxRows, timeout) 241 | } 242 | 243 | private implicit def toResultSet(list: KsqlEntityList): ResultSet = { 244 | import KsqlEntityHeaders._ 245 | 246 | if (list.size > 1) throw KsqlEntityListError(s"KSQL entity list with an invalid number of entities: '${list.size}'.") 247 | list.asScala.headOption.map { 248 | case e: CommandStatusEntity => 249 | val rows = Iterator(Seq( 250 | e.getCommandSequenceNumber, 251 | e.getCommandId.getType.name, 252 | e.getCommandId.getEntity, 253 | e.getCommandId.getAction.name, 254 | e.getCommandStatus.getStatus.name, 255 | e.getCommandStatus.getMessage 256 | )) 257 | new IteratorResultSet[Any](commandStatusEntity, maxRows, rows) 258 | case e: ConnectorDescription => 259 | val sources = if (e.getSources.isEmpty) { 260 | Seq(new SourceDescription( 261 | "", List.empty.asJava, List.empty.asJava, List.empty.asJava, "", "", "", "", "", false, "", "", 0, 0) 262 | ) 263 | } else { 264 | e.getSources.asScala 265 | } 266 | val rows = sources.map(s => Seq( 267 | e.getConnectorClass, 268 | e.getStatus.name(), 269 | e.getStatus.`type`.name, 270 | e.getStatus.connector.state, 271 | e.getStatus.connector.trace, 272 | e.getStatus.connector.workerId, 273 | e.getStatus.tasks.asScala.map(t => s"${t.id}-${t.state}-${t.workerId}: ${Option(t.trace).getOrElse("")}").mkString("\n"), 274 | s.getKey, 275 | s.getName, 276 | s.getType, 277 | s.getTopic, 278 | s.getFormat, 279 | s.getFields.asScala.map(f => s"${f.getName}:${f.getSchema.getTypeName}").mkString(", "), 280 | s.getPartitions, 281 | s.getReplication, 282 | s.getStatistics, 283 | s.getErrorStats, 284 | s.getTimestamp, 285 | e.getTopics.asScala.mkString(", ") 286 | )).toIterator 287 | new IteratorResultSet[Any](connectorDescriptionEntity, maxRows, rows) 288 | case e: ConnectorList => 289 | val rows = e.getConnectors.asList.asScala.map(c => Seq( 290 | c.getName, 291 | Option(c.getType).map(_.name()).getOrElse(""), 292 | c.getClassName 293 | )).toIterator 294 | new IteratorResultSet[String](connectorListEntity, maxRows, rows) 295 | case e: CreateConnectorEntity => 296 | val rows = Iterator(Seq( 297 | e.getInfo.name(), 298 | Option(e.getInfo.`type`).map(_.name).getOrElse(""), 299 | e.getInfo.tasks.asScala.map(t => s"[${t.task}]-${t.connector}").mkString("\n"), 300 | e.getInfo.config.asScala.map(c => s"${c._1} -> ${c._2}").mkString("\n") 301 | )) 302 | new IteratorResultSet[String](createConnectorEntity, maxRows, rows) 303 | case e: DropConnectorEntity => 304 | val rows = Iterator(Seq( 305 | e.getConnectorName 306 | )) 307 | new IteratorResultSet[String](dropConnectorEntity, maxRows, rows) 308 | case e: ErrorEntity => 309 | new IteratorResultSet[String](errorEntity, maxRows, Iterator(Seq(e.getErrorMessage))) 310 | case e: ExecutionPlan => 311 | new IteratorResultSet[String](executionPlanEntity, maxRows, Iterator(Seq(e.getExecutionPlan))) 312 | case e: FunctionDescriptionList => 313 | val rows = e.getFunctions.asScala.map(f => Seq( 314 | e.getName, 315 | e.getType.name, 316 | e.getDescription, 317 | e.getPath, 318 | e.getVersion, 319 | e.getAuthor, 320 | f.getDescription, 321 | f.getReturnType, 322 | f.getArguments.asScala.map(arg => s"${arg.getName}:${arg.getType}").mkString(", ") 323 | )).toIterator 324 | new IteratorResultSet[String](functionDescriptionListEntity, maxRows, rows) 325 | case e: FunctionNameList => 326 | val rows = e.getFunctions.asScala.map(f => Seq( 327 | f.getName, 328 | f.getType.name 329 | )).toIterator 330 | new IteratorResultSet[String](functionNameListEntity, maxRows, rows) 331 | case e: KafkaTopicsList => 332 | val rows = e.getTopics.asScala.map(t => Seq( 333 | t.getName, 334 | t.getReplicaInfo.asScala.mkString(", ") 335 | )).toIterator 336 | new IteratorResultSet[String](kafkaTopicsListEntity, maxRows, rows) 337 | case e: KafkaTopicsListExtended => 338 | val rows = e.getTopics.asScala.map(t => Seq( 339 | t.getName, 340 | t.getReplicaInfo.asScala.mkString(", "), 341 | t.getConsumerCount, 342 | t.getConsumerGroupCount 343 | )).toIterator 344 | new IteratorResultSet[Any](kafkaTopicsListExtendedEntity, maxRows, rows) 345 | case e: PropertiesList => 346 | val rows = e.getProperties.asScala.map(p => Seq( 347 | p._1, 348 | Option(p._2).map(_.toString).getOrElse("UNDEFINED") 349 | )).toIterator 350 | new IteratorResultSet[String](propertiesListEntity, maxRows, rows) 351 | case e: Queries => 352 | val rows = e.getQueries.asScala.map(q => Seq( 353 | q.getId.getId, 354 | q.getQueryString, 355 | q.getSinks.asScala.mkString(", ") 356 | )).toIterator 357 | new IteratorResultSet[String](queriesEntity, maxRows, rows) 358 | case e@(_: QueryDescriptionEntity | _: QueryDescriptionList) => 359 | val descriptions: Seq[QueryDescription] = e match { 360 | case qde: QueryDescriptionEntity => Seq(qde.getQueryDescription) 361 | case qdl: QueryDescriptionList => qdl.getQueryDescriptions.asScala 362 | } 363 | val rows = descriptions.map(d => Seq( 364 | d.getId.getId, 365 | d.getFields.asScala.map(_.getName).mkString(", "), 366 | d.getSources.asScala.mkString(", "), 367 | d.getSinks.asScala.mkString(", "), 368 | d.getTopology, 369 | d.getExecutionPlan, 370 | d.getState.orElse("") 371 | )).toIterator 372 | new IteratorResultSet[String](queryDescriptionEntityList, maxRows, rows) 373 | case e@(_: SourceDescriptionEntity | _: SourceDescriptionList) => 374 | val descriptions: Seq[SourceDescription] = e match { 375 | case sde: SourceDescriptionEntity => Seq(sde.getSourceDescription) 376 | case sdl: SourceDescriptionList => sdl.getSourceDescriptions.asScala 377 | } 378 | val rows = descriptions.map(d => Seq( 379 | d.getKey, 380 | d.getName, 381 | d.getType, 382 | d.getTopic, 383 | d.getFormat, 384 | d.getFields.asScala.map(f => s"${f.getName}:${f.getSchema.getTypeName}").mkString(", "), 385 | d.getPartitions, 386 | d.getReplication, 387 | d.getStatistics, 388 | d.getErrorStats, 389 | d.getTimestamp 390 | )).toIterator 391 | new IteratorResultSet[Any](sourceDescriptionEntityList, maxRows, rows) 392 | case e: StreamsList => 393 | val rows = e.getStreams.asScala.map(s => Seq( 394 | s.getName, 395 | s.getTopic, 396 | s.getFormat 397 | )).toIterator 398 | new IteratorResultSet[String](streamsListEntity, maxRows, rows) 399 | case e: TablesList => 400 | val rows = e.getTables.asScala.map(t => Seq( 401 | t.getName, 402 | t.getTopic, 403 | t.getFormat, 404 | t.getIsWindowed 405 | )).toIterator 406 | new IteratorResultSet[Any](tablesListEntity, maxRows, rows) 407 | case e: TopicDescription => 408 | val rows = Iterator(Seq( 409 | e.getName, 410 | e.getKafkaTopic, 411 | e.getFormat, 412 | e.getSchemaString 413 | )) 414 | new IteratorResultSet[String](topicDescriptionEntity, maxRows, rows) 415 | case e: TypeList => 416 | val rows = e.getTypes.asScala.map(t => Seq( 417 | t._1, 418 | t._2.getTypeName 419 | )).toIterator 420 | new IteratorResultSet[String](typesListEntity, maxRows, rows) 421 | }.getOrElse(new IteratorResultSet[Any](List.empty, maxRows, Iterator.empty)) 422 | } 423 | 424 | } 425 | -------------------------------------------------------------------------------- /src/main/scala/com/github/mmolimar/ksql/jdbc/resultset/ResultSet.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc.resultset 2 | 3 | import java.io.{InputStream, Reader} 4 | import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float => JFloat, Integer => JInt, Long => JLong, Short => JShort} 5 | import java.math.BigDecimal 6 | import java.net.URL 7 | import java.sql.{Array, Blob, Clob, Date, NClob, Ref, ResultSet, ResultSetMetaData, RowId, SQLWarning, SQLXML, Statement, Time, Timestamp} 8 | import java.util 9 | import java.util.Calendar 10 | 11 | import com.github.mmolimar.ksql.jdbc.Exceptions._ 12 | import com.github.mmolimar.ksql.jdbc._ 13 | 14 | import scala.reflect.ClassTag 15 | 16 | 17 | private[resultset] class ResultSetNotSupported extends ResultSet with WrapperNotSupported { 18 | 19 | override def getType: Int = throw NotSupported("getType") 20 | 21 | override def isBeforeFirst: Boolean = throw NotSupported("isBeforeFirst") 22 | 23 | override def next: Boolean = throw NotSupported("next") 24 | 25 | override def updateString(columnIndex: Int, x: String): Unit = throw NotSupported("updateString") 26 | 27 | override def updateString(columnLabel: String, x: String): Unit = throw NotSupported("updateString") 28 | 29 | override def getTimestamp(columnIndex: Int): Timestamp = throw NotSupported("getTimestamp") 30 | 31 | override def getTimestamp(columnLabel: String): Timestamp = throw NotSupported("getTimestamp") 32 | 33 | override def getTimestamp(columnIndex: Int, cal: Calendar): Timestamp = throw NotSupported("getTimestamp") 34 | 35 | override def getTimestamp(columnLabel: String, cal: Calendar): Timestamp = throw NotSupported("getTimestamp") 36 | 37 | override def updateNString(columnIndex: Int, nString: String): Unit = throw NotSupported("updateNString") 38 | 39 | override def updateNString(columnLabel: String, nString: String): Unit = throw NotSupported("updateNString") 40 | 41 | override def clearWarnings(): Unit = throw NotSupported("clearWarnings") 42 | 43 | override def updateTimestamp(columnIndex: Int, x: Timestamp): Unit = throw NotSupported("updateTimestamp") 44 | 45 | override def updateTimestamp(columnLabel: String, x: Timestamp): Unit = throw NotSupported("updateTimestamp") 46 | 47 | override def updateByte(columnIndex: Int, x: Byte): Unit = throw NotSupported("updateByte") 48 | 49 | override def updateByte(columnLabel: String, x: Byte): Unit = throw NotSupported("updateByte") 50 | 51 | override def updateBigDecimal(columnIndex: Int, x: BigDecimal): Unit = throw NotSupported("updateBigDecimal") 52 | 53 | override def updateBigDecimal(columnLabel: String, x: BigDecimal): Unit = throw NotSupported("updateBigDecimal") 54 | 55 | override def updateDouble(columnIndex: Int, x: Double): Unit = throw NotSupported("updateDouble") 56 | 57 | override def updateDouble(columnLabel: String, x: Double): Unit = throw NotSupported("updateDouble") 58 | 59 | override def updateDate(columnIndex: Int, x: Date): Unit = throw NotSupported("updateDate") 60 | 61 | override def updateDate(columnLabel: String, x: Date): Unit = throw NotSupported("updateDate") 62 | 63 | override def isAfterLast: Boolean = throw NotSupported("isAfterLast") 64 | 65 | override def updateBoolean(columnIndex: Int, x: Boolean): Unit = throw NotSupported("updateBoolean") 66 | 67 | override def updateBoolean(columnLabel: String, x: Boolean): Unit = throw NotSupported("updateBoolean") 68 | 69 | override def getBinaryStream(columnIndex: Int): InputStream = throw NotSupported("getBinaryStream") 70 | 71 | override def getBinaryStream(columnLabel: String): InputStream = throw NotSupported("getBinaryStream") 72 | 73 | override def beforeFirst(): Unit = throw NotSupported("beforeFirst") 74 | 75 | override def updateNCharacterStream(columnIndex: Int, x: Reader, length: Long): Unit = 76 | throw NotSupported("updateNCharacterStream") 77 | 78 | override def updateNCharacterStream(columnLabel: String, reader: Reader, length: Long): Unit = 79 | throw NotSupported("updateNCharacterStream") 80 | 81 | override def updateNCharacterStream(columnIndex: Int, x: Reader): Unit = 82 | throw NotSupported("updateNCharacterStream") 83 | 84 | override def updateNCharacterStream(columnLabel: String, reader: Reader): Unit = 85 | throw NotSupported("updateNCharacterStream") 86 | 87 | override def updateNClob(columnIndex: Int, nClob: NClob): Unit = throw NotSupported("updateNClob") 88 | 89 | override def updateNClob(columnLabel: String, nClob: NClob): Unit = throw NotSupported("updateNClob") 90 | 91 | override def updateNClob(columnIndex: Int, reader: Reader, length: Long): Unit = throw NotSupported("updateNClob") 92 | 93 | override def updateNClob(columnLabel: String, reader: Reader, length: Long): Unit = throw NotSupported("updateNClob") 94 | 95 | override def updateNClob(columnIndex: Int, reader: Reader): Unit = throw NotSupported("updateNClob") 96 | 97 | override def updateNClob(columnLabel: String, reader: Reader): Unit = throw NotSupported("updateNClob") 98 | 99 | override def last: Boolean = throw NotSupported("last") 100 | 101 | override def isLast: Boolean = throw NotSupported("isLast") 102 | 103 | override def getNClob(columnIndex: Int): NClob = throw NotSupported("getNClob") 104 | 105 | override def getNClob(columnLabel: String): NClob = throw NotSupported("getNClob") 106 | 107 | override def getCharacterStream(columnIndex: Int): Reader = throw NotSupported("getCharacterStream") 108 | 109 | override def getCharacterStream(columnLabel: String): Reader = throw NotSupported("getCharacterStream") 110 | 111 | override def updateArray(columnIndex: Int, x: Array): Unit = throw NotSupported("updateArray") 112 | 113 | override def updateArray(columnLabel: String, x: Array): Unit = throw NotSupported("updateArray") 114 | 115 | override def updateBlob(columnIndex: Int, x: Blob): Unit = throw NotSupported("updateBlob") 116 | 117 | override def updateBlob(columnLabel: String, x: Blob): Unit = throw NotSupported("updateBlob") 118 | 119 | override def updateBlob(columnIndex: Int, inputStream: InputStream, length: Long): Unit = throw NotSupported("updateBlob") 120 | 121 | override def updateBlob(columnLabel: String, inputStream: InputStream, length: Long): Unit = throw NotSupported("updateBlob") 122 | 123 | override def updateBlob(columnIndex: Int, inputStream: InputStream): Unit = throw NotSupported("updateBlob") 124 | 125 | override def updateBlob(columnLabel: String, inputStream: InputStream): Unit = throw NotSupported("updateBlob") 126 | 127 | override def getDouble(columnIndex: Int): Double = throw NotSupported("getDouble") 128 | 129 | override def getDouble(columnLabel: String): Double = throw NotSupported("getDouble") 130 | 131 | override def getArray(columnIndex: Int): Array = throw NotSupported("getArray") 132 | 133 | override def getArray(columnLabel: String): Array = throw NotSupported("getArray") 134 | 135 | override def isFirst: Boolean = throw NotSupported("isFirst") 136 | 137 | override def getURL(columnIndex: Int): URL = throw NotSupported("getURL") 138 | 139 | override def getURL(columnLabel: String): URL = throw NotSupported("getURL") 140 | 141 | override def updateRow(): Unit = throw NotSupported("updateRow") 142 | 143 | override def insertRow(): Unit = throw NotSupported("insertRow") 144 | 145 | override def getMetaData: ResultSetMetaData = throw NotSupported("getMetaData") 146 | 147 | override def updateBinaryStream(columnIndex: Int, x: InputStream, length: Int): Unit = throw NotSupported("updateBinaryStream") 148 | 149 | override def updateBinaryStream(columnLabel: String, x: InputStream, length: Int): Unit = throw NotSupported("updateBinaryStream") 150 | 151 | override def updateBinaryStream(columnIndex: Int, x: InputStream, length: Long): Unit = throw NotSupported("updateBinaryStream") 152 | 153 | override def updateBinaryStream(columnLabel: String, x: InputStream, length: Long): Unit = throw NotSupported("updateBinaryStream") 154 | 155 | override def updateBinaryStream(columnIndex: Int, x: InputStream): Unit = throw NotSupported("updateBinaryStream") 156 | 157 | override def updateBinaryStream(columnLabel: String, x: InputStream): Unit = throw NotSupported("updateBinaryStream") 158 | 159 | override def absolute(row: Int): Boolean = throw NotSupported("absolute") 160 | 161 | override def updateRowId(columnIndex: Int, x: RowId): Unit = throw NotSupported("updateRowId") 162 | 163 | override def updateRowId(columnLabel: String, x: RowId): Unit = throw NotSupported("updateRowId") 164 | 165 | override def getRowId(columnIndex: Int): RowId = throw NotSupported("getRowId") 166 | 167 | override def getRowId(columnLabel: String): RowId = throw NotSupported("getRowId") 168 | 169 | override def moveToInsertRow(): Unit = throw NotSupported("moveToInsertRow") 170 | 171 | override def rowInserted: Boolean = throw NotSupported("rowInserted") 172 | 173 | override def getFloat(columnIndex: Int): Float = throw NotSupported("getFloat") 174 | 175 | override def getFloat(columnLabel: String): Float = throw NotSupported("getFloat") 176 | 177 | override def getBigDecimal(columnIndex: Int, scale: Int): BigDecimal = throw NotSupported("getBigDecimal") 178 | 179 | override def getBigDecimal(columnLabel: String, scale: Int): BigDecimal = throw NotSupported("getBigDecimal") 180 | 181 | override def getBigDecimal(columnIndex: Int): BigDecimal = throw NotSupported("getBigDecimal") 182 | 183 | override def getBigDecimal(columnLabel: String): BigDecimal = throw NotSupported("getBigDecimal") 184 | 185 | override def getClob(columnIndex: Int): Clob = throw NotSupported("getClob") 186 | 187 | override def getClob(columnLabel: String): Clob = throw NotSupported("getClob") 188 | 189 | override def getRow: Int = throw NotSupported("getRow") 190 | 191 | override def getLong(columnIndex: Int): Long = throw NotSupported("getLong") 192 | 193 | override def getLong(columnLabel: String): Long = throw NotSupported("getLong") 194 | 195 | override def getHoldability: Int = throw NotSupported("getHoldability") 196 | 197 | override def updateFloat(columnIndex: Int, x: Float): Unit = throw NotSupported("updateFloat") 198 | 199 | override def updateFloat(columnLabel: String, x: Float): Unit = throw NotSupported("updateFloat") 200 | 201 | override def afterLast(): Unit = throw NotSupported("afterLast") 202 | 203 | override def refreshRow(): Unit = throw NotSupported("refreshRow") 204 | 205 | override def getNString(columnIndex: Int): String = throw NotSupported("getNString") 206 | 207 | override def getNString(columnLabel: String): String = throw NotSupported("getNString") 208 | 209 | override def deleteRow(): Unit = throw NotSupported("deleteRow") 210 | 211 | override def getConcurrency: Int = throw NotSupported("getConcurrency") 212 | 213 | override def updateObject(columnIndex: Int, x: scala.Any, scaleOrLength: Int): Unit = throw NotSupported("updateObject") 214 | 215 | override def updateObject(columnIndex: Int, x: scala.Any): Unit = throw NotSupported("updateObject") 216 | 217 | override def updateObject(columnLabel: String, x: scala.Any, scaleOrLength: Int): Unit = throw NotSupported("updateObject") 218 | 219 | override def updateObject(columnLabel: String, x: scala.Any): Unit = throw NotSupported("updateObject") 220 | 221 | override def getFetchSize: Int = throw NotSupported("getFetchSize") 222 | 223 | override def getTime(columnIndex: Int): Time = throw NotSupported("getTime") 224 | 225 | override def getTime(columnLabel: String): Time = throw NotSupported("getTime") 226 | 227 | override def getTime(columnIndex: Int, cal: Calendar): Time = throw NotSupported("getTime") 228 | 229 | override def getTime(columnLabel: String, cal: Calendar): Time = throw NotSupported("getTime") 230 | 231 | override def updateCharacterStream(columnIndex: Int, x: Reader, length: Int): Unit = throw NotSupported("updateCharacterStream") 232 | 233 | override def updateCharacterStream(columnLabel: String, reader: Reader, length: Int): Unit = throw NotSupported("updateCharacterStream") 234 | 235 | override def updateCharacterStream(columnIndex: Int, x: Reader, length: Long): Unit = throw NotSupported("updateCharacterStream") 236 | 237 | override def updateCharacterStream(columnLabel: String, reader: Reader, length: Long): Unit = throw NotSupported("updateCharacterStream") 238 | 239 | override def updateCharacterStream(columnIndex: Int, x: Reader): Unit = throw NotSupported("updateCharacterStream") 240 | 241 | override def updateCharacterStream(columnLabel: String, reader: Reader): Unit = throw NotSupported("updateCharacterStream") 242 | 243 | override def getByte(columnIndex: Int): Byte = throw NotSupported("getByte") 244 | 245 | override def getByte(columnLabel: String): Byte = throw NotSupported("getByte") 246 | 247 | override def getBoolean(columnIndex: Int): Boolean = throw NotSupported("getBoolean") 248 | 249 | override def getBoolean(columnLabel: String): Boolean = throw NotSupported("getBoolean") 250 | 251 | override def setFetchDirection(direction: Int): Unit = throw NotSupported("setFetchDirection") 252 | 253 | override def getFetchDirection: Int = throw NotSupported("getFetchDirection") 254 | 255 | override def updateRef(columnIndex: Int, x: Ref): Unit = throw NotSupported("updateRef") 256 | 257 | override def updateRef(columnLabel: String, x: Ref): Unit = throw NotSupported("updateRef") 258 | 259 | override def getAsciiStream(columnIndex: Int): InputStream = throw NotSupported("getAsciiStream") 260 | 261 | override def getAsciiStream(columnLabel: String): InputStream = throw NotSupported("getAsciiStream") 262 | 263 | override def getShort(columnIndex: Int): Short = throw NotSupported("getShort") 264 | 265 | override def getShort(columnLabel: String): Short = throw NotSupported("getShort") 266 | 267 | override def getObject(columnIndex: Int): AnyRef = throw NotSupported("getObject") 268 | 269 | override def getObject(columnLabel: String): AnyRef = throw NotSupported("getObject") 270 | 271 | override def getObject(columnIndex: Int, map: util.Map[String, Class[_]]): AnyRef = throw NotSupported("getObject") 272 | 273 | override def getObject(columnLabel: String, map: util.Map[String, Class[_]]): AnyRef = throw NotSupported("getObject") 274 | 275 | override def getObject[T](columnIndex: Int, `type`: Class[T]): T = throw NotSupported("getObject") 276 | 277 | override def getObject[T](columnLabel: String, `type`: Class[T]): T = throw NotSupported("getObject") 278 | 279 | override def updateShort(columnIndex: Int, x: Short): Unit = throw NotSupported("updateShort") 280 | 281 | override def updateShort(columnLabel: String, x: Short): Unit = throw NotSupported("updateShort") 282 | 283 | override def getNCharacterStream(columnIndex: Int): Reader = throw NotSupported("getNCharacterStream") 284 | 285 | override def getNCharacterStream(columnLabel: String): Reader = throw NotSupported("getNCharacterStream") 286 | 287 | override def close(): Unit = throw NotSupported("close") 288 | 289 | override def relative(rows: Int): Boolean = throw NotSupported("relative") 290 | 291 | override def updateInt(columnIndex: Int, x: Int): Unit = throw NotSupported("updateInt") 292 | 293 | override def updateInt(columnLabel: String, x: Int): Unit = throw NotSupported("updateInt") 294 | 295 | override def wasNull: Boolean = throw NotSupported("wasNull") 296 | 297 | override def rowUpdated: Boolean = throw NotSupported("rowUpdated") 298 | 299 | override def getRef(columnIndex: Int): Ref = throw NotSupported("getRef") 300 | 301 | override def getRef(columnLabel: String): Ref = throw NotSupported("getRef") 302 | 303 | override def updateLong(columnIndex: Int, x: Long): Unit = throw NotSupported("updateLong") 304 | 305 | override def updateLong(columnLabel: String, x: Long): Unit = throw NotSupported("updateLong") 306 | 307 | override def moveToCurrentRow(): Unit = throw NotSupported("moveToCurrentRow") 308 | 309 | override def isClosed: Boolean = throw NotSupported("isClosed") 310 | 311 | override def updateClob(columnIndex: Int, x: Clob): Unit = throw NotSupported("updateClob") 312 | 313 | override def updateClob(columnLabel: String, x: Clob): Unit = throw NotSupported("updateClob") 314 | 315 | override def updateClob(columnIndex: Int, reader: Reader, length: Long): Unit = throw NotSupported("updateClob") 316 | 317 | override def updateClob(columnLabel: String, reader: Reader, length: Long): Unit = throw NotSupported("updateClob") 318 | 319 | override def updateClob(columnIndex: Int, reader: Reader): Unit = throw NotSupported("updateClob") 320 | 321 | override def updateClob(columnLabel: String, reader: Reader): Unit = throw NotSupported("updateClob") 322 | 323 | override def findColumn(columnLabel: String): Int = throw NotSupported("findColumn") 324 | 325 | override def getWarnings: SQLWarning = throw NotSupported("getWarnings") 326 | 327 | override def getDate(columnIndex: Int): Date = throw NotSupported("getDate") 328 | 329 | override def getDate(columnLabel: String): Date = throw NotSupported("getDate") 330 | 331 | override def getDate(columnIndex: Int, cal: Calendar): Date = throw NotSupported("getDate") 332 | 333 | override def getDate(columnLabel: String, cal: Calendar): Date = throw NotSupported("getDate") 334 | 335 | override def getCursorName: String = throw NotSupported("getCursorName") 336 | 337 | override def updateNull(columnIndex: Int): Unit = throw NotSupported("updateNull") 338 | 339 | override def updateNull(columnLabel: String): Unit = throw NotSupported("updateNull") 340 | 341 | override def getStatement: Statement = throw NotSupported("getStatement") 342 | 343 | override def cancelRowUpdates(): Unit = throw NotSupported("cancelRowUpdates") 344 | 345 | override def getSQLXML(columnIndex: Int): SQLXML = throw NotSupported("getSQLXML") 346 | 347 | override def getSQLXML(columnLabel: String): SQLXML = throw NotSupported("getSQLXML") 348 | 349 | override def getUnicodeStream(columnIndex: Int): InputStream = throw NotSupported("getUnicodeStream") 350 | 351 | override def getUnicodeStream(columnLabel: String): InputStream = throw NotSupported("getUnicodeStream") 352 | 353 | override def getInt(columnIndex: Int): Int = throw NotSupported("getInt") 354 | 355 | override def getInt(columnLabel: String): Int = throw NotSupported("getInt") 356 | 357 | override def updateTime(columnIndex: Int, x: Time): Unit = throw NotSupported("updateTime") 358 | 359 | override def updateTime(columnLabel: String, x: Time): Unit = throw NotSupported("updateTime") 360 | 361 | override def setFetchSize(rows: Int): Unit = throw NotSupported("setFetchSize") 362 | 363 | override def previous: Boolean = throw NotSupported("previous") 364 | 365 | override def updateAsciiStream(columnIndex: Int, x: InputStream, length: Int): Unit = throw NotSupported("updateAsciiStream") 366 | 367 | override def updateAsciiStream(columnLabel: String, x: InputStream, length: Int): Unit = throw NotSupported("updateAsciiStream") 368 | 369 | override def updateAsciiStream(columnIndex: Int, x: InputStream, length: Long): Unit = throw NotSupported("updateAsciiStream") 370 | 371 | override def updateAsciiStream(columnLabel: String, x: InputStream, length: Long): Unit = throw NotSupported("updateAsciiStream") 372 | 373 | override def updateAsciiStream(columnIndex: Int, x: InputStream): Unit = throw NotSupported("updateAsciiStream") 374 | 375 | override def updateAsciiStream(columnLabel: String, x: InputStream): Unit = throw NotSupported("updateAsciiStream") 376 | 377 | override def rowDeleted: Boolean = throw NotSupported("rowDeleted") 378 | 379 | override def getBlob(columnIndex: Int): Blob = throw NotSupported("getBlob") 380 | 381 | override def getBlob(columnLabel: String): Blob = throw NotSupported("getBlob") 382 | 383 | override def first: Boolean = throw NotSupported("first") 384 | 385 | override def getBytes(columnIndex: Int): scala.Array[Byte] = throw NotSupported("getBytes") 386 | 387 | override def getBytes(columnLabel: String): scala.Array[Byte] = throw NotSupported("getBytes") 388 | 389 | override def updateBytes(columnIndex: Int, x: scala.Array[Byte]): Unit = throw NotSupported("updateBytes") 390 | 391 | override def updateBytes(columnLabel: String, x: scala.Array[Byte]): Unit = throw NotSupported("updateBytes") 392 | 393 | override def updateSQLXML(columnIndex: Int, xmlObject: SQLXML): Unit = throw NotSupported("updateSQLXML") 394 | 395 | override def updateSQLXML(columnLabel: String, xmlObject: SQLXML): Unit = throw NotSupported("updateSQLXML") 396 | 397 | override def getString(columnIndex: Int): String = throw NotSupported("getString") 398 | 399 | override def getString(columnLabel: String): String = throw NotSupported("getString") 400 | 401 | } 402 | 403 | private[resultset] abstract class AbstractResultSet[T](private val metadata: ResultSetMetaData, 404 | private val maxRows: Long, 405 | private val records: Iterator[T]) extends ResultSetNotSupported { 406 | 407 | private val indexByLabel: Map[String, Int] = (1 to metadata.getColumnCount) 408 | .map(index => metadata.getColumnLabel(index).toUpperCase -> index).toMap 409 | private var lastColumnNull = true 410 | private var rowCounter = 0 411 | 412 | protected var currentRow: Option[T] = None 413 | 414 | private var closed: Boolean = false 415 | 416 | def hasNext: Boolean = !closed && maxRows != 0 && rowCounter < maxRows && records.hasNext 417 | 418 | protected def nextResult: Boolean = records.hasNext match { 419 | case true => 420 | currentRow = Some(records.next) 421 | true 422 | case false => false 423 | } 424 | 425 | override final def next: Boolean = closed match { 426 | case true => throw ResultSetError("Result set is already closed.") 427 | case false if maxRows != 0 && rowCounter > maxRows => false 428 | case _ => 429 | val result = nextResult 430 | rowCounter += 1 431 | result 432 | } 433 | 434 | override final def close(): Unit = closed match { 435 | case true => // do nothing 436 | case false => 437 | currentRow = None 438 | closeInherit() 439 | closed = true 440 | } 441 | 442 | protected def closeInherit(): Unit 443 | 444 | override def getBoolean(columnIndex: Int): Boolean = getColumn[JBoolean](columnIndex) 445 | 446 | override def getBoolean(columnLabel: String): Boolean = getColumn[JBoolean](columnLabel) 447 | 448 | override def getByte(columnIndex: Int): Byte = getColumn[JByte](columnIndex) 449 | 450 | override def getByte(columnLabel: String): Byte = getColumn[JByte](columnLabel) 451 | 452 | override def getShort(columnIndex: Int): Short = getColumn[JShort](columnIndex) 453 | 454 | override def getShort(columnLabel: String): Short = getColumn[JShort](columnLabel) 455 | 456 | override def getInt(columnIndex: Int): Int = getColumn[JInt](columnIndex) 457 | 458 | override def getInt(columnLabel: String): Int = getColumn[JInt](columnLabel) 459 | 460 | override def getLong(columnIndex: Int): Long = getColumn[JLong](columnIndex) 461 | 462 | override def getLong(columnLabel: String): Long = getColumn[JLong](columnLabel) 463 | 464 | override def getFloat(columnIndex: Int): Float = getColumn[JFloat](columnIndex) 465 | 466 | override def getFloat(columnLabel: String): Float = getColumn[JFloat](columnLabel) 467 | 468 | override def getDouble(columnIndex: Int): Double = getColumn[JDouble](columnIndex) 469 | 470 | override def getDouble(columnLabel: String): Double = getColumn[JDouble](columnLabel) 471 | 472 | override def getBytes(columnIndex: Int): scala.Array[Byte] = getColumn[scala.Array[Byte]](columnIndex) 473 | 474 | override def getBytes(columnLabel: String): scala.Array[Byte] = getColumn[scala.Array[Byte]](columnLabel) 475 | 476 | override def getString(columnIndex: Int): String = getColumn[String](columnIndex) 477 | 478 | override def getString(columnLabel: String): String = getColumn[String](columnLabel) 479 | 480 | override def getObject(columnIndex: Int): AnyRef = getColumn[AnyRef](columnIndex) 481 | 482 | override def getObject(columnLabel: String): AnyRef = getColumn[AnyRef](columnLabel) 483 | 484 | override def getMetaData: ResultSetMetaData = metadata 485 | 486 | override def getWarnings: SQLWarning = None.orNull 487 | 488 | override def wasNull: Boolean = lastColumnNull 489 | 490 | private def getColumn[V <: AnyRef](columnLabel: String)(implicit ev: ClassTag[V]): V = { 491 | getColumn[V](getColumnIndex(columnLabel)) 492 | } 493 | 494 | private def getColumn[V <: AnyRef](columnIndex: Int)(implicit ev: ClassTag[V]): V = { 495 | checkRow(columnIndex) 496 | val result = inferValue[V](columnIndex) 497 | lastColumnNull = Option(result).forall(_ => false) 498 | result 499 | } 500 | 501 | private def checkRow(columnIndex: Int): Unit = { 502 | def checkIfEmpty(): Unit = if (isEmpty) throw EmptyRow() 503 | 504 | def checkColumnBounds(index: Int): Unit = { 505 | val (min, max) = getColumnBounds 506 | if (index < min || index > max) 507 | throw InvalidColumn(s"Column with index $index does not exist") 508 | } 509 | 510 | checkIfEmpty() 511 | checkColumnBounds(columnIndex) 512 | } 513 | 514 | private def inferValue[V <: AnyRef](columnIndex: Int)(implicit ev: ClassTag[V]): V = { 515 | val value = getValue[V](columnIndex) 516 | 517 | import ImplicitClasses._ 518 | ev.runtimeClass match { 519 | case Any_ if ev.runtimeClass == Option(value).map(_.getClass).getOrElse(classOf[Object]) => value 520 | case String_ => Option(value).map(_.toString).getOrElse(None.orNull) 521 | case JBoolean_ if value.isInstanceOf[String] => JBoolean.parseBoolean(value.asInstanceOf[String]) 522 | case JBoolean_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].intValue != 0 523 | case JShort_ if value.isInstanceOf[String] => JShort.parseShort(value.asInstanceOf[String]) 524 | case JShort_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].shortValue 525 | case JShort_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).shortValue 526 | case JInt_ if value.isInstanceOf[String] => JInt.parseInt(value.asInstanceOf[String]) 527 | case JInt_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].intValue 528 | case JInt_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).intValue 529 | case JLong_ if value.isInstanceOf[String] => JLong.parseLong(value.asInstanceOf[String]) 530 | case JLong_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].longValue 531 | case JLong_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).longValue 532 | case JDouble_ if value.isInstanceOf[String] => JDouble.parseDouble(value.asInstanceOf[String]) 533 | case JDouble_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].doubleValue 534 | case JDouble_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).doubleValue 535 | case JFloat_ if value.isInstanceOf[String] => JFloat.parseFloat(value.asInstanceOf[String]) 536 | case JFloat_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].floatValue 537 | case JFloat_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).floatValue 538 | case JByte_ if value.isInstanceOf[String] => value.asInstanceOf[String].toByte 539 | case JByte_ if value.isInstanceOf[Number] => value.asInstanceOf[Number].byteValue 540 | case JByte_ if value.isInstanceOf[JBoolean] => value.asInstanceOf[JBoolean].compareTo(false).byteValue 541 | case JByteArray_ if value.isInstanceOf[String] => value.asInstanceOf[String].getBytes 542 | case JByteArray_ if value.isInstanceOf[Number] => scala.Array[Byte](value.asInstanceOf[Number].byteValue) 543 | case JByteArray_ if value.isInstanceOf[JBoolean] => 544 | scala.Array[Byte](value.asInstanceOf[JBoolean].compareTo(false).byteValue) 545 | case _ => value 546 | } 547 | }.asInstanceOf[V] 548 | 549 | private object ImplicitClasses { 550 | val Any_ : Class[Any] = classOf[Any] 551 | val String_ : Class[String] = classOf[String] 552 | val JBoolean_ : Class[JBoolean] = classOf[JBoolean] 553 | val JShort_ : Class[JShort] = classOf[JShort] 554 | val JInt_ : Class[JInt] = classOf[JInt] 555 | val JLong_ : Class[JLong] = classOf[JLong] 556 | val JDouble_ : Class[JDouble] = classOf[JDouble] 557 | val JFloat_ : Class[JFloat] = classOf[JFloat] 558 | val JByte_ : Class[JByte] = classOf[JByte] 559 | val JByteArray_ : Class[scala.Array[Byte]] = classOf[scala.Array[Byte]] 560 | } 561 | 562 | protected def isEmpty: Boolean = currentRow.isEmpty 563 | 564 | protected def getColumnIndex(columnLabel: String): Int = { 565 | indexByLabel.getOrElse(columnLabel.toUpperCase, throw InvalidColumn()) 566 | } 567 | 568 | protected def getColumnBounds: (Int, Int) 569 | 570 | protected def getValue[V <: AnyRef](columnIndex: Int): V 571 | 572 | } 573 | -------------------------------------------------------------------------------- /src/it/scala/com/github/mmolimar/ksql/jdbc/KsqlDriverIntegrationTest.scala: -------------------------------------------------------------------------------- 1 | package com.github.mmolimar.ksql.jdbc 2 | 3 | import java.sql.{Connection, DriverManager, ResultSet, SQLException, Types} 4 | import java.util.Properties 5 | import java.util.concurrent.TimeUnit 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | 8 | import com.github.mmolimar.ksql.jdbc.KsqlEntityHeaders._ 9 | import com.github.mmolimar.ksql.jdbc.embedded._ 10 | import com.github.mmolimar.ksql.jdbc.utils.TestUtils 11 | import io.confluent.ksql.util.KsqlConstants.ESCAPE 12 | import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord} 13 | import org.scalatest.BeforeAndAfterAll 14 | import org.scalatest.matchers.should.Matchers 15 | import org.scalatest.wordspec.AnyWordSpec 16 | 17 | class KsqlDriverIntegrationTest extends AnyWordSpec with Matchers with BeforeAndAfterAll { 18 | 19 | val zkServer = new EmbeddedZookeeperServer 20 | val kafkaCluster = new EmbeddedKafkaCluster(zkConnection = zkServer.getConnection) 21 | val kafkaConnect = new EmbeddedKafkaConnect(brokerList = kafkaCluster.getBrokerList) 22 | val ksqlEngine = new EmbeddedKsqlEngine(brokerList = kafkaCluster.getBrokerList, connectUrl = kafkaConnect.getUrl) 23 | 24 | lazy val kafkaProducer: KafkaProducer[Array[Byte], Array[Byte]] = TestUtils.buildProducer(kafkaCluster.getBrokerList) 25 | 26 | val ksqlUrl = s"jdbc:ksql://localhost:${ksqlEngine.getPort}?timeout=20000" 27 | var ksqlConnection: Connection = _ 28 | val topic: String = "test-topic" 29 | 30 | val stop = new AtomicBoolean(false) 31 | val producerThread = new BackgroundOps(stop, () => produceMessages()) 32 | 33 | "A KsqlConnection" when { 34 | 35 | "managing a TABLE" should { 36 | 37 | val maxRecords = 5 38 | val table = "TEST_TABLE" 39 | 40 | "create the table properly" in { 41 | val resultSet = createTestTableOrStream(table) 42 | resultSet.next should be(true) 43 | resultSet.getLong(commandStatusEntity.head.name) should be(0L) 44 | resultSet.getString(commandStatusEntity(1).name) should be("TABLE") 45 | resultSet.getString(commandStatusEntity(2).name) should be(table.toUpperCase.escape) 46 | resultSet.getString(commandStatusEntity(3).name) should be("CREATE") 47 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 48 | resultSet.getString(commandStatusEntity(5).name) should be("Table created") 49 | resultSet.next should be(false) 50 | resultSet.close() 51 | } 52 | 53 | "insert records into the table" in { 54 | val resultSet = ksqlConnection.createStatement.executeQuery(s"INSERT INTO $table (FIELD1, FIELD2, FIELD3) VALUES (123, 45.4, 'lorem ipsum')") 55 | resultSet.next should be(false) 56 | resultSet.close() 57 | } 58 | 59 | "list the table already created" in { 60 | val resultSet = ksqlConnection.createStatement.executeQuery(s"SHOW TABLES") 61 | resultSet.next should be(true) 62 | resultSet.getString(tablesListEntity.head.name) should be(table.toUpperCase) 63 | resultSet.getString(tablesListEntity(1).name) should be(topic) 64 | resultSet.getString(tablesListEntity(2).name) should be("JSON") 65 | resultSet.getBoolean(tablesListEntity(3).name) should be(false) 66 | resultSet.next should be(false) 67 | resultSet.close() 68 | } 69 | 70 | "be able to get the execution plan for a query in a table" in { 71 | val resultSet = ksqlConnection.createStatement.executeQuery(s"EXPLAIN SELECT * FROM $table") 72 | resultSet.next should be(true) 73 | resultSet.getString(queryDescriptionEntity(1).name) should be("ROWKEY, FIELD1, FIELD2, FIELD3") 74 | resultSet.getString(queryDescriptionEntity(2).name) should be(table.toUpperCase) 75 | resultSet.next should be(false) 76 | resultSet.close() 77 | } 78 | 79 | "be able to query all fields in the table" in { 80 | var counter = 0 81 | val statement = ksqlConnection.createStatement 82 | statement.setMaxRows(maxRecords) 83 | statement.getMoreResults(1) should be(false) 84 | val resultSet = statement.executeQuery(s"SELECT * FROM $table EMIT CHANGES") 85 | statement.getMoreResults(1) should be(true) 86 | while (resultSet.next) { 87 | resultSet.getLong(1) should not be (-1) 88 | Option(resultSet.getString(2)) should not be None 89 | resultSet.getInt(3) should be(123) 90 | resultSet.getDouble(4) should be(45.4) 91 | resultSet.getString(5) should be("lorem ipsum") 92 | assertThrows[SQLException] { 93 | resultSet.getString(6) 94 | } 95 | counter += 1 96 | } 97 | counter should be(maxRecords) 98 | statement.getMoreResults() should be(false) 99 | 100 | resultSet.close() 101 | statement.close() 102 | 103 | val metadata = resultSet.getMetaData 104 | metadata.getColumnCount should be(5) 105 | metadata.getColumnName(1) should be("ROWTIME") 106 | metadata.getColumnName(2) should be("ROWKEY") 107 | metadata.getColumnName(3) should be("FIELD1") 108 | metadata.getColumnName(4) should be("FIELD2") 109 | metadata.getColumnName(5) should be("FIELD3") 110 | } 111 | 112 | "be able to query one field in the table and get its metadata" in { 113 | var counter = 0 114 | val resultSet = ksqlConnection.createStatement.executeQuery(s"SELECT FIELD3 FROM $table EMIT CHANGES LIMIT $maxRecords") 115 | while (resultSet.next) { 116 | resultSet.getString(1) should be("lorem ipsum") 117 | assertThrows[SQLException] { 118 | resultSet.getString(2) 119 | } 120 | counter += 1 121 | } 122 | counter should be(maxRecords) 123 | 124 | val metadata = resultSet.getMetaData 125 | metadata.getColumnCount should be(1) 126 | metadata.getColumnName(1) should be("FIELD3") 127 | 128 | } 129 | 130 | "be able to get the metadata for this table" in { 131 | var resultSet = ksqlConnection.getMetaData.getTables("", "", table, TableTypes.tableTypes.map(_.name).toArray) 132 | while (resultSet.next) { 133 | resultSet.getString("TABLE_NAME") should be(table.toUpperCase) 134 | resultSet.getString("TABLE_TYPE") should be(TableTypes.TABLE.name) 135 | resultSet.getString("TYPE_SCHEM") should be("JSON") 136 | resultSet.getString("REMARKS") should be(s"Topic: $topic. Windowed: false") 137 | assertThrows[SQLException] { 138 | resultSet.getString("UNKNOWN") 139 | } 140 | } 141 | 142 | resultSet = ksqlConnection.getMetaData.getColumns("", "", table, "") 143 | while (resultSet.next) { 144 | resultSet.getString("TABLE_NAME") should be(table.toUpperCase) 145 | assertThrows[SQLException] { 146 | resultSet.getString("UNKNOWN") 147 | } 148 | } 149 | 150 | resultSet = ksqlConnection.getMetaData.getColumns("", "", table, "FIELD2") 151 | while (resultSet.next) { 152 | resultSet.getString("TABLE_NAME") should be(table.toUpperCase) 153 | resultSet.getString("COLUMN_NAME") should be("FIELD2") 154 | resultSet.getInt("DATA_TYPE") should be(Types.DOUBLE) 155 | resultSet.getString("TYPE_NAME") should be("DOUBLE") 156 | resultSet.getString("IS_AUTOINCREMENT") should be("NO") 157 | resultSet.getString("IS_GENERATEDCOLUMN") should be("NO") 158 | assertThrows[SQLException] { 159 | resultSet.getString("UNKNOWN") 160 | } 161 | } 162 | 163 | resultSet = ksqlConnection.getMetaData.getColumns("", "", table, "_ID") 164 | while (resultSet.next) { 165 | resultSet.getString("TABLE_NAME") should be(table.toUpperCase) 166 | resultSet.getString("COLUMN_NAME") should be("_ID") 167 | resultSet.getInt("DATA_TYPE") should be(Types.BIGINT) 168 | resultSet.getString("TYPE_NAME") should be("BIGINT") 169 | resultSet.getString("IS_AUTOINCREMENT") should be("YES") 170 | resultSet.getString("IS_GENERATEDCOLUMN") should be("YES") 171 | assertThrows[SQLException] { 172 | resultSet.getString("UNKNOWN") 173 | } 174 | } 175 | } 176 | 177 | "describe the table" in { 178 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DESCRIBE $table") 179 | resultSet.next should be(true) 180 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 181 | resultSet.getString(sourceDescriptionEntity.head.name) should be("FIELD1") 182 | resultSet.getString(sourceDescriptionEntity(1).name) should be(table.toUpperCase) 183 | resultSet.getString(sourceDescriptionEntity(2).name) should be("TABLE") 184 | resultSet.getString(sourceDescriptionEntity(3).name) should be(topic) 185 | resultSet.getString(sourceDescriptionEntity(4).name) should be("JSON") 186 | resultSet.next should be(false) 187 | resultSet.close() 188 | } 189 | 190 | "describe extended the table" in { 191 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DESCRIBE EXTENDED $table") 192 | resultSet.next should be(true) 193 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 194 | resultSet.getString(sourceDescriptionEntity.head.name) should be("FIELD1") 195 | resultSet.getString(sourceDescriptionEntity(1).name) should be(table.toUpperCase) 196 | resultSet.getString(sourceDescriptionEntity(2).name) should be("TABLE") 197 | resultSet.getString(sourceDescriptionEntity(3).name) should be(topic) 198 | resultSet.getString(sourceDescriptionEntity(4).name) should be("JSON") 199 | resultSet.next should be(false) 200 | resultSet.close() 201 | } 202 | 203 | "drop the table" in { 204 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DROP TABLE $table") 205 | resultSet.next should be(true) 206 | resultSet.getLong(commandStatusEntity.head.name) should be(1L) 207 | resultSet.getString(commandStatusEntity(1).name) should be("TABLE") 208 | resultSet.getString(commandStatusEntity(2).name) should be(table.toUpperCase) 209 | resultSet.getString(commandStatusEntity(3).name) should be("DROP") 210 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 211 | resultSet.getString(commandStatusEntity(5).name) should be(s"Source ${table.toUpperCase.escape} (topic: $topic) was dropped.") 212 | resultSet.next should be(false) 213 | resultSet.close() 214 | } 215 | } 216 | 217 | "managing a STREAM" should { 218 | 219 | val maxRecords = 5 220 | val stream = "TEST_STREAM" 221 | 222 | "create the stream properly" in { 223 | val resultSet = createTestTableOrStream(str = stream, isStream = true) 224 | resultSet.next should be(true) 225 | resultSet.getLong(commandStatusEntity.head.name) should be(2L) 226 | resultSet.getString(commandStatusEntity(1).name) should be("STREAM") 227 | resultSet.getString(commandStatusEntity(2).name) should be(stream.toUpperCase.escape) 228 | resultSet.getString(commandStatusEntity(3).name) should be("CREATE") 229 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 230 | resultSet.getString(commandStatusEntity(5).name) should be("Stream created") 231 | resultSet.next should be(false) 232 | resultSet.close() 233 | } 234 | 235 | "insert records into the stream" in { 236 | val resultSet = ksqlConnection.createStatement.executeQuery(s"INSERT INTO $stream (FIELD1, FIELD2, FIELD3) VALUES (123, 45.4, 'lorem ipsum')") 237 | resultSet.next should be(false) 238 | resultSet.close() 239 | } 240 | 241 | "list the stream already created" in { 242 | val resultSet = ksqlConnection.createStatement.executeQuery(s"SHOW STREAMS") 243 | resultSet.next should be(true) 244 | resultSet.getString(streamsListEntity.head.name) should be(stream.toUpperCase) 245 | resultSet.getString(streamsListEntity(1).name) should be(topic) 246 | resultSet.getString(streamsListEntity(2).name) should be("JSON") 247 | resultSet.next should be(false) 248 | resultSet.close() 249 | } 250 | 251 | "be able to get the execution plan for a query in a stream" in { 252 | val resultSet = ksqlConnection.createStatement.executeQuery(s"EXPLAIN SELECT * FROM $stream") 253 | resultSet.next should be(true) 254 | resultSet.getString(queryDescriptionEntity(1).name) should be("ROWKEY, FIELD1, FIELD2, FIELD3") 255 | resultSet.getString(queryDescriptionEntity(2).name) should be(stream.toUpperCase) 256 | resultSet.next should be(false) 257 | resultSet.close() 258 | } 259 | 260 | "be able to query all fields in the stream" in { 261 | var counter = 0 262 | val statement = ksqlConnection.createStatement 263 | statement.setMaxRows(maxRecords) 264 | statement.getMoreResults(1) should be(false) 265 | val resultSet = statement.executeQuery(s"SELECT * FROM $stream EMIT CHANGES") 266 | statement.getMoreResults(1) should be(true) 267 | while (resultSet.next) { 268 | resultSet.getLong(1) should not be (-1) 269 | Option(resultSet.getString(2)) should not be None 270 | resultSet.getInt(3) should be(123) 271 | resultSet.getDouble(4) should be(45.4) 272 | resultSet.getString(5) should be("lorem ipsum") 273 | assertThrows[SQLException] { 274 | resultSet.getString(6) 275 | } 276 | counter += 1 277 | } 278 | counter should be(maxRecords) 279 | statement.getMoreResults(1) should be(false) 280 | 281 | resultSet.close() 282 | statement.close() 283 | 284 | val metadata = resultSet.getMetaData 285 | metadata.getColumnCount should be(5) 286 | metadata.getColumnName(1) should be("ROWTIME") 287 | metadata.getColumnName(2) should be("ROWKEY") 288 | metadata.getColumnName(3) should be("FIELD1") 289 | metadata.getColumnName(4) should be("FIELD2") 290 | metadata.getColumnName(5) should be("FIELD3") 291 | } 292 | 293 | "be able to query one field in the stream" in { 294 | var counter = 0 295 | val resultSet = ksqlConnection.createStatement.executeQuery(s"SELECT FIELD3 FROM $stream EMIT CHANGES LIMIT $maxRecords") 296 | while (resultSet.next) { 297 | resultSet.getString(1) should be("lorem ipsum") 298 | assertThrows[SQLException] { 299 | resultSet.getString(2) 300 | } 301 | counter += 1 302 | } 303 | counter should be(maxRecords) 304 | 305 | val metadata = resultSet.getMetaData 306 | metadata.getColumnCount should be(1) 307 | metadata.getColumnName(1) should be("FIELD3") 308 | } 309 | 310 | "be able to get the metadata for this stream" in { 311 | var resultSet = ksqlConnection.getMetaData.getTables("", "", stream, TableTypes.tableTypes.map(_.name).toArray) 312 | while (resultSet.next) { 313 | resultSet.getString("TABLE_NAME") should be(stream.toUpperCase) 314 | resultSet.getString("TABLE_TYPE") should be(TableTypes.STREAM.name) 315 | resultSet.getString("TYPE_SCHEM") should be("JSON") 316 | resultSet.getString("REMARKS") should be(s"Topic: $topic") 317 | assertThrows[SQLException] { 318 | resultSet.getString("UNKNOWN") 319 | } 320 | } 321 | 322 | resultSet = ksqlConnection.getMetaData.getColumns("", "", stream, "") 323 | while (resultSet.next) { 324 | resultSet.getString("TABLE_NAME") should be(stream.toUpperCase) 325 | assertThrows[SQLException] { 326 | resultSet.getString("UNKNOWN") 327 | } 328 | } 329 | 330 | resultSet = ksqlConnection.getMetaData.getColumns("", "", stream, "FIELD2") 331 | while (resultSet.next) { 332 | resultSet.getString("TABLE_NAME") should be(stream.toUpperCase) 333 | resultSet.getString("COLUMN_NAME") should be("FIELD2") 334 | resultSet.getInt("DATA_TYPE") should be(Types.DOUBLE) 335 | resultSet.getString("TYPE_NAME") should be("DOUBLE") 336 | resultSet.getString("IS_AUTOINCREMENT") should be("NO") 337 | resultSet.getString("IS_GENERATEDCOLUMN") should be("NO") 338 | assertThrows[SQLException] { 339 | resultSet.getString("UNKNOWN") 340 | } 341 | } 342 | 343 | resultSet = ksqlConnection.getMetaData.getColumns("", "", stream, "_ID") 344 | while (resultSet.next) { 345 | resultSet.getString("TABLE_NAME") should be(stream.toUpperCase) 346 | resultSet.getString("COLUMN_NAME") should be("_ID") 347 | resultSet.getInt("DATA_TYPE") should be(Types.BIGINT) 348 | resultSet.getString("TYPE_NAME") should be("BIGINT") 349 | resultSet.getString("IS_AUTOINCREMENT") should be("YES") 350 | resultSet.getString("IS_GENERATEDCOLUMN") should be("YES") 351 | assertThrows[SQLException] { 352 | resultSet.getString("UNKNOWN") 353 | } 354 | } 355 | } 356 | 357 | "describe the stream" in { 358 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DESCRIBE $stream") 359 | resultSet.next should be(true) 360 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 361 | resultSet.getString(sourceDescriptionEntity.head.name) should be("FIELD1") 362 | resultSet.getString(sourceDescriptionEntity(1).name) should be(stream.toUpperCase) 363 | resultSet.getString(sourceDescriptionEntity(2).name) should be("STREAM") 364 | resultSet.getString(sourceDescriptionEntity(3).name) should be(topic) 365 | resultSet.getString(sourceDescriptionEntity(4).name) should be("JSON") 366 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 367 | resultSet.next should be(false) 368 | resultSet.close() 369 | } 370 | 371 | "describe extended the stream" in { 372 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DESCRIBE EXTENDED $stream") 373 | resultSet.next should be(true) 374 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 375 | resultSet.getString(sourceDescriptionEntity.head.name) should be("FIELD1") 376 | resultSet.getString(sourceDescriptionEntity(1).name) should be(stream.toUpperCase) 377 | resultSet.getString(sourceDescriptionEntity(2).name) should be("STREAM") 378 | resultSet.getString(sourceDescriptionEntity(3).name) should be(topic) 379 | resultSet.getString(sourceDescriptionEntity(4).name) should be("JSON") 380 | resultSet.getMetaData.getColumnCount should be(sourceDescriptionEntity.size) 381 | resultSet.next should be(false) 382 | resultSet.close() 383 | } 384 | 385 | "drop the stream" in { 386 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DROP STREAM $stream") 387 | resultSet.next should be(true) 388 | resultSet.getLong(commandStatusEntity.head.name) should be(3L) 389 | resultSet.getString(commandStatusEntity(1).name) should be("STREAM") 390 | resultSet.getString(commandStatusEntity(2).name) should be(stream.toUpperCase) 391 | resultSet.getString(commandStatusEntity(3).name) should be("DROP") 392 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 393 | resultSet.getString(commandStatusEntity(5).name) should be(s"Source ${stream.toUpperCase.escape} (topic: $topic) was dropped.") 394 | resultSet.next should be(false) 395 | resultSet.close() 396 | } 397 | } 398 | 399 | "managing a CONNECTOR" should { 400 | 401 | val connectorName = TestUtils.randomString() 402 | val connectorClass = "org.apache.kafka.connect.tools.MockSourceConnector" 403 | 404 | "create the connector properly" in { 405 | val resultSet = ksqlConnection.createStatement.executeQuery(s"CREATE SOURCE CONNECTOR `$connectorName` " + 406 | s"""WITH("connector.class"='$connectorClass')""") 407 | resultSet.next should be(true) 408 | resultSet.getString(createConnectorEntity.head.name) should be(connectorName) 409 | resultSet.getString(createConnectorEntity(1).name) should be("SOURCE") 410 | resultSet.getString(createConnectorEntity(2).name) should be(s"[0]-$connectorName") 411 | resultSet.getString(createConnectorEntity(3).name) should be(s"connector.class -> $connectorClass\n" + 412 | s"name -> $connectorName") 413 | resultSet.next should be(false) 414 | resultSet.close() 415 | } 416 | 417 | "describe the connector" in { 418 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DESCRIBE CONNECTOR `$connectorName`") 419 | resultSet.next should be(true) 420 | resultSet.getString(connectorDescriptionEntity.head.name) should be(connectorClass) 421 | resultSet.getString(connectorDescriptionEntity(1).name) should be(connectorName) 422 | resultSet.getString(connectorDescriptionEntity(2).name) should be("SOURCE") 423 | resultSet.getString(connectorDescriptionEntity(3).name) should be("RUNNING") 424 | resultSet.getString(connectorDescriptionEntity(4).name) should be(None.orNull) 425 | resultSet.getString(connectorDescriptionEntity(5).name) should be(kafkaConnect.getWorker) 426 | resultSet.getString(connectorDescriptionEntity(6).name) should be(s"0-RUNNING-${kafkaConnect.getWorker}: ") 427 | resultSet.next should be(false) 428 | resultSet.close() 429 | } 430 | 431 | "list all connectors" in { 432 | val resultSet = ksqlConnection.createStatement.executeQuery(s"SHOW CONNECTORS") 433 | resultSet.next should be(true) 434 | resultSet.getString(connectorListEntity.head.name) should be(connectorName) 435 | resultSet.getString(connectorListEntity(1).name) should be("SOURCE") 436 | resultSet.getString(connectorListEntity(2).name) should be(connectorClass) 437 | resultSet.next should be(false) 438 | resultSet.close() 439 | } 440 | 441 | "drop the connector" in { 442 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DROP CONNECTOR `$connectorName`") 443 | resultSet.next should be(true) 444 | resultSet.getString(dropConnectorEntity.head.name) should be(connectorName) 445 | resultSet.next should be(false) 446 | resultSet.close() 447 | } 448 | } 449 | 450 | "managing a TYPE" should { 451 | 452 | val testType = "TEST" 453 | 454 | "create a specified type" in { 455 | val resultSet = ksqlConnection.createStatement.executeQuery(s"CREATE TYPE $testType AS STRUCT") 456 | resultSet.next should be(true) 457 | resultSet.getLong(commandStatusEntity.head.name) should be(4L) 458 | resultSet.getString(commandStatusEntity(1).name) should be("TYPE") 459 | resultSet.getString(commandStatusEntity(2).name) should be(testType) 460 | resultSet.getString(commandStatusEntity(3).name) should be("CREATE") 461 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 462 | resultSet.getString(commandStatusEntity(5).name) should be(s"Registered custom type with name '$testType' and SQL type STRUCT<`F1` STRING, `F2` STRING>") 463 | resultSet.next should be(false) 464 | resultSet.close() 465 | } 466 | 467 | "list all defined types" in { 468 | val resultSet = ksqlConnection.createStatement.executeQuery("SHOW TYPES") 469 | resultSet.next should be(true) 470 | resultSet.getString(typesListEntity.head.name) should be(testType) 471 | resultSet.getString(typesListEntity(1).name) should be("STRUCT") 472 | resultSet.next should be(false) 473 | resultSet.close() 474 | } 475 | 476 | "drop the type" in { 477 | val resultSet = ksqlConnection.createStatement.executeQuery(s"DROP TYPE $testType") 478 | resultSet.next should be(true) 479 | resultSet.getLong(commandStatusEntity.head.name) should be(5L) 480 | resultSet.getString(commandStatusEntity(1).name) should be("TYPE") 481 | resultSet.getString(commandStatusEntity(2).name) should be(testType) 482 | resultSet.getString(commandStatusEntity(3).name) should be("DROP") 483 | resultSet.getString(commandStatusEntity(4).name) should be("SUCCESS") 484 | resultSet.getString(commandStatusEntity(5).name) should be(s"Dropped type '$testType'") 485 | resultSet.next should be(false) 486 | resultSet.close() 487 | } 488 | 489 | } 490 | 491 | "getting info from topics" should { 492 | 493 | "print a topic" in { 494 | val statement = ksqlConnection.createStatement 495 | statement.setMaxRows(3) 496 | statement.getMoreResults(1) should be(false) 497 | val resultSet = statement.executeQuery(s"PRINT '$topic'") 498 | statement.getMoreResults(1) should be(true) 499 | resultSet.next should be(true) 500 | resultSet.getString(printTopic.head.name) should be("Format:STRING") 501 | resultSet.next should be(true) 502 | resultSet.next should be(true) 503 | resultSet.next should be(true) 504 | resultSet.next should be(false) 505 | statement.getMoreResults() should be(false) 506 | resultSet.close() 507 | statement.close() 508 | } 509 | 510 | "list topics" in { 511 | val resultSet = ksqlConnection.createStatement.executeQuery("SHOW TOPICS") 512 | resultSet.next should be(true) 513 | resultSet.getString(kafkaTopicsListEntity.head.name) should be(topic) 514 | resultSet.getString(kafkaTopicsListEntity(1).name) should be("1") 515 | resultSet.next should be(false) 516 | resultSet.close() 517 | } 518 | 519 | "list topics extended" in { 520 | val resultSet = ksqlConnection.createStatement.executeQuery("SHOW TOPICS EXTENDED") 521 | resultSet.next should be(true) 522 | resultSet.getString(kafkaTopicsListExtendedEntity.head.name) should be(topic) 523 | resultSet.getString(kafkaTopicsListExtendedEntity(1).name) should be("1") 524 | resultSet.getString(kafkaTopicsListExtendedEntity(2).name) should be("2") 525 | resultSet.getString(kafkaTopicsListExtendedEntity(3).name) should be("2") 526 | resultSet.next should be(false) 527 | resultSet.close() 528 | } 529 | } 530 | 531 | "setting properties" should { 532 | 533 | "set client info properties" in { 534 | val props = new Properties() 535 | props.setProperty("group.id", "test-group") 536 | props.setProperty("commit.interval.ms", "0") 537 | ksqlConnection.setClientInfo(props) 538 | 539 | assertThrows[SQLException] { 540 | ksqlConnection.setClientInfo("invalid.property", "test") 541 | } 542 | } 543 | 544 | "set a property" in { 545 | val resultSet = ksqlConnection.createStatement.executeQuery("SET 'group.id' = 'test-group'") 546 | resultSet.next should be(false) 547 | resultSet.close() 548 | } 549 | 550 | "unset a property" in { 551 | val resultSet = ksqlConnection.createStatement.executeQuery("UNSET 'group.id'") 552 | resultSet.next should be(false) 553 | resultSet.close() 554 | } 555 | 556 | "list properties" in { 557 | val resultSet = ksqlConnection.createStatement.executeQuery("SHOW PROPERTIES") 558 | resultSet.next should be(true) 559 | resultSet.getString(propertiesListEntity.head.name) should not be None.orNull 560 | resultSet.getString(propertiesListEntity(1).name) should not be None.orNull 561 | while(resultSet.next()) {} 562 | resultSet.close() 563 | } 564 | } 565 | 566 | "querying functions" should { 567 | 568 | "list functions" in { 569 | val resultSet = ksqlConnection.createStatement.executeQuery("SHOW FUNCTIONS") 570 | resultSet.next should be(true) 571 | resultSet.getString(functionNameListEntity.head.name) should not be None.orNull 572 | resultSet.getString(functionNameListEntity(1).name) should not be None.orNull 573 | while(resultSet.next()) {} 574 | resultSet.close() 575 | } 576 | 577 | "describe a function" in { 578 | val resultSet = ksqlConnection.createStatement.executeQuery("DESCRIBE FUNCTION UNIX_DATE") 579 | resultSet.next should be(true) 580 | resultSet.getString(functionDescriptionListEntity.head.name) should be("UNIX_DATE") 581 | resultSet.getString(functionDescriptionListEntity(1).name) should be("SCALAR") 582 | resultSet.getString(functionDescriptionListEntity(2).name) should be("Gets an integer representing days since epoch.") 583 | resultSet.getString(functionDescriptionListEntity(3).name) should be("internal") 584 | resultSet.getString(functionDescriptionListEntity(4).name) should be("") 585 | resultSet.getString(functionDescriptionListEntity(5).name) should be("") 586 | resultSet.getString(functionDescriptionListEntity(6).name) should be("Gets an integer representing days since epoch.") 587 | resultSet.getString(functionDescriptionListEntity(7).name) should be("INT") 588 | resultSet.getString(functionDescriptionListEntity(8).name) should be("") 589 | resultSet.next should be(false) 590 | resultSet.close() 591 | } 592 | } 593 | } 594 | 595 | private def produceMessages(): Unit = { 596 | val key = TestUtils.randomString().getBytes 597 | val value = 598 | """ 599 | |{ 600 | | "FIELD1": 123, 601 | | "FIELD2": 45.4, 602 | | "FIELD3": "lorem ipsum" 603 | |} 604 | """.stripMargin.getBytes 605 | val record = new ProducerRecord[Array[Byte], Array[Byte]](topic, key, value) 606 | kafkaProducer.send(record).get(10000, TimeUnit.MILLISECONDS) 607 | Thread.sleep(100) 608 | } 609 | 610 | private implicit class Escape(str: String) { 611 | def escape: String = s"$ESCAPE$str$ESCAPE" 612 | } 613 | 614 | private def createTestTableOrStream(str: String, isStream: Boolean = false): ResultSet = { 615 | ksqlConnection.createStatement.executeQuery(s"CREATE ${if (isStream) "STREAM" else "TABLE"} $str " + 616 | s"(FIELD1 INT, FIELD2 DOUBLE, FIELD3 VARCHAR) " + 617 | s"WITH (KAFKA_TOPIC='$topic', VALUE_FORMAT='JSON', KEY='FIELD1');") 618 | } 619 | 620 | override def beforeAll(): Unit = { 621 | DriverManager.registerDriver(new KsqlDriver) 622 | 623 | zkServer.startup() 624 | TestUtils.waitTillAvailable("localhost", zkServer.getPort, 5000) 625 | 626 | kafkaCluster.startup() 627 | kafkaCluster.getPorts.foreach { port => 628 | TestUtils.waitTillAvailable("localhost", port, 5000) 629 | } 630 | 631 | kafkaCluster.createTopic(topic) 632 | kafkaCluster.existTopic(topic) should be(true) 633 | producerThread.start() 634 | 635 | kafkaConnect.startup() 636 | TestUtils.waitTillAvailable("localhost", kafkaConnect.getPort, 5000) 637 | 638 | ksqlEngine.startup() 639 | TestUtils.waitTillAvailable("localhost", ksqlEngine.getPort, 5000) 640 | 641 | ksqlConnection = DriverManager.getConnection(ksqlUrl) 642 | } 643 | 644 | override def afterAll(): Unit = { 645 | info(s"Produced ${producerThread.getNumExecs} messages") 646 | stop.set(true) 647 | TestUtils.swallow(producerThread.interrupt()) 648 | 649 | TestUtils.swallow(ksqlConnection.close()) 650 | ksqlEngine.shutdown() 651 | 652 | kafkaConnect.shutdown() 653 | 654 | TestUtils.swallow(kafkaProducer.close()) 655 | kafkaCluster.shutdown() 656 | zkServer.shutdown() 657 | } 658 | 659 | } 660 | 661 | class BackgroundOps(stop: AtomicBoolean, exec: () => Unit) extends Thread { 662 | private var count = 0L 663 | 664 | override def run(): Unit = { 665 | while (!stop.get) { 666 | exec() 667 | this.count += 1 668 | } 669 | } 670 | 671 | def getNumExecs: Long = this.count 672 | } 673 | --------------------------------------------------------------------------------