From 55fba10feb6f81add6b066fdd9b60aa567c48948 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Wed, 8 May 2024 23:40:13 +0900 Subject: [PATCH 01/69] Fix DatabaseMetaData --- .../src/main/scala/ldbc/connector/DatabaseMetaData.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/DatabaseMetaData.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/DatabaseMetaData.scala index 2ecc21c52..04fa40b73 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/DatabaseMetaData.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/DatabaseMetaData.scala @@ -4362,7 +4362,7 @@ object DatabaseMetaData: if tinyInt1isBit && !transformedBitIsBoolean then sqlBuf.append( - " WHEN UPPER(DATA_TYPE)='TINYINT' AND LOCATE('ZEROFILL', UPPER(DTD_IDENTIFIER)) = 0 AND LOCATE('UNSIGNED', UPPER(DTD_IDENTIFIER)) = 0 AND LOCATE('(1)', DTD_IDENTIFIER) != 0 THEN 1" + " WHEN (UPPER(DATA_TYPE)='TINYINT' AND LOCATE('ZEROFILL', UPPER(DTD_IDENTIFIER)) = 0) AND LOCATE('UNSIGNED', UPPER(DTD_IDENTIFIER)) = 0 AND LOCATE('(1)', DTD_IDENTIFIER) != 0 THEN 1" ) end if @@ -5672,8 +5672,8 @@ object DatabaseMetaData: protected def getDatabase(catalog: Option[String], schema: Option[String]): Option[String] = (databaseTerm, catalog, schema) match - case (Some(DatabaseTerm.SCHEMA), None, value) => value - case (Some(DatabaseTerm.CATALOG), value, None) => value + case (Some(DatabaseTerm.SCHEMA), None, value) => value.fold(database)(_.some) + case (Some(DatabaseTerm.CATALOG), value, None) => value.fold(database)(_.some) case _ => database /** From 3984cd57d7e6de4a4dc697d75c35bd32ead6381c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 01:12:28 +0900 Subject: [PATCH 02/69] Create StringHelper --- .../ldbc/connector/util/StringHelper.scala | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala new file mode 100644 index 000000000..587680559 --- /dev/null +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + +package ldbc.connector.util + +object StringHelper: + + /** + * Determines whether or not the string 'searchIn' contains the string 'searchFor', disregarding case and starting at 'startAt'. Shorthand for a + * String.regionMatch(...) + * + * @param searchIn + * the string to search in + * @param startAt + * the position to start at + * @param searchFor + * the string to search for + * @return whether searchIn starts with searchFor, ignoring case + */ + def regionMatchesIgnoreCase(searchIn: String, startAt: Int, searchFor: String): Boolean = + searchIn.regionMatches(true, startAt, searchFor, 0, searchFor.length) + + def isCharAtPosNotEqualIgnoreCase(searchIn: String, pos: Int, firstCharOfSearchForUc: Char, firstCharOfSearchForLc: Char): Boolean = + val charAtPos = searchIn.charAt(pos) + charAtPos != firstCharOfSearchForUc && charAtPos != firstCharOfSearchForLc + + /** + * Finds the position of a substring within a string ignoring case. + * + * @param startingPosition + * the position to start the search from + * @param searchIn + * the string to search in + * @param searchFor + * the array of strings to search for + * @return the position where searchFor is found within searchIn starting from startingPosition. + */ + def indexOfIgnoreCase(startingPosition: Int, searchIn: String, searchFor: String): Int = + val searchInLength = searchIn.length + val searchForLength = searchFor.length + val stopSearchingAt = searchInLength - searchForLength + + if startingPosition > stopSearchingAt || searchForLength == 0 then + -1 + else + + // Some locales don't follow upper-case rule, so need to check both + val firstCharOfSearchForUc = Character.toUpperCase(searchFor.charAt(0)) + val firstCharOfSearchForLc = Character.toLowerCase(searchFor.charAt(0)) + + def loop(i: Int): Int = + if i > stopSearchingAt then -1 + else if isCharAtPosNotEqualIgnoreCase(searchIn, i, firstCharOfSearchForUc, firstCharOfSearchForLc) then + loop(i + 1) + else if regionMatchesIgnoreCase(searchIn, i, searchFor) then + i + else + loop(i + 1) + + loop(startingPosition) + + end indexOfIgnoreCase From 3a5343368890c1bd41e36fa2d2c13325be381e13 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 01:15:23 +0900 Subject: [PATCH 03/69] Create CallableStatement --- .../net/protocol/CallableStatement.scala | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala new file mode 100644 index 000000000..d87a17dc3 --- /dev/null +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + +package ldbc.connector.net.protocol + +//import java.time.* + +import scala.collection.immutable.ListMap + +import cats.* +import cats.syntax.all.* + +import cats.effect.* + +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.trace.{Tracer, Span} + +import ldbc.connector.* +import ldbc.connector.data.* +import ldbc.connector.exception.SQLException +import ldbc.connector.net.Protocol +import ldbc.connector.net.packet.response.* +import ldbc.connector.net.packet.request.* + +/** + * The interface used to execute SQL stored procedures. The JDBC API + * provides a stored procedure SQL escape syntax that allows stored procedures + * to be called in a standard way for all RDBMSs. This escape syntax has one + * form that includes a result parameter and one that does not. If used, the result + * parameter must be registered as an OUT parameter. The other parameters + * can be used for input, output or both. Parameters are referred to + * sequentially, by number, with the first parameter being 1. + *
+ *   {?= call <procedure-name>[(<arg1>,<arg2>, ...)]}
+ *   {call <procedure-name>[(<arg1>,<arg2>, ...)]}
+ * 
+ *

+ * IN parameter values are set using the set methods inherited from + * {@link PreparedStatement}. The type of all OUT parameters must be + * registered prior to executing the stored procedure; their values + * are retrieved after execution via the get methods provided here. + *

+ * A CallableStatement can return one {@link ResultSet} object or + * multiple ResultSet objects. Multiple + * ResultSet objects are handled using operations + * inherited from {@link Statement}. + *

+ * For maximum portability, a call's ResultSet objects and + * update counts should be processed prior to getting the values of output + * parameters. + * + * @tparam F + * the effect type + */ +trait CallableStatement[F[_]] extends PreparedStatement[F]: + + /** + * Registers the OUT parameter in ordinal position + * parameterIndex to the JDBC type + * sqlType. All OUT parameters must be registered + * before a stored procedure is executed. + *

+ * The JDBC type specified by sqlType for an OUT + * parameter determines the Java type that must be used + * in the get method to read the value of that parameter. + *

+ * If the JDBC type expected to be returned to this output parameter + * is specific to this particular database, sqlType + * should be java.sql.Types.OTHER. The method + * {@link #getObject} retrieves the value. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @param sqlType the JDBC type code defined by java.sql.Types. + * If the parameter is of JDBC type NUMERIC + * or DECIMAL, the version of + * registerOutParameter that accepts a scale value + * should be used. + */ + def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] + +object CallableStatement: + + private val PARAMETER_NAMESPACE_PREFIX = "@com_mysql_ldbc_outparam_" + + case class CallableStatementParameter( + paramName: Option[String], + isIn: Boolean, + isOut: Boolean, + index: Int, + jdbcType: Int, + typeName: Option[String], + precision: Int, + scale: Int, + nullability: Short, + inOutModifier: Int, + ) + + case class ParamInfo( + nativeSql: String, + dbInUse: Option[String], + isFunctionCall: Boolean, + numParameters: Int, + parameterList: List[CallableStatementParameter], + parameterMap: ListMap[String, CallableStatementParameter] + ) + + object ParamInfo: + + def apply[F[_]: Temporal](nativeSql: String, database: Option[String], resultSet: ResultSet[F], isFunctionCall: Boolean): F[ParamInfo] = + val parameterListF = Monad[F].whileM[List, CallableStatementParameter](resultSet.next()) { + for + index <- resultSet.getRow() + paramName <- resultSet.getString(4) + procedureColumn <- resultSet.getInt(5) + jdbcType <- resultSet.getInt(6) + typeName <- resultSet.getString(7) + precision <- resultSet.getInt(8) + scale <- resultSet.getInt(19) + nullability <- resultSet.getShort(12) + yield + val inOutModifier = procedureColumn match + case DatabaseMetaData.procedureColumnIn => 1 + case DatabaseMetaData.procedureColumnInOut => 2 + case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => 4 + case _ => 0 + + val (isOutParameter, isInParameter) = + if index - 1 == 0 && isFunctionCall then (true, false) + else if inOutModifier == DatabaseMetaData.procedureColumnInOut then (true, true) + else if inOutModifier == DatabaseMetaData.procedureColumnIn then (false, true) + else if inOutModifier == DatabaseMetaData.procedureColumnOut then (true, false) + else (false, false) + CallableStatementParameter(paramName, isInParameter, isOutParameter, index, jdbcType, typeName, precision, scale, nullability, inOutModifier) + } + + for + numParameters <- resultSet.getRow() + parameterList <- parameterListF + yield ParamInfo( + nativeSql = nativeSql, + dbInUse = database, + isFunctionCall = isFunctionCall, + numParameters = numParameters, + parameterList = parameterList, + parameterMap = ListMap(parameterList.map(p => p.paramName.getOrElse("") -> p): _*) + ) + + private[ldbc] case class Impl[F[_]: Temporal: Exchange: Tracer]( + protocol: Protocol[F], + serverVariables: Map[String, String], + sql: String, + paramInfo: ParamInfo, + params: Ref[F, ListMap[Int, Parameter]], + batchedArgs: Ref[F, Vector[String]], + connectionClosed: Ref[F, Boolean], + statementClosed: Ref[F, Boolean], + resultSetClosed: Ref[F, Boolean], + currentResultSet: Ref[F, Option[ResultSet[F]]], + updateCount: Ref[F, Int], + moreResults: Ref[F, Boolean], + autoGeneratedKeys: Ref[F, Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS], + lastInsertId: Ref[F, Int], + resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, + resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY + )(using ev: MonadError[F, Throwable]) extends CallableStatement[F], Statement.ShareStatement[F]: + + private val attributes = protocol.initialPacket.attributes ++ List( + Attribute("type", "CallableStatement"), + Attribute("sql", sql) + ) + + override def executeQuery(): F[ResultSet[F]] = + checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "query") + ))* + ) *> + setInOutParamsOnServer(paramInfo) *> ??? + } + } + + override def executeUpdate(): F[Int] = ??? + override def execute(): F[Boolean] = ??? + override def addBatch(): F[Unit] = ??? + override def executeBatch(): F[List[Int]] = ??? + override def close(): F[Unit] = ??? + + override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? + + private def sendQuery(sql: String): F[GenericResponsePackets] = + checkNullOrEmptyQuery(sql) *> protocol.resetSequenceId *> protocol.send( + ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) + ) *> protocol.receive(GenericResponsePackets.decoder(protocol.initialPacket.capabilityFlags)) + + private def mangleParameterName(origParameterName: String): String = + val offset = if origParameterName.nonEmpty && origParameterName.charAt(0) == '@' then 1 else 0 + + val paramNameBuf = new StringBuilder(PARAMETER_NAMESPACE_PREFIX.length + origParameterName.length) + paramNameBuf.append(PARAMETER_NAMESPACE_PREFIX) + paramNameBuf.append(origParameterName.substring(offset)) + + paramNameBuf.toString + + private def setInOutParamsOnServer(paramInfo: ParamInfo): F[Unit] = + if paramInfo.numParameters > 0 then + paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => + if param.isOut && param.isIn then + val paramName = param.paramName.getOrElse("nullnp" + param.index) + val inOutParameterName = mangleParameterName(paramName) + + val queryBuf = new StringBuilder(4 + inOutParameterName.length + 1) + queryBuf.append("SET ") + queryBuf.append(inOutParameterName) + queryBuf.append("=") + + acc *> params.get.flatMap { params => + params.get(param.index) match + case Some(parameter) => + val sql = (queryBuf.toString.toCharArray ++ parameter.sql).mkString + sendQuery(sql).flatMap { + case _: OKPacket => ev.unit + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + case None => ev.raiseError(new SQLException("Parameter not found")) + } + else acc + } + else ev.unit + + //private def setOutParams(paramInfo: ParamInfo): F[Unit] = + // if paramInfo.numParameters > 0 then + // paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => + // if !paramInfo.isFunctionCall && param.isOut then + // val paramName = param.paramName.getOrElse("nullnp" + param.index) + // val outParameterName = mangleParameterName(paramName) +// + // acc *> params.get.flatMap { params => + // val outParamIndex = + // if params.isEmpty then param.index + 1 + // else params.keys.find(_ == param.index).getOrElse(param.index + 1) + // ??? + // } + // ??? + // else acc + // } + // else ev.unit From f13530b1d9c714ddecfb6ebfcfbbf91f7668c48b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 01:15:47 +0900 Subject: [PATCH 04/69] Added prepareCall --- .../scala/ldbc/connector/Connection.scala | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index bf9cd70f5..ba5a434a0 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -7,16 +7,24 @@ package ldbc.connector import java.util.UUID + import scala.concurrent.duration.Duration import scala.collection.immutable.ListMap + import com.comcast.ip4s.* + import cats.* import cats.syntax.all.* + import cats.effect.* import cats.effect.std.Console + import fs2.io.net.* + import org.typelevel.otel4s.trace.Tracer + import ldbc.connector.data.* +import ldbc.connector.util.StringHelper import ldbc.connector.exception.* import ldbc.connector.net.* import ldbc.connector.net.protocol.* @@ -84,6 +92,36 @@ trait Connection[F[_]]: */ def prepareStatement(sql: String): F[PreparedStatement[F]] + /** + * Creates a CallableStatement object for calling + * database stored procedures. + * The CallableStatement object provides + * methods for setting up its IN and OUT parameters, and + * methods for executing the call to a stored procedure. + * + *

Note: This method is optimized for handling stored + * procedure call statements. Some drivers may send the call + * statement to the database when the method prepareCall + * is done; others + * may wait until the CallableStatement object + * is executed. This has no + * direct effect on users; however, it does affect which method + * throws certain SQLExceptions. + *

+ * Result sets created using the returned CallableStatement + * object will by default be type TYPE_FORWARD_ONLY + * and have a concurrency level of CONCUR_READ_ONLY. + * The holdability of the created result sets can be determined by + * calling {@link #getHoldability}. + * + * @param sql an SQL statement that may contain one or more '?' + * parameter placeholders. Typically this statement is specified using JDBC + * call escape syntax. + * @return a new default CallableStatement object containing the + * pre-compiled SQL statement + */ + def prepareCall(sql: String): F[CallableStatement[F]] + /** * Converts the given SQL statement into the system's native SQL grammar. * A driver may convert the JDBC SQL grammar into its system's @@ -313,6 +351,30 @@ trait Connection[F[_]]: */ def prepareStatement(sql: String, resultSetType: Int, resultSetConcurrency: Int): F[PreparedStatement[F]] + /** + * Creates a CallableStatement object that will generate + * ResultSet objects with the given type and concurrency. + * This method is the same as the prepareCall method + * above, but it allows the default result set + * type and concurrency to be overridden. + * The holdability of the created result sets can be determined by + * calling {@link #getHoldability}. + * + * @param sql a String object that is the SQL statement to + * be sent to the database; may contain on or more '?' parameters + * @param resultSetType a result set type; one of + * ResultSet.TYPE_FORWARD_ONLY, + * ResultSet.TYPE_SCROLL_INSENSITIVE, or + * ResultSet.TYPE_SCROLL_SENSITIVE + * @param resultSetConcurrency a concurrency type; one of + * ResultSet.CONCUR_READ_ONLY or + * ResultSet.CONCUR_UPDATABLE + * @return a new CallableStatement object containing the + * pre-compiled SQL statement that will produce ResultSet + * objects with the given type and concurrency + */ + def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int): F[CallableStatement[F]] + /** * Creates a default PreparedStatement object that has * the capability to retrieve auto-generated keys. The given constant @@ -691,6 +753,9 @@ object Connection: override def prepareStatement(sql: String): F[PreparedStatement[F]] = prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + override def prepareCall(sql: String): F[CallableStatement[F]] = + prepareCall(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) + override def nativeSQL(sql: String): F[String] = ev.pure(sql) override def setAutoCommit(autoCommit: Boolean): F[Unit] = @@ -824,6 +889,44 @@ object Connection: resultSetConcurrency ) + override def prepareCall(sql: String, resultSetType: Int, resultSetConcurrency: Int): F[CallableStatement[F]] = + for + metaData <- getMetaData() + procName <- extractProcedureName(sql) + resultSet <- ev.pure(databaseTerm.contains(DatabaseTerm.SCHEMA)).ifM( + metaData.getProcedureColumns(None, database, Some(procName), Some("%")), + metaData.getProcedureColumns(database, None, Some(procName), Some("%")), + ) + paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) + params <- Ref[F].of(ListMap.empty[Int, Parameter]) + batchedArgs <- Ref[F].of(Vector.empty[String]) + statementClosed <- Ref[F].of[Boolean](false) + resultSetClosed <- Ref[F].of[Boolean](false) + currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) + updateCount <- Ref[F].of(-1) + moreResults <- Ref[F].of(false) + autoGeneratedKeys <- + Ref[F].of[Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS](Statement.NO_GENERATED_KEYS) + lastInsertId <- Ref[F].of(0) + yield CallableStatement.Impl[F]( + protocol, + serverVariables, + sql, + paramInfo, + params, + batchedArgs, + connectionClosed, + statementClosed, + resultSetClosed, + currentResultSet, + updateCount, + moreResults, + autoGeneratedKeys, + lastInsertId, + resultSetType, + resultSetConcurrency + ) + override def prepareStatement( sql: String, autoGeneratedKeys: NO_GENERATED_KEYS | RETURN_GENERATED_KEYS @@ -1065,6 +1168,35 @@ object Connection: override def changeUser(user: String, password: String): F[Unit] = protocol.resetSequenceId *> protocol.changeUser(user, password) + private def extractProcedureName(sql: String): F[String] = + + var endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "CALL ") + var offset = 5 + + if endCallIndex == -1 then + endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "SELECT ") + offset = 7 + + if endCallIndex != -1 then + val nameBuf = new StringBuilder() + + val trimmedStatement = sql.substring(endCallIndex + offset).trim() + val statementLength = trimmedStatement.length + + (0 until statementLength).takeWhile { i => + val c = trimmedStatement.charAt(i) + + if Character.isWhitespace(c) || c == '(' || c == '?' then + false + else + nameBuf.append(c) + true + } + + ev.pure(nameBuf.toString()) + else + ev.raiseError(new SQLException("Invalid SQL statement")) + def apply[F[_]: Temporal: Network: Console]( host: String, port: Int, From f78ca4745814ce4c6db385b0413fd057d6d360ce Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 01:16:05 +0900 Subject: [PATCH 05/69] Added CallableStatementTest --- .../connector/CallableStatementTest.scala | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala new file mode 100644 index 000000000..a28ded007 --- /dev/null +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + +package ldbc.connector + +import cats.effect.* + +import munit.CatsEffectSuite + +import org.typelevel.otel4s.trace.Tracer + +class CallableStatementTest extends CatsEffectSuite: + + given Tracer[IO] = Tracer.noop[IO] + + private val connection = Connection[IO]( + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + database = Some("connector_test"), + allowPublicKeyRetrieval = true, + //ssl = SSL.Trusted + ) + + test("") { + assertIOBoolean(connection.use { conn => + for + _ <- conn.prepareCall("{call demoSp(?, ?)}") + yield true + }) + } From daa3b40817c80485ef286af7920f0188cb4703ed Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 01:16:42 +0900 Subject: [PATCH 06/69] Action sbt scalafmtAll --- .../scala/ldbc/connector/Connection.scala | 23 ++-- .../net/protocol/CallableStatement.scala | 130 ++++++++++-------- .../ldbc/connector/util/StringHelper.scala | 18 +-- .../connector/CallableStatementTest.scala | 17 ++- 4 files changed, 103 insertions(+), 85 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index ba5a434a0..142f62778 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -893,11 +893,12 @@ object Connection: for metaData <- getMetaData() procName <- extractProcedureName(sql) - resultSet <- ev.pure(databaseTerm.contains(DatabaseTerm.SCHEMA)).ifM( - metaData.getProcedureColumns(None, database, Some(procName), Some("%")), - metaData.getProcedureColumns(database, None, Some(procName), Some("%")), - ) - paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) + resultSet <- ev.pure(databaseTerm.contains(DatabaseTerm.SCHEMA)) + .ifM( + metaData.getProcedureColumns(None, database, Some(procName), Some("%")), + metaData.getProcedureColumns(database, None, Some(procName), Some("%")) + ) + paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) params <- Ref[F].of(ListMap.empty[Int, Parameter]) batchedArgs <- Ref[F].of(Vector.empty[String]) statementClosed <- Ref[F].of[Boolean](false) @@ -1171,31 +1172,29 @@ object Connection: private def extractProcedureName(sql: String): F[String] = var endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "CALL ") - var offset = 5 + var offset = 5 if endCallIndex == -1 then endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "SELECT ") - offset = 7 + offset = 7 if endCallIndex != -1 then val nameBuf = new StringBuilder() val trimmedStatement = sql.substring(endCallIndex + offset).trim() - val statementLength = trimmedStatement.length + val statementLength = trimmedStatement.length (0 until statementLength).takeWhile { i => val c = trimmedStatement.charAt(i) - if Character.isWhitespace(c) || c == '(' || c == '?' then - false + if Character.isWhitespace(c) || c == '(' || c == '?' then false else nameBuf.append(c) true } ev.pure(nameBuf.toString()) - else - ev.raiseError(new SQLException("Invalid SQL statement")) + else ev.raiseError(new SQLException("Invalid SQL statement")) def apply[F[_]: Temporal: Network: Console]( host: String, diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index d87a17dc3..bb2154697 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -16,7 +16,7 @@ import cats.syntax.all.* import cats.effect.* import org.typelevel.otel4s.Attribute -import org.typelevel.otel4s.trace.{Tracer, Span} +import org.typelevel.otel4s.trace.{ Tracer, Span } import ldbc.connector.* import ldbc.connector.data.* @@ -87,46 +87,51 @@ object CallableStatement: private val PARAMETER_NAMESPACE_PREFIX = "@com_mysql_ldbc_outparam_" case class CallableStatementParameter( - paramName: Option[String], - isIn: Boolean, - isOut: Boolean, - index: Int, - jdbcType: Int, - typeName: Option[String], - precision: Int, - scale: Int, - nullability: Short, - inOutModifier: Int, - ) + paramName: Option[String], + isIn: Boolean, + isOut: Boolean, + index: Int, + jdbcType: Int, + typeName: Option[String], + precision: Int, + scale: Int, + nullability: Short, + inOutModifier: Int + ) case class ParamInfo( - nativeSql: String, - dbInUse: Option[String], - isFunctionCall: Boolean, - numParameters: Int, - parameterList: List[CallableStatementParameter], - parameterMap: ListMap[String, CallableStatementParameter] - ) + nativeSql: String, + dbInUse: Option[String], + isFunctionCall: Boolean, + numParameters: Int, + parameterList: List[CallableStatementParameter], + parameterMap: ListMap[String, CallableStatementParameter] + ) object ParamInfo: - def apply[F[_]: Temporal](nativeSql: String, database: Option[String], resultSet: ResultSet[F], isFunctionCall: Boolean): F[ParamInfo] = + def apply[F[_]: Temporal]( + nativeSql: String, + database: Option[String], + resultSet: ResultSet[F], + isFunctionCall: Boolean + ): F[ParamInfo] = val parameterListF = Monad[F].whileM[List, CallableStatementParameter](resultSet.next()) { for - index <- resultSet.getRow() - paramName <- resultSet.getString(4) + index <- resultSet.getRow() + paramName <- resultSet.getString(4) procedureColumn <- resultSet.getInt(5) - jdbcType <- resultSet.getInt(6) - typeName <- resultSet.getString(7) - precision <- resultSet.getInt(8) - scale <- resultSet.getInt(19) - nullability <- resultSet.getShort(12) + jdbcType <- resultSet.getInt(6) + typeName <- resultSet.getString(7) + precision <- resultSet.getInt(8) + scale <- resultSet.getInt(19) + nullability <- resultSet.getShort(12) yield val inOutModifier = procedureColumn match - case DatabaseMetaData.procedureColumnIn => 1 - case DatabaseMetaData.procedureColumnInOut => 2 + case DatabaseMetaData.procedureColumnIn => 1 + case DatabaseMetaData.procedureColumnInOut => 2 case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => 4 - case _ => 0 + case _ => 0 val (isOutParameter, isInParameter) = if index - 1 == 0 && isFunctionCall then (true, false) @@ -134,19 +139,30 @@ object CallableStatement: else if inOutModifier == DatabaseMetaData.procedureColumnIn then (false, true) else if inOutModifier == DatabaseMetaData.procedureColumnOut then (true, false) else (false, false) - CallableStatementParameter(paramName, isInParameter, isOutParameter, index, jdbcType, typeName, precision, scale, nullability, inOutModifier) + CallableStatementParameter( + paramName, + isInParameter, + isOutParameter, + index, + jdbcType, + typeName, + precision, + scale, + nullability, + inOutModifier + ) } for numParameters <- resultSet.getRow() parameterList <- parameterListF yield ParamInfo( - nativeSql = nativeSql, - dbInUse = database, + nativeSql = nativeSql, + dbInUse = database, isFunctionCall = isFunctionCall, - numParameters = numParameters, - parameterList = parameterList, - parameterMap = ListMap(parameterList.map(p => p.paramName.getOrElse("") -> p): _*) + numParameters = numParameters, + parameterList = parameterList, + parameterMap = ListMap(parameterList.map(p => p.paramName.getOrElse("") -> p)*) ) private[ldbc] case class Impl[F[_]: Temporal: Exchange: Tracer]( @@ -166,7 +182,9 @@ object CallableStatement: lastInsertId: Ref[F, Int], resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY - )(using ev: MonadError[F, Throwable]) extends CallableStatement[F], Statement.ShareStatement[F]: + )(using ev: MonadError[F, Throwable]) + extends CallableStatement[F], + Statement.ShareStatement[F]: private val attributes = protocol.initialPacket.attributes ++ List( Attribute("type", "CallableStatement"), @@ -186,11 +204,11 @@ object CallableStatement: } } - override def executeUpdate(): F[Int] = ??? - override def execute(): F[Boolean] = ??? - override def addBatch(): F[Unit] = ??? - override def executeBatch(): F[List[Int]] = ??? - override def close(): F[Unit] = ??? + override def executeUpdate(): F[Int] = ??? + override def execute(): F[Boolean] = ??? + override def addBatch(): F[Unit] = ??? + override def executeBatch(): F[List[Int]] = ??? + override def close(): F[Unit] = ??? override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? @@ -212,7 +230,7 @@ object CallableStatement: if paramInfo.numParameters > 0 then paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => if param.isOut && param.isIn then - val paramName = param.paramName.getOrElse("nullnp" + param.index) + val paramName = param.paramName.getOrElse("nullnp" + param.index) val inOutParameterName = mangleParameterName(paramName) val queryBuf = new StringBuilder(4 + inOutParameterName.length + 1) @@ -225,9 +243,9 @@ object CallableStatement: case Some(parameter) => val sql = (queryBuf.toString.toCharArray ++ parameter.sql).mkString sendQuery(sql).flatMap { - case _: OKPacket => ev.unit + case _: OKPacket => ev.unit case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) - case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) } case None => ev.raiseError(new SQLException("Parameter not found")) } @@ -235,20 +253,20 @@ object CallableStatement: } else ev.unit - //private def setOutParams(paramInfo: ParamInfo): F[Unit] = + // private def setOutParams(paramInfo: ParamInfo): F[Unit] = // if paramInfo.numParameters > 0 then // paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => // if !paramInfo.isFunctionCall && param.isOut then // val paramName = param.paramName.getOrElse("nullnp" + param.index) // val outParameterName = mangleParameterName(paramName) // - // acc *> params.get.flatMap { params => - // val outParamIndex = - // if params.isEmpty then param.index + 1 - // else params.keys.find(_ == param.index).getOrElse(param.index + 1) - // ??? - // } - // ??? - // else acc - // } - // else ev.unit +// acc *> params.get.flatMap { params => +// val outParamIndex = +// if params.isEmpty then param.index + 1 +// else params.keys.find(_ == param.index).getOrElse(param.index + 1) +// ??? +// } +// ??? +// else acc +// } +// else ev.unit diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala index 587680559..19c763d1d 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/util/StringHelper.scala @@ -23,7 +23,12 @@ object StringHelper: def regionMatchesIgnoreCase(searchIn: String, startAt: Int, searchFor: String): Boolean = searchIn.regionMatches(true, startAt, searchFor, 0, searchFor.length) - def isCharAtPosNotEqualIgnoreCase(searchIn: String, pos: Int, firstCharOfSearchForUc: Char, firstCharOfSearchForLc: Char): Boolean = + def isCharAtPosNotEqualIgnoreCase( + searchIn: String, + pos: Int, + firstCharOfSearchForUc: Char, + firstCharOfSearchForLc: Char + ): Boolean = val charAtPos = searchIn.charAt(pos) charAtPos != firstCharOfSearchForUc && charAtPos != firstCharOfSearchForLc @@ -39,12 +44,11 @@ object StringHelper: * @return the position where searchFor is found within searchIn starting from startingPosition. */ def indexOfIgnoreCase(startingPosition: Int, searchIn: String, searchFor: String): Int = - val searchInLength = searchIn.length + val searchInLength = searchIn.length val searchForLength = searchFor.length val stopSearchingAt = searchInLength - searchForLength - if startingPosition > stopSearchingAt || searchForLength == 0 then - -1 + if startingPosition > stopSearchingAt || searchForLength == 0 then -1 else // Some locales don't follow upper-case rule, so need to check both @@ -55,10 +59,8 @@ object StringHelper: if i > stopSearchingAt then -1 else if isCharAtPosNotEqualIgnoreCase(searchIn, i, firstCharOfSearchForUc, firstCharOfSearchForLc) then loop(i + 1) - else if regionMatchesIgnoreCase(searchIn, i, searchFor) then - i - else - loop(i + 1) + else if regionMatchesIgnoreCase(searchIn, i, searchFor) then i + else loop(i + 1) loop(startingPosition) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index a28ded007..0f9339ed4 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -17,19 +17,18 @@ class CallableStatementTest extends CatsEffectSuite: given Tracer[IO] = Tracer.noop[IO] private val connection = Connection[IO]( - host = "127.0.0.1", - port = 13306, - user = "ldbc", - password = Some("password"), - database = Some("connector_test"), - allowPublicKeyRetrieval = true, - //ssl = SSL.Trusted + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + database = Some("connector_test"), + allowPublicKeyRetrieval = true + // ssl = SSL.Trusted ) test("") { assertIOBoolean(connection.use { conn => - for - _ <- conn.prepareCall("{call demoSp(?, ?)}") + for _ <- conn.prepareCall("{call demoSp(?, ?)}") yield true }) } From 01554334b87f2f39bfb72c32f37f72bb81996fd1 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:21:59 +0900 Subject: [PATCH 07/69] Added parameter method --- .../src/main/scala/ldbc/connector/data/Parameter.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Parameter.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Parameter.scala index 83669516b..4947f57b2 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Parameter.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Parameter.scala @@ -203,3 +203,10 @@ object Parameter: override def columnDataType: ColumnDataType = ColumnDataType.MYSQL_TYPE_SHORT override def sql: Array[Char] = ("'" + value.toString + "'").toCharArray override def encode: BitVector = uint16L.encode(value.getValue).require + + def parameter(value: String): Parameter = new Parameter: + override def columnDataType: ColumnDataType = ColumnDataType.MYSQL_TYPE_STRING + override def sql: Array[Char] = value.toCharArray + override def encode: BitVector = + val bytes = value.getBytes + BitVector(bytes.length) |+| BitVector(copyOf(bytes, bytes.length)) From d80eedbb384640a1860cfbfc1b9b2b8b7d355908 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:22:37 +0900 Subject: [PATCH 08/69] Added rowLength method --- .../src/main/scala/ldbc/connector/ResultSet.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/ResultSet.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/ResultSet.scala index 7caa08f39..397e5c494 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/ResultSet.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/ResultSet.scala @@ -602,6 +602,14 @@ trait ResultSet[F[_]]: */ def hasRows(): F[Boolean] + /** + * Returns the number of rows in this ResultSet object. + * + * @return + * the number of rows + */ + def rowLength(): F[Int] + object ResultSet: /** @@ -958,6 +966,11 @@ object ResultSet: ev.pure(records.nonEmpty) } + override def rowLength(): F[Int] = + checkClose { + ev.pure(records.size) + } + private def checkClose[T](f: => F[T]): F[T] = isClosed.get.flatMap { isClosed => if isClosed then raiseError("Operation not allowed after ResultSet closed") From 15933607beff6bef836d62967883c25fd3f4714c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:23:12 +0900 Subject: [PATCH 09/69] Fixed executeQuery --- .../net/protocol/CallableStatement.scala | 127 ++++++++++++++---- 1 file changed, 99 insertions(+), 28 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index bb2154697..ecc8bacdb 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -57,6 +57,9 @@ import ldbc.connector.net.packet.request.* */ trait CallableStatement[F[_]] extends PreparedStatement[F]: + private[ldbc] def setParameter(index: Int, value: String): F[Unit] = + params.update(_ + (index -> Parameter.parameter(value))) + /** * Registers the OUT parameter in ordinal position * parameterIndex to the JDBC type @@ -154,7 +157,7 @@ object CallableStatement: } for - numParameters <- resultSet.getRow() + numParameters <- resultSet.rowLength() parameterList <- parameterListF yield ParamInfo( nativeSql = nativeSql, @@ -186,23 +189,45 @@ object CallableStatement: extends CallableStatement[F], Statement.ShareStatement[F]: + private def buildQuery(original: String, params: ListMap[Int, Parameter]): String = + val query = original.toCharArray + params + .foldLeft(query) { + case (query, (offset, param)) => + val index = query.indexOf('?', offset - 1) + if index < 0 then query + else + val (head, tail) = query.splitAt(index) + val (tailHead, tailTail) = tail.splitAt(1) + head ++ param.sql ++ tailTail + } + .mkString + private val attributes = protocol.initialPacket.attributes ++ List( Attribute("type", "CallableStatement"), Attribute("sql", sql) ) override def executeQuery(): F[ResultSet[F]] = - checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => - params.get.flatMap { params => - span.addAttributes( - (attributes ++ List( - Attribute("params", params.map((_, param) => param.toString).mkString(", ")), - Attribute("execute", "query") - ))* - ) *> - setInOutParamsOnServer(paramInfo) *> ??? + checkClosed() *> + checkNullOrEmptyQuery(sql) *> + exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => + setInOutParamsOnServer(paramInfo) *> + setOutParams(paramInfo) *> + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "query") + ))* + ) *> + protocol.resetSequenceId *> + protocol.send( + ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) + ) *> + receiveQueryResult() + } <* params.set(ListMap.empty) } - } override def executeUpdate(): F[Int] = ??? override def execute(): F[Boolean] = ??? @@ -217,6 +242,50 @@ object CallableStatement: ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> protocol.receive(GenericResponsePackets.decoder(protocol.initialPacket.capabilityFlags)) + private def receiveQueryResult(): F[ResultSet[F]] = + protocol.receive(ColumnsNumberPacket.decoder(protocol.initialPacket.capabilityFlags)).flatMap { + case _: OKPacket => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + yield ResultSet + .empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case result: ColumnsNumberPacket => + for + columnDefinitions <- + protocol.repeatProcess( + result.size, + ColumnDefinitionPacket.decoder(protocol.initialPacket.capabilityFlags) + ) + resultSetRow <- + protocol.readUntilEOF[ResultSetRowPacket]( + ResultSetRowPacket.decoder(protocol.initialPacket.capabilityFlags, columnDefinitions), + Vector.empty + ) + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) + resultSet = ResultSet( + columnDefinitions, + resultSetRow, + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow, + resultSetType, + resultSetConcurrency + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + } + private def mangleParameterName(origParameterName: String): String = val offset = if origParameterName.nonEmpty && origParameterName.charAt(0) == '@' then 1 else 0 @@ -253,20 +322,22 @@ object CallableStatement: } else ev.unit - // private def setOutParams(paramInfo: ParamInfo): F[Unit] = - // if paramInfo.numParameters > 0 then - // paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => - // if !paramInfo.isFunctionCall && param.isOut then - // val paramName = param.paramName.getOrElse("nullnp" + param.index) - // val outParameterName = mangleParameterName(paramName) -// -// acc *> params.get.flatMap { params => -// val outParamIndex = -// if params.isEmpty then param.index + 1 -// else params.keys.find(_ == param.index).getOrElse(param.index + 1) -// ??? -// } -// ??? -// else acc -// } -// else ev.unit + private def setOutParams(paramInfo: ParamInfo): F[Unit] = + if paramInfo.numParameters > 0 then + paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => + if !paramInfo.isFunctionCall && param.isOut then + val paramName = param.paramName.getOrElse("nullnp" + param.index) + val outParameterName = mangleParameterName(paramName) + + acc *> params.get.flatMap { params => + for + outParamIndex <- ( + if params.isEmpty then ev.pure(param.index) + else params.keys.find(_ == param.index).fold(ev.raiseError(new SQLException(s"Parameter ${param.index} is not registered as an output parameter")))(_.pure[F]) + ) + _ <- setParameter(outParamIndex, outParameterName) + yield () + } + else acc + } + else ev.unit From 650911289bf74a42aa49a32059cf90d9512dbef6 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:23:27 +0900 Subject: [PATCH 10/69] Fixed CallableStatementTest --- .../ldbc/connector/CallableStatementTest.scala | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 0f9339ed4..d7e1ecbfe 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -27,8 +27,14 @@ class CallableStatementTest extends CatsEffectSuite: ) test("") { - assertIOBoolean(connection.use { conn => - for _ <- conn.prepareCall("{call demoSp(?, ?)}") - yield true - }) + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement.executeQuery() + decoded <- resultSet.getString(1) + yield decoded + }, + Some("abcdefg") + ) } From 5b45fccc4ec75430b1673a0fe8afe744fa6f6051 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:23:47 +0900 Subject: [PATCH 11/69] Action sbt scalafmtAll --- .../net/protocol/CallableStatement.scala | 69 +++++++++++-------- .../connector/CallableStatementTest.scala | 3 +- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index ecc8bacdb..86b03c0a4 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -197,7 +197,7 @@ object CallableStatement: val index = query.indexOf('?', offset - 1) if index < 0 then query else - val (head, tail) = query.splitAt(index) + val (head, tail) = query.splitAt(index) val (tailHead, tailTail) = tail.splitAt(1) head ++ param.sql ++ tailTail } @@ -213,20 +213,20 @@ object CallableStatement: checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => setInOutParamsOnServer(paramInfo) *> - setOutParams(paramInfo) *> - params.get.flatMap { params => - span.addAttributes( - (attributes ++ List( - Attribute("params", params.map((_, param) => param.toString).mkString(", ")), - Attribute("execute", "query") - ))* - ) *> - protocol.resetSequenceId *> - protocol.send( - ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) + setOutParams(paramInfo) *> + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "query") + ))* ) *> - receiveQueryResult() - } <* params.set(ListMap.empty) + protocol.resetSequenceId *> + protocol.send( + ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) + ) *> + receiveQueryResult() + } <* params.set(ListMap.empty) } override def executeUpdate(): F[Int] = ??? @@ -247,7 +247,7 @@ object CallableStatement: case _: OKPacket => for resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) yield ResultSet .empty( serverVariables, @@ -270,18 +270,18 @@ object CallableStatement: Vector.empty ) resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) + resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) resultSet = ResultSet( - columnDefinitions, - resultSetRow, - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow, - resultSetType, - resultSetConcurrency - ) + columnDefinitions, + resultSetRow, + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow, + resultSetType, + resultSetConcurrency + ) _ <- currentResultSet.set(Some(resultSet)) yield resultSet } @@ -326,15 +326,24 @@ object CallableStatement: if paramInfo.numParameters > 0 then paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => if !paramInfo.isFunctionCall && param.isOut then - val paramName = param.paramName.getOrElse("nullnp" + param.index) + val paramName = param.paramName.getOrElse("nullnp" + param.index) val outParameterName = mangleParameterName(paramName) acc *> params.get.flatMap { params => for outParamIndex <- ( - if params.isEmpty then ev.pure(param.index) - else params.keys.find(_ == param.index).fold(ev.raiseError(new SQLException(s"Parameter ${param.index} is not registered as an output parameter")))(_.pure[F]) - ) + if params.isEmpty then ev.pure(param.index) + else + params.keys + .find(_ == param.index) + .fold( + ev.raiseError( + new SQLException( + s"Parameter ${ param.index } is not registered as an output parameter" + ) + ) + )(_.pure[F]) + ) _ <- setParameter(outParamIndex, outParameterName) yield () } diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index d7e1ecbfe..068362cd3 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -31,7 +31,8 @@ class CallableStatementTest extends CatsEffectSuite: connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement.executeQuery() + resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement + .executeQuery() decoded <- resultSet.getString(1) yield decoded }, From cf1bae6f2415018be935e1488f10515047490adf Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:31:57 +0900 Subject: [PATCH 12/69] Fixed ConnectionTest --- .../shared/src/test/scala/ldbc/connector/ConnectionTest.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala index 6087f1b73..988d31fc4 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala @@ -1597,7 +1597,6 @@ class ConnectionTest extends CatsEffectSuite: port = 13306, user = "ldbc", password = Some("password"), - database = Some("connector_test"), ssl = SSL.Trusted, databaseTerm = Some(DatabaseMetaData.DatabaseTerm.SCHEMA) ) @@ -1651,7 +1650,6 @@ class ConnectionTest extends CatsEffectSuite: port = 13306, user = "ldbc", password = Some("password"), - database = Some("connector_test"), ssl = SSL.Trusted, databaseTerm = Some(DatabaseMetaData.DatabaseTerm.SCHEMA) ) From d75c2e510d7db5f3326e7fe8e2462f269214e59f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:37:58 +0900 Subject: [PATCH 13/69] Delete unused --- .../scala/ldbc/connector/net/protocol/CallableStatement.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 86b03c0a4..0133da32a 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -6,8 +6,6 @@ package ldbc.connector.net.protocol -//import java.time.* - import scala.collection.immutable.ListMap import cats.* From 6ea3b7ff473ede03809e12e4bdbc4743125b2ba2 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:46:10 +0900 Subject: [PATCH 14/69] Create ParameterMetaData --- .../connector/sql/ParameterMetaData.scala | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala new file mode 100644 index 000000000..250cb7ea8 --- /dev/null +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2023-2024 by Takahiko Tominaga + * This software is licensed under the MIT License (MIT). + * For more information see LICENSE or https://opensource.org/licenses/MIT + */ + +package ldbc.connector.sql + +/** + * An object that can be used to get information about the types + * and properties for each parameter marker in a + * PreparedStatement object. For some queries and driver + * implementations, the data that would be returned by a ParameterMetaData + * object may not be available until the PreparedStatement has + * been executed. + *

+ * Some driver implementations may not be able to provide information about the + * types and properties for each parameter marker in a CallableStatement + * object. + */ +trait ParameterMetaData: + + /** + * Retrieves the number of parameters in the PreparedStatement + * object for which this ParameterMetaData object contains + * information. + * + * @return the number of parameters + */ + def getParameterCount(): Int + + /** + * Retrieves whether null values are allowed in the designated parameter. + * + * @param param the first parameter is 1, the second is 2, ... + * @return the nullability status of the given parameter; one of + * ParameterMetaData.parameterNoNulls, + * ParameterMetaData.parameterNullable, or + * ParameterMetaData.parameterNullableUnknown + */ + def isNullable(param: Int): Int + + /** + * Retrieves whether values for the designated parameter can be signed numbers. + * + * @param param the first parameter is 1, the second is 2, ... + * @return true if so; false otherwise + */ + def isSigned(param: Int): Boolean + + /** + * Retrieves the designated parameter's specified column size. + * + *

The returned value represents the maximum column size for the given parameter. + * For numeric data, this is the maximum precision. For character data, this is the length in characters. + * For datetime datatypes, this is the length in characters of the String representation (assuming the + * maximum allowed precision of the fractional seconds component). For binary data, this is the length in bytes. For the ROWID datatype, + * this is the length in bytes. 0 is returned for data types where the + * column size is not applicable. + * + * @param param the first parameter is 1, the second is 2, ... + * @return precision + */ + def getPrecision(param: Int): Int + + /** + * Retrieves the designated parameter's number of digits to right of the decimal point. + * 0 is returned for data types where the scale is not applicable. + * + * @param param the first parameter is 1, the second is 2, ... + * @return scale + */ + def getScale(param: Int): Int + + /** + * Retrieves the designated parameter's SQL type. + * + * @param param the first parameter is 1, the second is 2, ... + * @return SQL type from java.sql.Types + */ + def getParameterType(param: Int): Int + + /** + * Retrieves the designated parameter's database-specific type name. + * + * @param param the first parameter is 1, the second is 2, ... + * @return type the name used by the database. If the parameter type is + * a user-defined type, then a fully-qualified type name is returned. + */ + def getParameterTypeName(param: Int): String + + /** + * Retrieves the fully-qualified name of the Java class whose instances + * should be passed to the method PreparedStatement.setObject. + * + * @param param the first parameter is 1, the second is 2, ... + * @return the fully-qualified name of the class in the Java programming + * language that would be used by the method + * PreparedStatement.setObject to set the value + * in the specified parameter. This is the class name used + * for custom mapping. + */ + def getParameterClassName(param: Int): String + + /** + * Retrieves the designated parameter's mode. + * + * @param param the first parameter is 1, the second is 2, ... + * @return mode of the parameter; one of + * ParameterMetaData.parameterModeIn, + * ParameterMetaData.parameterModeOut, or + * ParameterMetaData.parameterModeInOut + * ParameterMetaData.parameterModeUnknown. + */ + def getParameterMode(param: Int): Int + +object ParameterMetaData: + + /** + * The constant indicating that a + * parameter will not allow NULL values. + */ + val parameterNoNulls = 0 + + /** + * The constant indicating that a + * parameter will allow NULL values. + */ + val parameterNullable = 1 + + /** + * The constant indicating that the + * nullability of a parameter is unknown. + */ + val parameterNullableUnknown = 2 + + /** + * The constant indicating that the mode of the parameter is unknown. + */ + val parameterModeUnknown = 0 + + /** + * The constant indicating that the parameter's mode is IN. + */ + val parameterModeIn = 1 + + /** + * The constant indicating that the parameter's mode is INOUT. + */ + val parameterModeInOut = 2 + + /** + * The constant indicating that the parameter's mode is OUT. + */ + val parameterModeOut = 4 From f30830996172ea584a115ba3e395822aeab94b8b Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:46:23 +0900 Subject: [PATCH 15/69] Used ParameterMetaData variables --- .../ldbc/connector/net/protocol/CallableStatement.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 0133da32a..9c22f769c 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -18,6 +18,7 @@ import org.typelevel.otel4s.trace.{ Tracer, Span } import ldbc.connector.* import ldbc.connector.data.* +import ldbc.connector.sql.* import ldbc.connector.exception.SQLException import ldbc.connector.net.Protocol import ldbc.connector.net.packet.response.* @@ -129,10 +130,10 @@ object CallableStatement: nullability <- resultSet.getShort(12) yield val inOutModifier = procedureColumn match - case DatabaseMetaData.procedureColumnIn => 1 - case DatabaseMetaData.procedureColumnInOut => 2 - case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => 4 - case _ => 0 + case DatabaseMetaData.procedureColumnIn => ParameterMetaData.parameterModeIn + case DatabaseMetaData.procedureColumnInOut => ParameterMetaData.parameterModeInOut + case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => ParameterMetaData.parameterModeOut + case _ => ParameterMetaData.parameterModeUnknown val (isOutParameter, isInParameter) = if index - 1 == 0 && isFunctionCall then (true, false) From 3399f97d488f5e7f32eaef10c10cec6c60f677d4 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 11:46:34 +0900 Subject: [PATCH 16/69] Action sbt scalafmtAll --- .../ldbc/connector/net/protocol/CallableStatement.scala | 9 +++++---- .../scala/ldbc/connector/sql/ParameterMetaData.scala | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 9c22f769c..eee20c655 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -130,10 +130,11 @@ object CallableStatement: nullability <- resultSet.getShort(12) yield val inOutModifier = procedureColumn match - case DatabaseMetaData.procedureColumnIn => ParameterMetaData.parameterModeIn - case DatabaseMetaData.procedureColumnInOut => ParameterMetaData.parameterModeInOut - case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => ParameterMetaData.parameterModeOut - case _ => ParameterMetaData.parameterModeUnknown + case DatabaseMetaData.procedureColumnIn => ParameterMetaData.parameterModeIn + case DatabaseMetaData.procedureColumnInOut => ParameterMetaData.parameterModeInOut + case DatabaseMetaData.procedureColumnOut | DatabaseMetaData.procedureColumnReturn => + ParameterMetaData.parameterModeOut + case _ => ParameterMetaData.parameterModeUnknown val (isOutParameter, isInParameter) = if index - 1 == 0 && isFunctionCall then (true, false) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala index 250cb7ea8..b46d89c0d 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/sql/ParameterMetaData.scala @@ -26,7 +26,7 @@ trait ParameterMetaData: * information. * * @return the number of parameters - */ + */ def getParameterCount(): Int /** From 7353bca6af5118f2c7665428c300b2b29fea83b3 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 14:47:11 +0900 Subject: [PATCH 17/69] Added receive output param logic --- .../net/protocol/CallableStatement.scala | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index eee20c655..769d2e1ac 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -86,6 +86,8 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: object CallableStatement: + val NOT_OUTPUT_PARAMETER_INDICATOR: Int = Int.MinValue + private val PARAMETER_NAMESPACE_PREFIX = "@com_mysql_ldbc_outparam_" case class CallableStatementParameter( @@ -179,6 +181,8 @@ object CallableStatement: statementClosed: Ref[F, Boolean], resultSetClosed: Ref[F, Boolean], currentResultSet: Ref[F, Option[ResultSet[F]]], + outputParameterResults: Ref[F, Option[ResultSet[F]]], + parameterIndexToRsIndex: Ref[F, Map[Int, Int]], updateCount: Ref[F, Int], moreResults: Ref[F, Boolean], autoGeneratedKeys: Ref[F, Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS], @@ -225,8 +229,27 @@ object CallableStatement: protocol.send( ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> - receiveQueryResult() - } <* params.set(ListMap.empty) + receiveUntilOkPacket(Vector.empty).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } + } <* + params.set(ListMap.empty) <* + retrieveOutParams() } override def executeUpdate(): F[Int] = ??? @@ -242,6 +265,39 @@ object CallableStatement: ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> protocol.receive(GenericResponsePackets.decoder(protocol.initialPacket.capabilityFlags)) + private def receiveUntilOkPacket(resultSets: Vector[ResultSet[F]]): F[Vector[ResultSet[F]]] = + protocol.receive(ColumnsNumberPacket.decoder(protocol.initialPacket.capabilityFlags)).flatMap { + case _: OKPacket => resultSets.pure[F] + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case result: ColumnsNumberPacket => + for + columnDefinitions <- + protocol.repeatProcess( + result.size, + ColumnDefinitionPacket.decoder(protocol.initialPacket.capabilityFlags) + ) + resultSetRow <- + protocol.readUntilEOF[ResultSetRowPacket]( + ResultSetRowPacket.decoder(protocol.initialPacket.capabilityFlags, columnDefinitions), + Vector.empty + ) + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) + resultSet = ResultSet( + columnDefinitions, + resultSetRow, + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow, + resultSetType, + resultSetConcurrency + ) + resultSets <- receiveUntilOkPacket(resultSets :+ resultSet) + yield resultSets + } + private def receiveQueryResult(): F[ResultSet[F]] = protocol.receive(ColumnsNumberPacket.decoder(protocol.initialPacket.capabilityFlags)).flatMap { case _: OKPacket => @@ -350,3 +406,43 @@ object CallableStatement: else acc } else ev.unit + + /** + * Issues a second query to retrieve all output parameters. + */ + private def retrieveOutParams(): F[Unit] = + val parameters = paramInfo.parameterList.foldLeft(Vector.empty[(Int, String)]) { (acc, param) => + if param.isOut then + val paramName = param.paramName.getOrElse("nullnp" + param.index) + val outParameterName = mangleParameterName(paramName) + acc :+ (param.index, outParameterName) + else acc + } + + if paramInfo.numParameters > 0 && parameters.nonEmpty then + val outParameterQuery = new StringBuilder("SELECT ") + + parameters.zipWithIndex.foreach { case ((paramIndex, paramName), index) => + if index != 0 then + outParameterQuery.append(", ") + if !paramName.startsWith("@") then + outParameterQuery.append("@") + outParameterQuery.append(paramName) + else + if !paramName.startsWith("@") then + outParameterQuery.append("@") + outParameterQuery.append(paramName) + } + + val sql = outParameterQuery.toString + checkClosed() *> + checkNullOrEmptyQuery(sql) *> + protocol.resetSequenceId *> + protocol.send(ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty)) *> + receiveQueryResult().flatMap { resultSet => + outputParameterResults.update(_ => Some(resultSet)) + } *> + parameters.zipWithIndex.foldLeft(ev.unit) { case (acc, ((paramIndex, _), index)) => + acc *> parameterIndexToRsIndex.update(_ + (paramIndex -> (index + 1))) + } + else ev.unit From b52bb2143cd61cdfa11b23fd99db1dbccbf1e7e9 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 14:47:20 +0900 Subject: [PATCH 18/69] Added parameterIndexToRsIndex --- .../shared/src/main/scala/ldbc/connector/Connection.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index 142f62778..4f406e0d7 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -904,6 +904,8 @@ object Connection: statementClosed <- Ref[F].of[Boolean](false) resultSetClosed <- Ref[F].of[Boolean](false) currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) + outputParameterResults <- Ref[F].of[Option[ResultSet[F]]](None) + parameterIndexToRsIndex <- Ref[F].of(List.fill(paramInfo.numParameters)(CallableStatement.NOT_OUTPUT_PARAMETER_INDICATOR).zipWithIndex.map((param, index) => index -> param).toMap) updateCount <- Ref[F].of(-1) moreResults <- Ref[F].of(false) autoGeneratedKeys <- @@ -920,6 +922,8 @@ object Connection: statementClosed, resultSetClosed, currentResultSet, + outputParameterResults, + parameterIndexToRsIndex, updateCount, moreResults, autoGeneratedKeys, From 795970d95a9af717f30897c15ec4cc48738be62a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 14:48:35 +0900 Subject: [PATCH 19/69] Added getInt --- .../net/protocol/CallableStatement.scala | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 769d2e1ac..a0774ffc6 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -84,6 +84,17 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: */ def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] + /** + * Retrieves the value of the designated JDBC INTEGER parameter + * as an int in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getInt(parameterIndex: Int): F[Int] + object CallableStatement: val NOT_OUTPUT_PARAMETER_INDICATOR: Int = Int.MinValue @@ -260,6 +271,14 @@ object CallableStatement: override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? + override def getInt(parameterIndex: Int): F[Int] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- (if index == NOT_OUTPUT_PARAMETER_INDICATOR then ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) else resultSet.getInt(index)) + yield value + private def sendQuery(sql: String): F[GenericResponsePackets] = checkNullOrEmptyQuery(sql) *> protocol.resetSequenceId *> protocol.send( ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) @@ -446,3 +465,25 @@ object CallableStatement: acc *> parameterIndexToRsIndex.update(_ + (paramIndex -> (index + 1))) } else ev.unit + + /** + * Returns the ResultSet that holds the output parameters, or throws an + * appropriate exception if none exist, or they weren't returned. + * + * @return + * the ResultSet that holds the output parameters + */ + private def getOutputParameters(): F[ResultSet[F]] = + outputParameterResults.get.flatMap { + case None => + if paramInfo.numParameters == 0 then + ev.raiseError(new SQLException("No output parameters registered.")) + else + ev.raiseError(new SQLException("No output parameters returned by procedure.")) + case Some(resultSet) => resultSet.pure[F] + } + + private def checkBounds(paramIndex: Int): F[Unit] = + if paramIndex < 1 || paramIndex > paramInfo.numParameters then + ev.raiseError(new SQLException(s"Parameter index of ${paramIndex} is out of range (1, ${paramInfo.numParameters})")) + else ev.unit From 9a05b8ce8514f8cb59f6920384f4d1752bef4440 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 14:48:52 +0900 Subject: [PATCH 20/69] Added get outparam value test --- .../ldbc/connector/CallableStatementTest.scala | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 068362cd3..60c077d23 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -33,9 +33,23 @@ class CallableStatementTest extends CatsEffectSuite: callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement .executeQuery() - decoded <- resultSet.getString(1) - yield decoded + value <- resultSet.getString(1) + yield value }, Some("abcdefg") ) } + + test("") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement + .executeQuery() + outParam <- callableStatement.getInt(2) + yield outParam + }, + 2 + ) + } From acf72a7b87b04bba3d6e0ab8a7a0a69706f6aaeb Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 14:49:05 +0900 Subject: [PATCH 21/69] Action sbt scalafmtAll --- .../scala/ldbc/connector/Connection.scala | 24 ++-- .../net/protocol/CallableStatement.scala | 113 +++++++++--------- .../connector/CallableStatementTest.scala | 2 +- 3 files changed, 74 insertions(+), 65 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index 4f406e0d7..a000e1e3c 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -898,16 +898,22 @@ object Connection: metaData.getProcedureColumns(None, database, Some(procName), Some("%")), metaData.getProcedureColumns(database, None, Some(procName), Some("%")) ) - paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) - params <- Ref[F].of(ListMap.empty[Int, Parameter]) - batchedArgs <- Ref[F].of(Vector.empty[String]) - statementClosed <- Ref[F].of[Boolean](false) - resultSetClosed <- Ref[F].of[Boolean](false) - currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) + paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) + params <- Ref[F].of(ListMap.empty[Int, Parameter]) + batchedArgs <- Ref[F].of(Vector.empty[String]) + statementClosed <- Ref[F].of[Boolean](false) + resultSetClosed <- Ref[F].of[Boolean](false) + currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) outputParameterResults <- Ref[F].of[Option[ResultSet[F]]](None) - parameterIndexToRsIndex <- Ref[F].of(List.fill(paramInfo.numParameters)(CallableStatement.NOT_OUTPUT_PARAMETER_INDICATOR).zipWithIndex.map((param, index) => index -> param).toMap) - updateCount <- Ref[F].of(-1) - moreResults <- Ref[F].of(false) + parameterIndexToRsIndex <- Ref[F].of( + List + .fill(paramInfo.numParameters)(CallableStatement.NOT_OUTPUT_PARAMETER_INDICATOR) + .zipWithIndex + .map((param, index) => index -> param) + .toMap + ) + updateCount <- Ref[F].of(-1) + moreResults <- Ref[F].of(false) autoGeneratedKeys <- Ref[F].of[Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS](Statement.NO_GENERATED_KEYS) lastInsertId <- Ref[F].of(0) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index a0774ffc6..5f0b7bd46 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -182,24 +182,24 @@ object CallableStatement: ) private[ldbc] case class Impl[F[_]: Temporal: Exchange: Tracer]( - protocol: Protocol[F], - serverVariables: Map[String, String], - sql: String, - paramInfo: ParamInfo, - params: Ref[F, ListMap[Int, Parameter]], - batchedArgs: Ref[F, Vector[String]], - connectionClosed: Ref[F, Boolean], - statementClosed: Ref[F, Boolean], - resultSetClosed: Ref[F, Boolean], - currentResultSet: Ref[F, Option[ResultSet[F]]], - outputParameterResults: Ref[F, Option[ResultSet[F]]], + protocol: Protocol[F], + serverVariables: Map[String, String], + sql: String, + paramInfo: ParamInfo, + params: Ref[F, ListMap[Int, Parameter]], + batchedArgs: Ref[F, Vector[String]], + connectionClosed: Ref[F, Boolean], + statementClosed: Ref[F, Boolean], + resultSetClosed: Ref[F, Boolean], + currentResultSet: Ref[F, Option[ResultSet[F]]], + outputParameterResults: Ref[F, Option[ResultSet[F]]], parameterIndexToRsIndex: Ref[F, Map[Int, Int]], - updateCount: Ref[F, Int], - moreResults: Ref[F, Boolean], - autoGeneratedKeys: Ref[F, Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS], - lastInsertId: Ref[F, Int], - resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, - resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY + updateCount: Ref[F, Int], + moreResults: Ref[F, Boolean], + autoGeneratedKeys: Ref[F, Statement.NO_GENERATED_KEYS | Statement.RETURN_GENERATED_KEYS], + lastInsertId: Ref[F, Int], + resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, + resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY )(using ev: MonadError[F, Throwable]) extends CallableStatement[F], Statement.ShareStatement[F]: @@ -247,12 +247,12 @@ object CallableStatement: resultSetCurrentCursor <- Ref[F].of(0) resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) _ <- currentResultSet.set(Some(resultSet)) yield resultSet case Some(resultSet) => @@ -274,9 +274,12 @@ object CallableStatement: override def getInt(parameterIndex: Int): F[Int] = for resultSet <- checkBounds(parameterIndex) *> getOutputParameters() - paramMap <- parameterIndexToRsIndex.get + paramMap <- parameterIndexToRsIndex.get index = paramMap.getOrElse(parameterIndex, parameterIndex) - value <- (if index == NOT_OUTPUT_PARAMETER_INDICATOR then ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) else resultSet.getInt(index)) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getInt(index)) yield value private def sendQuery(sql: String): F[GenericResponsePackets] = @@ -286,7 +289,7 @@ object CallableStatement: private def receiveUntilOkPacket(resultSets: Vector[ResultSet[F]]): F[Vector[ResultSet[F]]] = protocol.receive(ColumnsNumberPacket.decoder(protocol.initialPacket.capabilityFlags)).flatMap { - case _: OKPacket => resultSets.pure[F] + case _: OKPacket => resultSets.pure[F] case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) case result: ColumnsNumberPacket => for @@ -301,18 +304,18 @@ object CallableStatement: Vector.empty ) resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) + resultSetCurrentRow <- Ref[F].of(resultSetRow.headOption) resultSet = ResultSet( - columnDefinitions, - resultSetRow, - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow, - resultSetType, - resultSetConcurrency - ) + columnDefinitions, + resultSetRow, + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow, + resultSetType, + resultSetConcurrency + ) resultSets <- receiveUntilOkPacket(resultSets :+ resultSet) yield resultSets } @@ -432,7 +435,7 @@ object CallableStatement: private def retrieveOutParams(): F[Unit] = val parameters = paramInfo.parameterList.foldLeft(Vector.empty[(Int, String)]) { (acc, param) => if param.isOut then - val paramName = param.paramName.getOrElse("nullnp" + param.index) + val paramName = param.paramName.getOrElse("nullnp" + param.index) val outParameterName = mangleParameterName(paramName) acc :+ (param.index, outParameterName) else acc @@ -441,16 +444,15 @@ object CallableStatement: if paramInfo.numParameters > 0 && parameters.nonEmpty then val outParameterQuery = new StringBuilder("SELECT ") - parameters.zipWithIndex.foreach { case ((paramIndex, paramName), index) => - if index != 0 then - outParameterQuery.append(", ") - if !paramName.startsWith("@") then - outParameterQuery.append("@") - outParameterQuery.append(paramName) - else - if !paramName.startsWith("@") then - outParameterQuery.append("@") - outParameterQuery.append(paramName) + parameters.zipWithIndex.foreach { + case ((paramIndex, paramName), index) => + if index != 0 then + outParameterQuery.append(", ") + if !paramName.startsWith("@") then outParameterQuery.append("@") + outParameterQuery.append(paramName) + else + if !paramName.startsWith("@") then outParameterQuery.append("@") + outParameterQuery.append(paramName) } val sql = outParameterQuery.toString @@ -461,8 +463,9 @@ object CallableStatement: receiveQueryResult().flatMap { resultSet => outputParameterResults.update(_ => Some(resultSet)) } *> - parameters.zipWithIndex.foldLeft(ev.unit) { case (acc, ((paramIndex, _), index)) => - acc *> parameterIndexToRsIndex.update(_ + (paramIndex -> (index + 1))) + parameters.zipWithIndex.foldLeft(ev.unit) { + case (acc, ((paramIndex, _), index)) => + acc *> parameterIndexToRsIndex.update(_ + (paramIndex -> (index + 1))) } else ev.unit @@ -476,14 +479,14 @@ object CallableStatement: private def getOutputParameters(): F[ResultSet[F]] = outputParameterResults.get.flatMap { case None => - if paramInfo.numParameters == 0 then - ev.raiseError(new SQLException("No output parameters registered.")) - else - ev.raiseError(new SQLException("No output parameters returned by procedure.")) + if paramInfo.numParameters == 0 then ev.raiseError(new SQLException("No output parameters registered.")) + else ev.raiseError(new SQLException("No output parameters returned by procedure.")) case Some(resultSet) => resultSet.pure[F] } private def checkBounds(paramIndex: Int): F[Unit] = if paramIndex < 1 || paramIndex > paramInfo.numParameters then - ev.raiseError(new SQLException(s"Parameter index of ${paramIndex} is out of range (1, ${paramInfo.numParameters})")) + ev.raiseError( + new SQLException(s"Parameter index of ${ paramIndex } is out of range (1, ${ paramInfo.numParameters })") + ) else ev.unit diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 60c077d23..460a0094f 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -46,7 +46,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt(2) yield outParam }, From 2b6f2078a7eab06088d4fd6a545748a0eab94f89 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:05:13 +0900 Subject: [PATCH 22/69] Added output parameter get function by index --- .../net/protocol/CallableStatement.scala | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 5f0b7bd46..e89629e46 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -6,6 +6,8 @@ package ldbc.connector.net.protocol +import java.time.* + import scala.collection.immutable.ListMap import cats.* @@ -84,6 +86,59 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: */ def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] + /** + * Retrieves the value of the designated JDBC CHAR, + * VARCHAR, or LONGVARCHAR parameter as a + * String in the Java programming language. + *

+ * For the fixed-length type JDBC CHAR, + * the String object + * returned has exactly the same value the SQL + * CHAR value had in the + * database, including any padding added by the database. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, + * the result + * is None. + */ + def getString(parameterIndex: Int): F[Option[String]] + + /** + * Retrieves the value of the designated JDBC BIT + * or BOOLEAN parameter as a + * boolean in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, + * the result is false. + */ + def getBoolean(parameterIndex: Int): F[Boolean] + + /** + * Retrieves the value of the designated JDBC TINYINT parameter + * as a byte in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getByte(parameterIndex: Int): F[Byte] + + /** + * Retrieves the value of the designated JDBC SMALLINT parameter + * as a short in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getShort(parameterIndex: Int): F[Short] + /** * Retrieves the value of the designated JDBC INTEGER parameter * as an int in the Java programming language. @@ -95,6 +150,92 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: */ def getInt(parameterIndex: Int): F[Int] + /** + * Retrieves the value of the designated JDBC BIGINT parameter + * as a long in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getLong(parameterIndex: Int): F[Long] + + /** + * Retrieves the value of the designated JDBC FLOAT parameter + * as a float in the Java programming language. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getFloat(parameterIndex: Int): F[Float] + + /** + * Retrieves the value of the designated JDBC DOUBLE parameter as a double + * in the Java programming language. + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getDouble(parameterIndex: Int): F[Double] + + /** + * Retrieves the value of the designated JDBC BINARY or + * VARBINARY parameter as an array of byte + * values in the Java programming language. + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getBytes(parameterIndex: Int): F[Option[Array[Byte]]] + + /** + * Retrieves the value of the designated JDBC DATE parameter as a + * java.time.LocalDate object. + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getDate(parameterIndex: Int): F[Option[LocalDate]] + + /** + * Retrieves the value of the designated JDBC TIME parameter as a + * java.time.LocalTime object. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is null. + */ + def getTime(parameterIndex: Int): F[Option[LocalTime]] + + /** + * Retrieves the value of the designated JDBC TIMESTAMP parameter as a + * java.time.LocalDateTime object. + * + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getTimestamp(parameterIndex: Int): F[Option[LocalDateTime]] + + /** + * Retrieves the value of the designated JDBC NUMERIC parameter as a + * java.math.BigDecimal object with as many digits to the + * right of the decimal point as the value contains. + * @param parameterIndex the first parameter is 1, the second is 2, + * and so on + * @return the parameter value in full precision. If the value is + * SQL NULL, the result is None. + */ + def getBigDecimal(parameterIndex: Int): F[Option[BigDecimal]] + object CallableStatement: val NOT_OUTPUT_PARAMETER_INDICATOR: Int = Int.MinValue @@ -271,6 +412,50 @@ object CallableStatement: override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? + override def getString(parameterIndex: Int): F[Option[String]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getString(index)) + yield value + + override def getBoolean(parameterIndex: Int): F[Boolean] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getBoolean(index)) + yield value + + override def getByte(parameterIndex: Int): F[Byte] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getByte(index)) + yield value + + override def getShort(parameterIndex: Int): F[Short] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getShort(index)) + yield value + override def getInt(parameterIndex: Int): F[Int] = for resultSet <- checkBounds(parameterIndex) *> getOutputParameters() @@ -282,6 +467,94 @@ object CallableStatement: else resultSet.getInt(index)) yield value + override def getLong(parameterIndex: Int): F[Long] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getLong(index)) + yield value + + override def getFloat(parameterIndex: Int): F[Float] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getFloat(index)) + yield value + + override def getDouble(parameterIndex: Int): F[Double] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getDouble(index)) + yield value + + override def getBytes(parameterIndex: Int): F[Option[Array[Byte]]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getBytes(index)) + yield value + + override def getDate(parameterIndex: Int): F[Option[LocalDate]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getDate(index)) + yield value + + override def getTime(parameterIndex: Int): F[Option[LocalTime]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getTime(index)) + yield value + + override def getTimestamp(parameterIndex: Int): F[Option[LocalDateTime]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getTimestamp(index)) + yield value + + override def getBigDecimal(parameterIndex: Int): F[Option[BigDecimal]] = + for + resultSet <- checkBounds(parameterIndex) *> getOutputParameters() + paramMap <- parameterIndexToRsIndex.get + index = paramMap.getOrElse(parameterIndex, parameterIndex) + value <- + (if index == NOT_OUTPUT_PARAMETER_INDICATOR then + ev.raiseError(new SQLException(s"Parameter $parameterIndex is not registered as an output parameter")) + else resultSet.getBigDecimal(index)) + yield value + private def sendQuery(sql: String): F[GenericResponsePackets] = checkNullOrEmptyQuery(sql) *> protocol.resetSequenceId *> protocol.send( ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) From 84b7cee412db636b45f91e823b2ef1ebe89a60ad Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:22:14 +0900 Subject: [PATCH 23/69] Added output parameter get function by parameter name --- .../net/protocol/CallableStatement.scala | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index e89629e46..770cb3403 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -236,6 +236,135 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: */ def getBigDecimal(parameterIndex: Int): F[Option[BigDecimal]] + /** + * Retrieves the value of a JDBC CHAR, VARCHAR, + * or LONGVARCHAR parameter as a String in + * the Java programming language. + *

+ * For the fixed-length type JDBC CHAR, + * the String object + * returned has exactly the same value the SQL + * CHAR value had in the + * database, including any padding added by the database. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getString(parameterName: String): F[Option[String]] + + /** + * Retrieves the value of a JDBC BIT or BOOLEAN + * parameter as a + * boolean in the Java programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is false. + */ + def getBoolean(parameterName: String): F[Boolean] + + /** + * Retrieves the value of a JDBC TINYINT parameter as a byte + * in the Java programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getByte(parameterName: String): F[Byte] + + /** + * Retrieves the value of a JDBC SMALLINT parameter as a short + * in the Java programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is 0. + */ + def getShort(parameterName: String): F[Short] + + /** + * Retrieves the value of a JDBC INTEGER parameter as an int + * in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, + * the result is 0. + */ + def getInt(parameterName: String): F[Int] + + /** + * Retrieves the value of a JDBC BIGINT parameter as a long + * in the Java programming language. + * + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, + * the result is 0. + */ + def getLong(parameterName: String): F[Long] + + /** + * Retrieves the value of a JDBC FLOAT parameter as a float + * in the Java programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, + * the result is 0. + */ + def getFloat(parameterName: String): F[Float] + + /** + * Retrieves the value of a JDBC DOUBLE parameter as a double + * in the Java programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, + * the result is 0. + */ + def getDouble(parameterName: String): F[Double] + + /** + * Retrieves the value of a JDBC BINARY or VARBINARY + * parameter as an array of byte values in the Java + * programming language. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result is + * None. + */ + def getBytes(parameterName: String): F[Option[Array[Byte]]] + + /** + * Retrieves the value of a JDBC DATE parameter as a + * java.sql.Date object. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getDate(parameterName: String): F[Option[LocalDate]] + + /** + * Retrieves the value of a JDBC TIME parameter as a + * java.sql.Time object. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is null. + */ + def getTime(parameterName: String): F[Option[LocalTime]] + + /** + * Retrieves the value of a JDBC TIMESTAMP parameter as a + * java.sql.Timestamp object. + * @param parameterName the name of the parameter + * @return the parameter value. If the value is SQL NULL, the result + * is None. + */ + def getTimestamp(parameterName: String): F[Option[LocalDateTime]] + + /** + * Retrieves the value of a JDBC NUMERIC parameter as a + * java.math.BigDecimal object with as many digits to the + * right of the decimal point as the value contains. + * @param parameterName the name of the parameter + * @return the parameter value in full precision. If the value is + * SQL NULL, the result is None. + */ + def getBigDecimal(parameterName: String): F[Option[BigDecimal]] + object CallableStatement: val NOT_OUTPUT_PARAMETER_INDICATOR: Int = Int.MinValue @@ -555,6 +684,84 @@ object CallableStatement: else resultSet.getBigDecimal(index)) yield value + override def getString(parameterName: String): F[Option[String]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getString(mangleParameterName(parameterName)) + yield value + + override def getBoolean(parameterName: String): F[Boolean] = + for + resultSet <- getOutputParameters() + value <- resultSet.getBoolean(mangleParameterName(parameterName)) + yield value + + override def getByte(parameterName: String): F[Byte] = + for + resultSet <- getOutputParameters() + value <- resultSet.getByte(mangleParameterName(parameterName)) + yield value + + override def getShort(parameterName: String): F[Short] = + for + resultSet <- getOutputParameters() + value <- resultSet.getShort(mangleParameterName(parameterName)) + yield value + + override def getInt(parameterName: String): F[Int] = + for + resultSet <- getOutputParameters() + value <- resultSet.getInt(mangleParameterName(parameterName)) + yield value + + override def getLong(parameterName: String): F[Long] = + for + resultSet <- getOutputParameters() + value <- resultSet.getLong(mangleParameterName(parameterName)) + yield value + + override def getFloat(parameterName: String): F[Float] = + for + resultSet <- getOutputParameters() + value <- resultSet.getFloat(mangleParameterName(parameterName)) + yield value + + override def getDouble(parameterName: String): F[Double] = + for + resultSet <- getOutputParameters() + value <- resultSet.getDouble(mangleParameterName(parameterName)) + yield value + + override def getBytes(parameterName: String): F[Option[Array[Byte]]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getBytes(mangleParameterName(parameterName)) + yield value + + override def getDate(parameterName: String): F[Option[LocalDate]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getDate(mangleParameterName(parameterName)) + yield value + + override def getTime(parameterName: String): F[Option[LocalTime]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getTime(mangleParameterName(parameterName)) + yield value + + override def getTimestamp(parameterName: String): F[Option[LocalDateTime]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getTimestamp(mangleParameterName(parameterName)) + yield value + + override def getBigDecimal(parameterName: String): F[Option[BigDecimal]] = + for + resultSet <- getOutputParameters() + value <- resultSet.getBigDecimal(mangleParameterName(parameterName)) + yield value + private def sendQuery(sql: String): F[GenericResponsePackets] = checkNullOrEmptyQuery(sql) *> protocol.resetSequenceId *> protocol.send( ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty) From b13a9f89081139aed074ad6e2ea9f4e47db0db6c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:22:28 +0900 Subject: [PATCH 24/69] Added get outparam value test by parameter name --- .../ldbc/connector/CallableStatementTest.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 460a0094f..4fbb23b99 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -53,3 +53,17 @@ class CallableStatementTest extends CatsEffectSuite: 2 ) } + + test("") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement + .executeQuery() + outParam <- callableStatement.getInt("inOutParam") + yield outParam + }, + 2 + ) + } From 5ee49edba59170dc619aa24ff6256f2db67fe063 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:22:34 +0900 Subject: [PATCH 25/69] Action sbt scalafmtAll --- .../net/protocol/CallableStatement.scala | 26 +++++++++---------- .../connector/CallableStatementTest.scala | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 770cb3403..b69648006 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -687,79 +687,79 @@ object CallableStatement: override def getString(parameterName: String): F[Option[String]] = for resultSet <- getOutputParameters() - value <- resultSet.getString(mangleParameterName(parameterName)) + value <- resultSet.getString(mangleParameterName(parameterName)) yield value override def getBoolean(parameterName: String): F[Boolean] = for resultSet <- getOutputParameters() - value <- resultSet.getBoolean(mangleParameterName(parameterName)) + value <- resultSet.getBoolean(mangleParameterName(parameterName)) yield value override def getByte(parameterName: String): F[Byte] = for resultSet <- getOutputParameters() - value <- resultSet.getByte(mangleParameterName(parameterName)) + value <- resultSet.getByte(mangleParameterName(parameterName)) yield value override def getShort(parameterName: String): F[Short] = for resultSet <- getOutputParameters() - value <- resultSet.getShort(mangleParameterName(parameterName)) + value <- resultSet.getShort(mangleParameterName(parameterName)) yield value override def getInt(parameterName: String): F[Int] = for resultSet <- getOutputParameters() - value <- resultSet.getInt(mangleParameterName(parameterName)) + value <- resultSet.getInt(mangleParameterName(parameterName)) yield value override def getLong(parameterName: String): F[Long] = for resultSet <- getOutputParameters() - value <- resultSet.getLong(mangleParameterName(parameterName)) + value <- resultSet.getLong(mangleParameterName(parameterName)) yield value override def getFloat(parameterName: String): F[Float] = for resultSet <- getOutputParameters() - value <- resultSet.getFloat(mangleParameterName(parameterName)) + value <- resultSet.getFloat(mangleParameterName(parameterName)) yield value override def getDouble(parameterName: String): F[Double] = for resultSet <- getOutputParameters() - value <- resultSet.getDouble(mangleParameterName(parameterName)) + value <- resultSet.getDouble(mangleParameterName(parameterName)) yield value override def getBytes(parameterName: String): F[Option[Array[Byte]]] = for resultSet <- getOutputParameters() - value <- resultSet.getBytes(mangleParameterName(parameterName)) + value <- resultSet.getBytes(mangleParameterName(parameterName)) yield value override def getDate(parameterName: String): F[Option[LocalDate]] = for resultSet <- getOutputParameters() - value <- resultSet.getDate(mangleParameterName(parameterName)) + value <- resultSet.getDate(mangleParameterName(parameterName)) yield value override def getTime(parameterName: String): F[Option[LocalTime]] = for resultSet <- getOutputParameters() - value <- resultSet.getTime(mangleParameterName(parameterName)) + value <- resultSet.getTime(mangleParameterName(parameterName)) yield value override def getTimestamp(parameterName: String): F[Option[LocalDateTime]] = for resultSet <- getOutputParameters() - value <- resultSet.getTimestamp(mangleParameterName(parameterName)) + value <- resultSet.getTimestamp(mangleParameterName(parameterName)) yield value override def getBigDecimal(parameterName: String): F[Option[BigDecimal]] = for resultSet <- getOutputParameters() - value <- resultSet.getBigDecimal(mangleParameterName(parameterName)) + value <- resultSet.getBigDecimal(mangleParameterName(parameterName)) yield value private def sendQuery(sql: String): F[GenericResponsePackets] = diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 4fbb23b99..bef7b2c1c 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -60,7 +60,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt("inOutParam") yield outParam }, From ad174f84d5302538c957d26c526e8a4531cd5c99 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:23:41 +0900 Subject: [PATCH 26/69] Fix README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cb357573a..a61a73e1b 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ JVM, JS and Native platforms are all supported. - [x] Statement - [x] PreparedStatement -- [ ] CallableStatement +- [x] CallableStatement - [ ] ResultSet Insert/Update/Delete ### Transaction function implementation @@ -86,13 +86,13 @@ JVM, JS and Native platforms are all supported. ### Utility Commands -- [x] [COM_QUIT](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_quit.html) -- [x] [COM_INIT_DB](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_init_db.html) -- [x] [COM_STATISTICS](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_statistics.html) -- [x] [COM_PING](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_ping.html) -- [x] [COM_CHANGE_USER](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_change_user.html) -- [x] [COM_RESET_CONNECTION](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_reset_connection.html) -- [x] [COM_SET_OPTION](https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_set_option.html) +- [x] COM_QUIT +- [x] COM_INIT_DB +- [x] COM_STATISTICS +- [x] COM_PING +- [x] COM_CHANGE_USER +- [x] COM_RESET_CONNECTION +- [x] COM_SET_OPTION ### Connection pooling implementation From 01de4d3d27b4cac62919e31b798aa853e9b37ca7 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:29:27 +0900 Subject: [PATCH 27/69] Fix extractProcedureName --- .../scala/ldbc/connector/Connection.scala | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index a000e1e3c..ee2bf1774 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -1180,31 +1180,18 @@ object Connection: protocol.resetSequenceId *> protocol.changeUser(user, password) private def extractProcedureName(sql: String): F[String] = + val (keyword, offset) = + if (sql.toUpperCase.contains("CALL ")) ("CALL ", 5) + else if (sql.toUpperCase.contains("SELECT ")) ("SELECT ", 7) + else ("", -1) - var endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "CALL ") - var offset = 5 - - if endCallIndex == -1 then - endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, "SELECT ") - offset = 7 - - if endCallIndex != -1 then - val nameBuf = new StringBuilder() - + if offset != -1 then + val endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, keyword) val trimmedStatement = sql.substring(endCallIndex + offset).trim() - val statementLength = trimmedStatement.length - - (0 until statementLength).takeWhile { i => - val c = trimmedStatement.charAt(i) - - if Character.isWhitespace(c) || c == '(' || c == '?' then false - else - nameBuf.append(c) - true - } - - ev.pure(nameBuf.toString()) - else ev.raiseError(new SQLException("Invalid SQL statement")) + val name = trimmedStatement.takeWhile(c => !Character.isWhitespace(c) && c != '(' && c != '?') + ev.pure(name) + else + ev.raiseError(new SQLException("Invalid SQL statement")) def apply[F[_]: Temporal: Network: Console]( host: String, From dd4db7c116f5344f74e791a98dc05a963728781f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:29:41 +0900 Subject: [PATCH 28/69] Action sbt scalafmtAll --- .../src/main/scala/ldbc/connector/Connection.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index ee2bf1774..ebda01ef7 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -1181,17 +1181,16 @@ object Connection: private def extractProcedureName(sql: String): F[String] = val (keyword, offset) = - if (sql.toUpperCase.contains("CALL ")) ("CALL ", 5) - else if (sql.toUpperCase.contains("SELECT ")) ("SELECT ", 7) + if sql.toUpperCase.contains("CALL ") then ("CALL ", 5) + else if sql.toUpperCase.contains("SELECT ") then ("SELECT ", 7) else ("", -1) if offset != -1 then - val endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, keyword) + val endCallIndex = StringHelper.indexOfIgnoreCase(0, sql, keyword) val trimmedStatement = sql.substring(endCallIndex + offset).trim() - val name = trimmedStatement.takeWhile(c => !Character.isWhitespace(c) && c != '(' && c != '?') + val name = trimmedStatement.takeWhile(c => !Character.isWhitespace(c) && c != '(' && c != '?') ev.pure(name) - else - ev.raiseError(new SQLException("Invalid SQL statement")) + else ev.raiseError(new SQLException("Invalid SQL statement")) def apply[F[_]: Temporal: Network: Console]( host: String, From 247d3b279f6bb33dd2244281f1372bb51efc386d Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:32:52 +0900 Subject: [PATCH 29/69] Fixed retrieveOutParams --- .../net/protocol/CallableStatement.scala | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index b69648006..ba7d1b86f 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -922,20 +922,14 @@ object CallableStatement: } if paramInfo.numParameters > 0 && parameters.nonEmpty then - val outParameterQuery = new StringBuilder("SELECT ") - - parameters.zipWithIndex.foreach { - case ((paramIndex, paramName), index) => - if index != 0 then - outParameterQuery.append(", ") - if !paramName.startsWith("@") then outParameterQuery.append("@") - outParameterQuery.append(paramName) - else - if !paramName.startsWith("@") then outParameterQuery.append("@") - outParameterQuery.append(paramName) - } - val sql = outParameterQuery.toString + val sql = parameters.zipWithIndex.map { + case ((_, paramName), index) => + val prefix = if index != 0 then ", " else "" + val atSign = if !paramName.startsWith("@") then "@" else "" + s"$prefix$atSign$paramName" + }.mkString("SELECT ", "", "") + checkClosed() *> checkNullOrEmptyQuery(sql) *> protocol.resetSequenceId *> From 9cb15f4617fe578d3a2c20260a54bbbdc8231d23 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:41:09 +0900 Subject: [PATCH 30/69] Added comment --- .../net/protocol/CallableStatement.scala | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index ba7d1b86f..311d40427 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -369,8 +369,32 @@ object CallableStatement: val NOT_OUTPUT_PARAMETER_INDICATOR: Int = Int.MinValue - private val PARAMETER_NAMESPACE_PREFIX = "@com_mysql_ldbc_outparam_" + private val PARAMETER_NAMESPACE_PREFIX = "@ldbc_mysql_outparam_" + /** + * CallableStatementParameter represents a parameter in a stored procedure. + * + * @param paramName + * the name of the parameter + * @param isIn + * whether the parameter is an input parameter + * @param isOut + * whether the parameter is an output parameter + * @param index + * the index of the parameter + * @param jdbcType + * the JDBC type of the parameter + * @param typeName + * the name of the type of the parameter + * @param precision + * the precision of the parameter + * @param scale + * the scale of the parameter + * @param nullability + * the nullability of the parameter + * @param inOutModifier + * the in/out modifier of the parameter + */ case class CallableStatementParameter( paramName: Option[String], isIn: Boolean, @@ -384,6 +408,22 @@ object CallableStatement: inOutModifier: Int ) + /** + * ParamInfo represents the information about the parameters in a stored procedure. + * + * @param nativeSql + * the original SQL statement + * @param dbInUse + * the database in use + * @param isFunctionCall + * whether the SQL statement is a function call + * @param numParameters + * the number of parameters in the SQL statement + * @param parameterList + * a list of CallableStatementParameter representing each parameter + * @param parameterMap + * a map from parameter name to CallableStatementParameter + */ case class ParamInfo( nativeSql: String, dbInUse: Option[String], @@ -844,6 +884,14 @@ object CallableStatement: yield resultSet } + /** + * Change the parameter name to an arbitrary prefixed naming. + * + * @param origParameterName + * the original parameter name + * @return + * the parameter name + */ private def mangleParameterName(origParameterName: String): String = val offset = if origParameterName.nonEmpty && origParameterName.charAt(0) == '@' then 1 else 0 @@ -853,6 +901,12 @@ object CallableStatement: paramNameBuf.toString + /** + * Set output parameters to be used by the server. + * + * @param paramInfo + * the parameter information + */ private def setInOutParamsOnServer(paramInfo: ParamInfo): F[Unit] = if paramInfo.numParameters > 0 then paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => @@ -880,6 +934,12 @@ object CallableStatement: } else ev.unit + /** + * Set output parameters to be handled by the client. + * + * @param paramInfo + * the parameter information + */ private def setOutParams(paramInfo: ParamInfo): F[Unit] = if paramInfo.numParameters > 0 then paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => @@ -958,6 +1018,12 @@ object CallableStatement: case Some(resultSet) => resultSet.pure[F] } + /** + * Checks if the parameter index is within the bounds of the number of parameters. + * + * @param paramIndex + * the parameter index to check + */ private def checkBounds(paramIndex: Int): F[Unit] = if paramIndex < 1 || paramIndex > paramInfo.numParameters then ev.raiseError( From 46c819e5c47f70d0560d535d3d2bb5f07de082f5 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:41:17 +0900 Subject: [PATCH 31/69] Action sbt scalafmtAll --- .../connector/net/protocol/CallableStatement.scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 311d40427..9c5a04777 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -983,12 +983,14 @@ object CallableStatement: if paramInfo.numParameters > 0 && parameters.nonEmpty then - val sql = parameters.zipWithIndex.map { - case ((_, paramName), index) => - val prefix = if index != 0 then ", " else "" - val atSign = if !paramName.startsWith("@") then "@" else "" - s"$prefix$atSign$paramName" - }.mkString("SELECT ", "", "") + val sql = parameters.zipWithIndex + .map { + case ((_, paramName), index) => + val prefix = if index != 0 then ", " else "" + val atSign = if !paramName.startsWith("@") then "@" else "" + s"$prefix$atSign$paramName" + } + .mkString("SELECT ", "", "") checkClosed() *> checkNullOrEmptyQuery(sql) *> From 8dfb0ddd2ae55e16c1c78f47c2a1b934c5a78006 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:54:58 +0900 Subject: [PATCH 32/69] Added procedure code --- database/connector_test.sql | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/database/connector_test.sql b/database/connector_test.sql index b3f493e64..3abd2081d 100644 --- a/database/connector_test.sql +++ b/database/connector_test.sql @@ -175,7 +175,32 @@ INSERT INTO `all_types` VALUES ( CREATE TABLE `transaction_test`(`c1` BIGINT NOT NULL); -delimiter // +DELIMITER // +CREATE PROCEDURE proc1() +BEGIN +SELECT VERSION(); +END; +// + +CREATE PROCEDURE proc2(IN param INT) +BEGIN +SELECT param; +END; +// + +CREATE PROCEDURE proc3(IN param1 INT, IN param2 VARCHAR(8)) +BEGIN +SELECT param1, param2; +END; +// + +CREATE PROCEDURE proc4(OUT param1 INT, OUT param2 VARCHAR(8)) +BEGIN + SET param1 = -1; + SET param2 = 'hello'; +END; +// + CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) BEGIN DECLARE z INT; @@ -187,7 +212,7 @@ SELECT inputParam; SELECT CONCAT('zyxw', inputParam); END // -delimiter ; +DELIMITER ; CREATE TABLE `privileges_table` ( `c1` INT NOT NULL PRIMARY KEY, From 3d83314b99217652c4ad224e9cb87131540379cc Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:55:06 +0900 Subject: [PATCH 33/69] Fixed CallableStatementTest --- .../connector/CallableStatementTest.scala | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index bef7b2c1c..24de3e158 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -26,13 +26,67 @@ class CallableStatementTest extends CatsEffectSuite: // ssl = SSL.Trusted ) + test("The result of calling an empty procedure matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc1()") + resultSet <- callableStatement.executeQuery() + value <- resultSet.getString(1) + yield value + }, + Some("8.0.33") + ) + } + + test("The result of calling a procedure that accepts only one IN parameter argument matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc2(?)") + resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.executeQuery() + value <- resultSet.getInt(1) + yield value + }, + 1024 + ) + } + + test("The result of calling a procedure that accepts one or more IN parameter arguments matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc3(?, ?)") + resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.setString(2, "Hello") *> callableStatement.executeQuery() + param1 <- resultSet.getInt(1) + param2 <- resultSet.getString(2) + yield (param1, param2) + }, + (1024, Some("Hello")) + ) + } + + test("The result of calling a procedure that accepts one or more OUT parameter arguments matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc4(?, ?)") + _ <- callableStatement.setInt(1, 1) *> callableStatement.setInt(2, 2) *> callableStatement.executeQuery() + param1 <- callableStatement.getInt(1) + param2 <- callableStatement.getString(2) + yield (param1, param2) + }, + (-1, Some("hello")) + ) + } + test("") { assertIO( connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() value <- resultSet.getString(1) yield value }, @@ -46,7 +100,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt(2) yield outParam }, @@ -60,7 +114,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt("inOutParam") yield outParam }, From fd17c3973b871473b42840941032f8208f158526 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 15:55:12 +0900 Subject: [PATCH 34/69] Action sbt scalafmtAll --- .../connector/CallableStatementTest.scala | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 24de3e158..45c1d8f00 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -31,8 +31,8 @@ class CallableStatementTest extends CatsEffectSuite: connection.use { conn => for callableStatement <- conn.prepareCall("CALL proc1()") - resultSet <- callableStatement.executeQuery() - value <- resultSet.getString(1) + resultSet <- callableStatement.executeQuery() + value <- resultSet.getString(1) yield value }, Some("8.0.33") @@ -44,20 +44,23 @@ class CallableStatementTest extends CatsEffectSuite: connection.use { conn => for callableStatement <- conn.prepareCall("CALL proc2(?)") - resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.executeQuery() - value <- resultSet.getInt(1) + resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.executeQuery() + value <- resultSet.getInt(1) yield value }, 1024 ) } - test("The result of calling a procedure that accepts one or more IN parameter arguments matches the specified value.") { + test( + "The result of calling a procedure that accepts one or more IN parameter arguments matches the specified value." + ) { assertIO( connection.use { conn => for callableStatement <- conn.prepareCall("CALL proc3(?, ?)") - resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.setString(2, "Hello") *> callableStatement.executeQuery() + resultSet <- callableStatement.setInt(1, 1024) *> callableStatement.setString(2, "Hello") *> callableStatement + .executeQuery() param1 <- resultSet.getInt(1) param2 <- resultSet.getString(2) yield (param1, param2) @@ -66,12 +69,14 @@ class CallableStatementTest extends CatsEffectSuite: ) } - test("The result of calling a procedure that accepts one or more OUT parameter arguments matches the specified value.") { + test( + "The result of calling a procedure that accepts one or more OUT parameter arguments matches the specified value." + ) { assertIO( connection.use { conn => for callableStatement <- conn.prepareCall("CALL proc4(?, ?)") - _ <- callableStatement.setInt(1, 1) *> callableStatement.setInt(2, 2) *> callableStatement.executeQuery() + _ <- callableStatement.setInt(1, 1) *> callableStatement.setInt(2, 2) *> callableStatement.executeQuery() param1 <- callableStatement.getInt(1) param2 <- callableStatement.getString(2) yield (param1, param2) @@ -86,7 +91,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() value <- resultSet.getString(1) yield value }, @@ -100,7 +105,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt(2) yield outParam }, @@ -114,7 +119,7 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") resultSet <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .executeQuery() + .executeQuery() outParam <- callableStatement.getInt("inOutParam") yield outParam }, From 7f3059d87ff4cd11caf9ce2e3960d004f1efa7a2 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:02:38 +0900 Subject: [PATCH 35/69] Fixed executeQuery --- .../net/protocol/CallableStatement.scala | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 9c5a04777..b09ae8109 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -537,40 +537,56 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => - setInOutParamsOnServer(paramInfo) *> - setOutParams(paramInfo) *> + if sql.toUpperCase.startsWith("CALL") then + setInOutParamsOnServer(paramInfo) *> + setOutParams(paramInfo) *> + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "query") + ))* + ) *> + protocol.resetSequenceId *> + protocol.send( + ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) + ) *> + receiveUntilOkPacket(Vector.empty).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } + } <* + params.set(ListMap.empty) <* + retrieveOutParams() + else params.get.flatMap { params => span.addAttributes( (attributes ++ List( Attribute("params", params.map((_, param) => param.toString).mkString(", ")), Attribute("execute", "query") - ))* + )) * ) *> protocol.resetSequenceId *> protocol.send( ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> - receiveUntilOkPacket(Vector.empty).flatMap { resultSets => - resultSets.headOption match - case None => - for - resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) - resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) - _ <- currentResultSet.set(Some(resultSet)) - yield resultSet - case Some(resultSet) => - currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] - } + receiveQueryResult() } <* - params.set(ListMap.empty) <* - retrieveOutParams() + params.set(ListMap.empty) } override def executeUpdate(): F[Int] = ??? From b514f1cb3c5e1ffe756dbcf1d69824f36431f72e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:03:02 +0900 Subject: [PATCH 36/69] Added procedure function code --- database/connector_test.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/database/connector_test.sql b/database/connector_test.sql index 3abd2081d..f600b7c3f 100644 --- a/database/connector_test.sql +++ b/database/connector_test.sql @@ -212,6 +212,20 @@ SELECT inputParam; SELECT CONCAT('zyxw', inputParam); END // + +CREATE FUNCTION func1() + RETURNS INT DETERMINISTIC +BEGIN +RETURN -1; +END; +// + +CREATE FUNCTION func2() + RETURNS VARCHAR(12) DETERMINISTIC +BEGIN +RETURN 'hello, world'; +END; +// DELIMITER ; CREATE TABLE `privileges_table` ( From 9b1710df28b081b0c6ad4ff1f873c349d6090170 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:05:00 +0900 Subject: [PATCH 37/69] Added stored function test --- .../connector/CallableStatementTest.scala | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 45c1d8f00..713dce9e1 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -126,3 +126,29 @@ class CallableStatementTest extends CatsEffectSuite: 2 ) } + + test("The result of calling a stored function with an empty parameter argument matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("SELECT func1()") + resultSet <- callableStatement.executeQuery() + value <- resultSet.getInt(1) + yield value + }, + -1 + ) + } + + test("The result of calling a stored function with an empty parameter argument matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("SELECT func2()") + resultSet <- callableStatement.executeQuery() + value <- resultSet.getString(1) + yield value + }, + Some("hello, world") + ) + } From aa7907fe2d3be420230bf453fd8bcb4d829bea33 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:05:08 +0900 Subject: [PATCH 38/69] Action sbt scalafmtAll --- .../scala/ldbc/connector/net/protocol/CallableStatement.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index b09ae8109..c8a32340c 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -578,7 +578,7 @@ object CallableStatement: (attributes ++ List( Attribute("params", params.map((_, param) => param.toString).mkString(", ")), Attribute("execute", "query") - )) * + ))* ) *> protocol.resetSequenceId *> protocol.send( From 343bac13e50de86912b42eb6fb55dcfd7b24e8d1 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:14:03 +0900 Subject: [PATCH 39/69] Fixed ConnectionTest --- .../shared/src/test/scala/ldbc/connector/ConnectionTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala index 988d31fc4..67c1dcfc6 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/ConnectionTest.scala @@ -1605,7 +1605,7 @@ class ConnectionTest extends CatsEffectSuite: connection.use { conn => for metaData <- conn.getMetaData() - resultSet <- metaData.getFunctions(None, None, None) + resultSet <- metaData.getFunctions(None, Some("sys"), None) values <- Monad[IO].whileM[Vector, String](resultSet.next()) { for functionCat <- resultSet.getString("FUNCTION_CAT") @@ -1658,7 +1658,7 @@ class ConnectionTest extends CatsEffectSuite: connection.use { conn => for metaData <- conn.getMetaData() - resultSet <- metaData.getFunctionColumns(None, None, None, Some("in_host")) + resultSet <- metaData.getFunctionColumns(None, Some("sys"), None, Some("in_host")) values <- Monad[IO].whileM[Vector, String](resultSet.next()) { for functionCat <- resultSet.getString("FUNCTION_CAT") From 82f4fe493e2b433ade63d3945822df23a7524bd6 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:20:05 +0900 Subject: [PATCH 40/69] Added stored function code --- database/connector_test.sql | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/database/connector_test.sql b/database/connector_test.sql index f600b7c3f..886093717 100644 --- a/database/connector_test.sql +++ b/database/connector_test.sql @@ -175,6 +175,9 @@ INSERT INTO `all_types` VALUES ( CREATE TABLE `transaction_test`(`c1` BIGINT NOT NULL); +CREATE TABLE `tax` (`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, `value` DOUBLE NOT NULL, `start_date` DATE NOT NULL); +INSERT INTO `tax` (`value`, `start_date`) VALUES (0.05, '2020-01-01'), (0.08, '2020-02-01'), (0.1, '2020-03-01'); + DELIMITER // CREATE PROCEDURE proc1() BEGIN @@ -226,6 +229,21 @@ BEGIN RETURN 'hello, world'; END; // + +CREATE FUNCTION getPrice(price int) + RETURNS INT DETERMINISTIC +BEGIN + declare tax DOUBLE DEFAULT 0.1; + + SELECT VALUE INTO tax + from tax + WHERE start_date <= current_date + ORDER BY start_date DESC + LIMIT 1; + + RETURN TRUNCATE(price + (price * tax), 0); +END; +// DELIMITER ; CREATE TABLE `privileges_table` ( From 377d2f3ace6764ad4715b8f765ea60f3eadc45bf Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:20:11 +0900 Subject: [PATCH 41/69] Added stored function test --- .../ldbc/connector/CallableStatementTest.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 713dce9e1..5244260d4 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -152,3 +152,16 @@ class CallableStatementTest extends CatsEffectSuite: Some("hello, world") ) } + + test("The result of calling a stored function with arguments matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("select getPrice(?)") + resultSet <- callableStatement.setInt(1, 100) *> callableStatement.executeQuery() + value <- resultSet.getInt(1) + yield value + }, + 110 + ) + } From aef8b0981a1d5ce8ed6bee46d55923747fe0782c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:48:44 +0900 Subject: [PATCH 42/69] Fixed executeUpdate --- .../net/protocol/CallableStatement.scala | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index c8a32340c..120cdc5f0 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -538,40 +538,7 @@ object CallableStatement: checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => if sql.toUpperCase.startsWith("CALL") then - setInOutParamsOnServer(paramInfo) *> - setOutParams(paramInfo) *> - params.get.flatMap { params => - span.addAttributes( - (attributes ++ List( - Attribute("params", params.map((_, param) => param.toString).mkString(", ")), - Attribute("execute", "query") - ))* - ) *> - protocol.resetSequenceId *> - protocol.send( - ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) - ) *> - receiveUntilOkPacket(Vector.empty).flatMap { resultSets => - resultSets.headOption match - case None => - for - resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) - resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) - _ <- currentResultSet.set(Some(resultSet)) - yield resultSet - case Some(resultSet) => - currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] - } - } <* - params.set(ListMap.empty) <* - retrieveOutParams() + executeCallStatement(span) <* retrieveOutParams() else params.get.flatMap { params => span.addAttributes( @@ -585,11 +552,31 @@ object CallableStatement: ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> receiveQueryResult() - } <* - params.set(ListMap.empty) + } + } <* params.set(ListMap.empty) + + override def executeUpdate(): F[Int] = + checkClosed() *> + checkNullOrEmptyQuery(sql) *> + exchange[F, Int]("statement") { (span: Span[F]) => + if sql.toUpperCase.startsWith("CALL") then + executeCallStatement(span) *> retrieveOutParams() *> ev.pure(-1) + else + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "update") + )) * + ) *> + sendQuery(buildQuery(sql, params)).flatMap { + case result: OKPacket => lastInsertId.set(result.lastInsertId) *> ev.pure(result.affectedRows) + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + } } - override def executeUpdate(): F[Int] = ??? override def execute(): F[Boolean] = ??? override def addBatch(): F[Unit] = ??? override def executeBatch(): F[List[Int]] = ??? @@ -1048,3 +1035,37 @@ object CallableStatement: new SQLException(s"Parameter index of ${ paramIndex } is out of range (1, ${ paramInfo.numParameters })") ) else ev.unit + + private def executeCallStatement(span: Span[F]): F[ResultSet[F]] = + setInOutParamsOnServer(paramInfo) *> + setOutParams(paramInfo) *> + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "query") + )) * + ) *> + protocol.resetSequenceId *> + protocol.send( + ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) + ) *> + receiveUntilOkPacket(Vector.empty).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } + } From 0a16ab68984ce06bd5644e2b9d7244fe34c5afa1 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:48:54 +0900 Subject: [PATCH 43/69] Added executeUpdate test --- .../ldbc/connector/CallableStatementTest.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 5244260d4..0154df5ba 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -39,6 +39,21 @@ class CallableStatementTest extends CatsEffectSuite: ) } + test("The result of calling by executeUpdate an empty procedure matches the specified value.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc1()") + resultSet <- callableStatement.executeUpdate() *> callableStatement.getResultSet() + value <- resultSet match + case Some(rs) => rs.getString(1) + case None => IO.raiseError(new Exception("No result set")) + yield value + }, + Some("8.0.33") + ) + } + test("The result of calling a procedure that accepts only one IN parameter argument matches the specified value.") { assertIO( connection.use { conn => From 7f244644bbed282c989658a3847a0287b10af502 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:49:07 +0900 Subject: [PATCH 44/69] Action sbt scalafmtAll --- .../net/protocol/CallableStatement.scala | 36 +++++++++---------- .../connector/CallableStatementTest.scala | 6 ++-- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 120cdc5f0..3d76bfbd6 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -537,8 +537,7 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then - executeCallStatement(span) <* retrieveOutParams() + if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span) <* retrieveOutParams() else params.get.flatMap { params => span.addAttributes( @@ -555,32 +554,31 @@ object CallableStatement: } } <* params.set(ListMap.empty) - override def executeUpdate(): F[Int] = + override def executeUpdate(): F[Int] = checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, Int]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then - executeCallStatement(span) *> retrieveOutParams() *> ev.pure(-1) + if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span) *> retrieveOutParams() *> ev.pure(-1) else params.get.flatMap { params => span.addAttributes( (attributes ++ List( Attribute("params", params.map((_, param) => param.toString).mkString(", ")), Attribute("execute", "update") - )) * + ))* ) *> sendQuery(buildQuery(sql, params)).flatMap { case result: OKPacket => lastInsertId.set(result.lastInsertId) *> ev.pure(result.affectedRows) case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) - case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) } } } - override def execute(): F[Boolean] = ??? - override def addBatch(): F[Unit] = ??? - override def executeBatch(): F[List[Int]] = ??? - override def close(): F[Unit] = ??? + override def execute(): F[Boolean] = ??? + override def addBatch(): F[Unit] = ??? + override def executeBatch(): F[List[Int]] = ??? + override def close(): F[Unit] = ??? override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? @@ -1044,7 +1042,7 @@ object CallableStatement: (attributes ++ List( Attribute("params", params.map((_, param) => param.toString).mkString(", ")), Attribute("execute", "query") - )) * + ))* ) *> protocol.resetSequenceId *> protocol.send( @@ -1055,14 +1053,14 @@ object CallableStatement: case None => for resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) _ <- currentResultSet.set(Some(resultSet)) yield resultSet case Some(resultSet) => diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 0154df5ba..fa5434952 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -45,9 +45,9 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL proc1()") resultSet <- callableStatement.executeUpdate() *> callableStatement.getResultSet() - value <- resultSet match - case Some(rs) => rs.getString(1) - case None => IO.raiseError(new Exception("No result set")) + value <- resultSet match + case Some(rs) => rs.getString(1) + case None => IO.raiseError(new Exception("No result set")) yield value }, Some("8.0.33") From 4633e1846cc7a2fd2aa02ab59952b6bda76f1424 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 16:51:26 +0900 Subject: [PATCH 45/69] Fixed close method --- .../scala/ldbc/connector/net/protocol/CallableStatement.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 3d76bfbd6..6238fa8b4 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -578,7 +578,8 @@ object CallableStatement: override def execute(): F[Boolean] = ??? override def addBatch(): F[Unit] = ??? override def executeBatch(): F[List[Int]] = ??? - override def close(): F[Unit] = ??? + + override def close(): F[Unit] = statementClosed.set(true) *> resultSetClosed.set(true) override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? From ddb02c3dc39bfb40c00e498833de59a09fdc3501 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 17:37:38 +0900 Subject: [PATCH 46/69] Rename outputParameterResults -> outputParameterResult --- .../ldbc/connector/net/protocol/CallableStatement.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 6238fa8b4..199ca79a7 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -502,7 +502,7 @@ object CallableStatement: statementClosed: Ref[F, Boolean], resultSetClosed: Ref[F, Boolean], currentResultSet: Ref[F, Option[ResultSet[F]]], - outputParameterResults: Ref[F, Option[ResultSet[F]]], + outputParameterResult: Ref[F, Option[ResultSet[F]]], parameterIndexToRsIndex: Ref[F, Map[Int, Int]], updateCount: Ref[F, Int], moreResults: Ref[F, Boolean], @@ -999,7 +999,7 @@ object CallableStatement: protocol.resetSequenceId *> protocol.send(ComQueryPacket(sql, protocol.initialPacket.capabilityFlags, ListMap.empty)) *> receiveQueryResult().flatMap { resultSet => - outputParameterResults.update(_ => Some(resultSet)) + outputParameterResult.update(_ => Some(resultSet)) } *> parameters.zipWithIndex.foldLeft(ev.unit) { case (acc, ((paramIndex, _), index)) => @@ -1015,7 +1015,7 @@ object CallableStatement: * the ResultSet that holds the output parameters */ private def getOutputParameters(): F[ResultSet[F]] = - outputParameterResults.get.flatMap { + outputParameterResult.get.flatMap { case None => if paramInfo.numParameters == 0 then ev.raiseError(new SQLException("No output parameters registered.")) else ev.raiseError(new SQLException("No output parameters returned by procedure.")) From 76120da67a8986a89b54a37aed2206435d2c777c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 17:55:33 +0900 Subject: [PATCH 47/69] Fixed getMoreResults --- .../net/protocol/CallableStatement.scala | 103 ++++++++++++++---- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 199ca79a7..cc9b6570d 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -503,6 +503,7 @@ object CallableStatement: resultSetClosed: Ref[F, Boolean], currentResultSet: Ref[F, Option[ResultSet[F]]], outputParameterResult: Ref[F, Option[ResultSet[F]]], + resultSets: Ref[F, List[ResultSet[F]]], parameterIndexToRsIndex: Ref[F, Map[Int, Int]], updateCount: Ref[F, Int], moreResults: Ref[F, Boolean], @@ -537,7 +538,24 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span) <* retrieveOutParams() + if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } <* retrieveOutParams() else params.get.flatMap { params => span.addAttributes( @@ -558,7 +576,24 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, Int]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span) *> retrieveOutParams() *> ev.pure(-1) + if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } *> retrieveOutParams() *> ev.pure(-1) else params.get.flatMap { params => span.addAttributes( @@ -575,7 +610,48 @@ object CallableStatement: } } - override def execute(): F[Boolean] = ??? + override def execute(): F[Boolean] = + checkClosed() *> + checkNullOrEmptyQuery(sql) *> + exchange[F, Boolean]("statement") { (span: Span[F]) => + if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { results => + moreResults.update(_ => results.nonEmpty) *> + currentResultSet.update(_ => results.headOption) *> + resultSets.set(results.toList) *> + ev.pure(results.nonEmpty) + } + else + params.get.flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "update") + )) * + ) *> + sendQuery(buildQuery(sql, params)).flatMap { + case result: OKPacket => lastInsertId.set(result.lastInsertId) *> ev.pure(result.affectedRows) + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + }.map(_ => false) + } + + override def getMoreResults(): F[Boolean] = + checkClosed() *> moreResults.get.flatMap { isMoreResults => + if isMoreResults then + resultSets.get.flatMap { + case Nil => moreResults.set(false) *> ev.pure(false) + case resultSet :: tail => + currentResultSet.get.flatMap { + case Some(current) => + current.close() *> currentResultSet.set(Some(resultSet)) *> resultSets.set(tail) *> ev.pure(true) + case None => + currentResultSet.set(Some(resultSet)) *> resultSets.set(tail) *> ev.pure(true) + } + } + else ev.pure(false) + } + override def addBatch(): F[Unit] = ??? override def executeBatch(): F[List[Int]] = ??? @@ -1035,7 +1111,7 @@ object CallableStatement: ) else ev.unit - private def executeCallStatement(span: Span[F]): F[ResultSet[F]] = + private def executeCallStatement(span: Span[F]): F[Vector[ResultSet[F]]] = setInOutParamsOnServer(paramInfo) *> setOutParams(paramInfo) *> params.get.flatMap { params => @@ -1049,22 +1125,5 @@ object CallableStatement: protocol.send( ComQueryPacket(buildQuery(sql, params), protocol.initialPacket.capabilityFlags, ListMap.empty) ) *> - receiveUntilOkPacket(Vector.empty).flatMap { resultSets => - resultSets.headOption match - case None => - for - resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) - resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) - _ <- currentResultSet.set(Some(resultSet)) - yield resultSet - case Some(resultSet) => - currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] - } + receiveUntilOkPacket(Vector.empty) } From 2f53b9ce5eef191fd09083141fa6c37996ea58c6 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 17:55:48 +0900 Subject: [PATCH 48/69] Added resultSets property --- .../shared/src/main/scala/ldbc/connector/Connection.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index ebda01ef7..d41dadac8 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -904,7 +904,8 @@ object Connection: statementClosed <- Ref[F].of[Boolean](false) resultSetClosed <- Ref[F].of[Boolean](false) currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) - outputParameterResults <- Ref[F].of[Option[ResultSet[F]]](None) + outputParameterResult <- Ref[F].of[Option[ResultSet[F]]](None) + resultSets <- Ref[F].of(List.empty[ResultSet[F]]) parameterIndexToRsIndex <- Ref[F].of( List .fill(paramInfo.numParameters)(CallableStatement.NOT_OUTPUT_PARAMETER_INDICATOR) @@ -928,7 +929,8 @@ object Connection: statementClosed, resultSetClosed, currentResultSet, - outputParameterResults, + outputParameterResult, + resultSets, parameterIndexToRsIndex, updateCount, moreResults, From df2aa1c267a67e6077215443dcec543d8f0bcd67 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 17:55:59 +0900 Subject: [PATCH 49/69] Action sbt scalafmtAll --- .../scala/ldbc/connector/Connection.scala | 14 +-- .../net/protocol/CallableStatement.scala | 117 +++++++++--------- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala index d41dadac8..cfd4c7378 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/Connection.scala @@ -898,13 +898,13 @@ object Connection: metaData.getProcedureColumns(None, database, Some(procName), Some("%")), metaData.getProcedureColumns(database, None, Some(procName), Some("%")) ) - paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) - params <- Ref[F].of(ListMap.empty[Int, Parameter]) - batchedArgs <- Ref[F].of(Vector.empty[String]) - statementClosed <- Ref[F].of[Boolean](false) - resultSetClosed <- Ref[F].of[Boolean](false) - currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) - outputParameterResult <- Ref[F].of[Option[ResultSet[F]]](None) + paramInfo <- CallableStatement.ParamInfo(sql, database, resultSet, isFunctionCall = false) + params <- Ref[F].of(ListMap.empty[Int, Parameter]) + batchedArgs <- Ref[F].of(Vector.empty[String]) + statementClosed <- Ref[F].of[Boolean](false) + resultSetClosed <- Ref[F].of[Boolean](false) + currentResultSet <- Ref[F].of[Option[ResultSet[F]]](None) + outputParameterResult <- Ref[F].of[Option[ResultSet[F]]](None) resultSets <- Ref[F].of(List.empty[ResultSet[F]]) parameterIndexToRsIndex <- Ref[F].of( List diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index cc9b6570d..59bfec284 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -538,24 +538,25 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, ResultSet[F]]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { resultSets => - resultSets.headOption match - case None => - for - resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) - resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) - _ <- currentResultSet.set(Some(resultSet)) - yield resultSet - case Some(resultSet) => - currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] - } <* retrieveOutParams() + if sql.toUpperCase.startsWith("CALL") then + executeCallStatement(span).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } <* retrieveOutParams() else params.get.flatMap { params => span.addAttributes( @@ -576,24 +577,25 @@ object CallableStatement: checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, Int]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { resultSets => - resultSets.headOption match - case None => - for - resultSetCurrentCursor <- Ref[F].of(0) - resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) - resultSet = ResultSet.empty( - serverVariables, - protocol.initialPacket.serverVersion, - resultSetClosed, - resultSetCurrentCursor, - resultSetCurrentRow - ) - _ <- currentResultSet.set(Some(resultSet)) - yield resultSet - case Some(resultSet) => - currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] - } *> retrieveOutParams() *> ev.pure(-1) + if sql.toUpperCase.startsWith("CALL") then + executeCallStatement(span).flatMap { resultSets => + resultSets.headOption match + case None => + for + resultSetCurrentCursor <- Ref[F].of(0) + resultSetCurrentRow <- Ref[F].of[Option[ResultSetRowPacket]](None) + resultSet = ResultSet.empty( + serverVariables, + protocol.initialPacket.serverVersion, + resultSetClosed, + resultSetCurrentCursor, + resultSetCurrentRow + ) + _ <- currentResultSet.set(Some(resultSet)) + yield resultSet + case Some(resultSet) => + currentResultSet.update(_ => Some(resultSet)) *> resultSet.pure[F] + } *> retrieveOutParams() *> ev.pure(-1) else params.get.flatMap { params => span.addAttributes( @@ -610,30 +612,33 @@ object CallableStatement: } } - override def execute(): F[Boolean] = + override def execute(): F[Boolean] = checkClosed() *> checkNullOrEmptyQuery(sql) *> exchange[F, Boolean]("statement") { (span: Span[F]) => - if sql.toUpperCase.startsWith("CALL") then executeCallStatement(span).flatMap { results => - moreResults.update(_ => results.nonEmpty) *> - currentResultSet.update(_ => results.headOption) *> - resultSets.set(results.toList) *> - ev.pure(results.nonEmpty) - } + if sql.toUpperCase.startsWith("CALL") then + executeCallStatement(span).flatMap { results => + moreResults.update(_ => results.nonEmpty) *> + currentResultSet.update(_ => results.headOption) *> + resultSets.set(results.toList) *> + ev.pure(results.nonEmpty) + } else - params.get.flatMap { params => - span.addAttributes( - (attributes ++ List( - Attribute("params", params.map((_, param) => param.toString).mkString(", ")), - Attribute("execute", "update") - )) * - ) *> - sendQuery(buildQuery(sql, params)).flatMap { - case result: OKPacket => lastInsertId.set(result.lastInsertId) *> ev.pure(result.affectedRows) - case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) - case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) - } - }.map(_ => false) + params.get + .flatMap { params => + span.addAttributes( + (attributes ++ List( + Attribute("params", params.map((_, param) => param.toString).mkString(", ")), + Attribute("execute", "update") + ))* + ) *> + sendQuery(buildQuery(sql, params)).flatMap { + case result: OKPacket => lastInsertId.set(result.lastInsertId) *> ev.pure(result.affectedRows) + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + } + .map(_ => false) } override def getMoreResults(): F[Boolean] = From 063456057217fa7ce323d9b092ac5d80e62881ba Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:03:47 +0900 Subject: [PATCH 50/69] Fixed getMoreResults --- .../ldbc/connector/net/protocol/CallableStatement.scala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 59bfec284..0f4904569 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -647,12 +647,7 @@ object CallableStatement: resultSets.get.flatMap { case Nil => moreResults.set(false) *> ev.pure(false) case resultSet :: tail => - currentResultSet.get.flatMap { - case Some(current) => - current.close() *> currentResultSet.set(Some(resultSet)) *> resultSets.set(tail) *> ev.pure(true) - case None => - currentResultSet.set(Some(resultSet)) *> resultSets.set(tail) *> ev.pure(true) - } + currentResultSet.set(Some(resultSet)) *> resultSets.set(tail) *> ev.pure(true) } else ev.pure(false) } From 71424146ca16e6ea93e76da9638c551709d17e67 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:03:55 +0900 Subject: [PATCH 51/69] Added execute test --- .../connector/CallableStatementTest.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index fa5434952..b2cbba398 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -6,6 +6,8 @@ package ldbc.connector +import cats.* + import cats.effect.* import munit.CatsEffectSuite @@ -142,6 +144,28 @@ class CallableStatementTest extends CatsEffectSuite: ) } + test("The results retrieved in multiple result sets returned from the procedure match the specified values.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + hasResult <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement + .execute() + values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { + for + resultSet <- callableStatement.getResultSet().flatMap { + case Some(rs) => IO.pure(rs) + case None => IO.raiseError(new Exception("No result set")) + } + value <- resultSet.getString(1) + yield value + } + yield values + }, + List(Some("abcdefg"), Some("zyxwabcdefg")) + ) + } + test("The result of calling a stored function with an empty parameter argument matches the specified value.") { assertIO( connection.use { conn => From 645dc9c8ab7d0658d686f04bc56f2fc35a25215a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:04:02 +0900 Subject: [PATCH 52/69] Action sbt scalafmtAll --- .../ldbc/connector/CallableStatementTest.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index b2cbba398..b05511177 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -150,16 +150,16 @@ class CallableStatementTest extends CatsEffectSuite: for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") hasResult <- callableStatement.setString(1, "abcdefg") *> callableStatement.setInt(2, 1) *> callableStatement - .execute() + .execute() values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { - for - resultSet <- callableStatement.getResultSet().flatMap { - case Some(rs) => IO.pure(rs) - case None => IO.raiseError(new Exception("No result set")) - } - value <- resultSet.getString(1) - yield value - } + for + resultSet <- callableStatement.getResultSet().flatMap { + case Some(rs) => IO.pure(rs) + case None => IO.raiseError(new Exception("No result set")) + } + value <- resultSet.getString(1) + yield value + } yield values }, List(Some("abcdefg"), Some("zyxwabcdefg")) From 7b9fdbc52c8d87e4b82395e10c90e6688c8178f0 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:17:05 +0900 Subject: [PATCH 53/69] Fixed addBatch --- .../net/protocol/CallableStatement.scala | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 0f4904569..cdc76e2c0 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -529,6 +529,16 @@ object CallableStatement: } .mkString + private def buildBatchQuery(original: String, params: ListMap[Int, Parameter]): String = + val placeholderCount = original.split("\\?", -1).length - 1 + require(placeholderCount == params.size, "The number of parameters does not match the number of placeholders") + original.trim.toLowerCase match + case q if q.startsWith("insert") => + val bindQuery = buildQuery(original, params) + bindQuery.split("VALUES").last + case q if q.startsWith("update") || q.startsWith("delete") => buildQuery(original, params) + case _ => throw new IllegalArgumentException("The batch query must be an INSERT, UPDATE, or DELETE statement.") + private val attributes = protocol.initialPacket.attributes ++ List( Attribute("type", "CallableStatement"), Attribute("sql", sql) @@ -652,7 +662,11 @@ object CallableStatement: else ev.pure(false) } - override def addBatch(): F[Unit] = ??? + override def addBatch(): F[Unit] = + checkClosed() *> checkNullOrEmptyQuery(sql) *> setOutParams() *> params.get.flatMap { params => + batchedArgs.update(_ :+ buildBatchQuery(sql, params)) + } *> params.set(ListMap.empty) + override def executeBatch(): F[List[Int]] = ??? override def close(): F[Unit] = statementClosed.set(true) *> resultSetClosed.set(true) @@ -1014,11 +1028,8 @@ object CallableStatement: /** * Set output parameters to be handled by the client. - * - * @param paramInfo - * the parameter information */ - private def setOutParams(paramInfo: ParamInfo): F[Unit] = + private def setOutParams(): F[Unit] = if paramInfo.numParameters > 0 then paramInfo.parameterList.foldLeft(ev.unit) { (acc, param) => if !paramInfo.isFunctionCall && param.isOut then @@ -1113,7 +1124,7 @@ object CallableStatement: private def executeCallStatement(span: Span[F]): F[Vector[ResultSet[F]]] = setInOutParamsOnServer(paramInfo) *> - setOutParams(paramInfo) *> + setOutParams() *> params.get.flatMap { params => span.addAttributes( (attributes ++ List( From d4b3d71948aca17f5a718482851ed9f3066b4a2c Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:17:14 +0900 Subject: [PATCH 54/69] Action sbt scalafmtAll --- .../scala/ldbc/connector/net/protocol/CallableStatement.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index cdc76e2c0..8c7ef3d7a 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -662,7 +662,7 @@ object CallableStatement: else ev.pure(false) } - override def addBatch(): F[Unit] = + override def addBatch(): F[Unit] = checkClosed() *> checkNullOrEmptyQuery(sql) *> setOutParams() *> params.get.flatMap { params => batchedArgs.update(_ :+ buildBatchQuery(sql, params)) } *> params.set(ListMap.empty) From cbeed9b21c863db3bbdde31f01219b7e8ccc3308 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 18:30:09 +0900 Subject: [PATCH 55/69] Added comment --- .../ldbc/connector/net/protocol/CallableStatement.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 8c7ef3d7a..a6aa17111 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -1122,6 +1122,14 @@ object CallableStatement: ) else ev.unit + /** + * Executes a CALL/Stored function statement. + * + * @param span + * the span + * @return + * a list of ResultSet + */ private def executeCallStatement(span: Span[F]): F[Vector[ResultSet[F]]] = setInOutParamsOnServer(paramInfo) *> setOutParams() *> From a5ba74930a93c83b9f65999fe5a8c79db468de8e Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 21:17:37 +0900 Subject: [PATCH 56/69] Fix comment --- .../net/protocol/CallableStatement.scala | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index a6aa17111..2518b67f3 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -89,7 +89,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC CHAR, * VARCHAR, or LONGVARCHAR parameter as a - * String in the Java programming language. + * String in the Sava programming language *

* For the fixed-length type JDBC CHAR, * the String object @@ -108,7 +108,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC BIT * or BOOLEAN parameter as a - * boolean in the Java programming language. + * boolean in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -119,7 +119,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC TINYINT parameter - * as a byte in the Java programming language. + * as a byte in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -130,7 +130,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC SMALLINT parameter - * as a short in the Java programming language. + * as a short in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -141,7 +141,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC INTEGER parameter - * as an int in the Java programming language. + * as an int in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -152,7 +152,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC BIGINT parameter - * as a long in the Java programming language. + * as a long in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -163,7 +163,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC FLOAT parameter - * as a float in the Java programming language. + * as a float in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -174,7 +174,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC DOUBLE parameter as a double - * in the Java programming language. + * in the Sava programming language * @param parameterIndex the first parameter is 1, the second is 2, * and so on * @return the parameter value. If the value is SQL NULL, the result @@ -185,7 +185,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC BINARY or * VARBINARY parameter as an array of byte - * values in the Java programming language. + * values in the Sava programming language * @param parameterIndex the first parameter is 1, the second is 2, * and so on * @return the parameter value. If the value is SQL NULL, the result @@ -239,7 +239,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC CHAR, VARCHAR, * or LONGVARCHAR parameter as a String in - * the Java programming language. + * the Sava programming language *

* For the fixed-length type JDBC CHAR, * the String object @@ -255,7 +255,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC BIT or BOOLEAN * parameter as a - * boolean in the Java programming language. + * boolean in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, the result * is false. @@ -264,7 +264,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC TINYINT parameter as a byte - * in the Java programming language. + * in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, the result * is 0. @@ -273,7 +273,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC SMALLINT parameter as a short - * in the Java programming language. + * in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, the result * is 0. @@ -282,7 +282,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC INTEGER parameter as an int - * in the Java programming language. + * in the Sava programming language * * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, @@ -292,7 +292,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC BIGINT parameter as a long - * in the Java programming language. + * in the Sava programming language * * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, @@ -302,7 +302,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC FLOAT parameter as a float - * in the Java programming language. + * in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, * the result is 0. @@ -311,7 +311,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC DOUBLE parameter as a double - * in the Java programming language. + * in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, * the result is 0. From f61de6672e5df5f8932e35e8564a3169a189eb9f Mon Sep 17 00:00:00 2001 From: takapi327 Date: Fri, 10 May 2024 21:30:27 +0900 Subject: [PATCH 57/69] Fix comment --- .../ldbc/connector/net/protocol/CallableStatement.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 2518b67f3..f5e956183 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -68,7 +68,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: * before a stored procedure is executed. *

* The JDBC type specified by sqlType for an OUT - * parameter determines the Java type that must be used + * parameter determines the Scala type that must be used * in the get method to read the value of that parameter. *

* If the JDBC type expected to be returned to this output parameter @@ -108,7 +108,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of the designated JDBC BIT * or BOOLEAN parameter as a - * boolean in the Sava programming language + * Boolean in the Sava programming language * * @param parameterIndex the first parameter is 1, the second is 2, * and so on @@ -255,7 +255,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC BIT or BOOLEAN * parameter as a - * boolean in the Sava programming language + * Boolean in the Sava programming language * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, the result * is false. @@ -320,7 +320,7 @@ trait CallableStatement[F[_]] extends PreparedStatement[F]: /** * Retrieves the value of a JDBC BINARY or VARBINARY - * parameter as an array of byte values in the Java + * parameter as an array of byte values in the Scala * programming language. * @param parameterName the name of the parameter * @return the parameter value. If the value is SQL NULL, the result is From 29324f12cd7a23633682c151e0f8b48c05d6e2c6 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 13:18:38 +0900 Subject: [PATCH 58/69] Fixed executeBatch query --- .../net/protocol/CallableStatement.scala | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index f5e956183..e60e17650 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -667,7 +667,64 @@ object CallableStatement: batchedArgs.update(_ :+ buildBatchQuery(sql, params)) } *> params.set(ListMap.empty) - override def executeBatch(): F[List[Int]] = ??? + override def executeBatch(): F[List[Int]] = + checkClosed() *> + checkNullOrEmptyQuery(sql) *> + exchange[F, List[Int]]("statement") { (span: Span[F]) => + batchedArgs.get.flatMap { args => + span.addAttributes( + (attributes ++ List( + Attribute("execute", "batch"), + Attribute("size", args.length.toLong), + Attribute("sql", args.toArray.toSeq) + ))* + ) *> ( + if args.isEmpty then ev.pure(List.empty) + else + sql.toUpperCase match + case q if q.startsWith("INSERT") => + sendQuery(sql.split("VALUES").head + " VALUES" + args.mkString(",")) + .flatMap { + case _: OKPacket => ev.pure(List.fill(args.length)(Statement.SUCCESS_NO_INFO)) + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + case q if q.startsWith("update") || q.startsWith("delete") || q.startsWith("CALL") => + protocol.resetSequenceId *> + protocol.comSetOption(EnumMySQLSetOption.MYSQL_OPTION_MULTI_STATEMENTS_ON) *> + protocol.resetSequenceId *> + protocol.send( + ComQueryPacket( + args.mkString(";"), + protocol.initialPacket.capabilityFlags, + ListMap.empty + ) + ) *> + args + .foldLeft(ev.pure(Vector.empty[Int])) { ($acc, _) => + for + acc <- $acc + result <- + protocol + .receive(GenericResponsePackets.decoder(protocol.initialPacket.capabilityFlags)) + .flatMap { + case result: OKPacket => + lastInsertId.set(result.lastInsertId) *> ev.pure(acc :+ result.affectedRows) + case error: ERRPacket => + ev.raiseError(error.toException("Failed to execute batch", acc)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + yield result + } + .map(_.toList) <* + protocol.resetSequenceId <* + protocol.comSetOption(EnumMySQLSetOption.MYSQL_OPTION_MULTI_STATEMENTS_OFF) + case _ => ev.raiseError( + new SQLException("The batch query must be an INSERT, UPDATE, or DELETE, CALL statement.") + ) + ) + } + } <* params.set(ListMap.empty) <* batchedArgs.set(Vector.empty) override def close(): F[Unit] = statementClosed.set(true) *> resultSetClosed.set(true) From a431986790e11b22bab7591bd777e170d59d5594 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 13:19:03 +0900 Subject: [PATCH 59/69] Action sbt scalafmtAll --- .../ldbc/connector/net/protocol/CallableStatement.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index e60e17650..39e4c24e2 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -719,10 +719,11 @@ object CallableStatement: .map(_.toList) <* protocol.resetSequenceId <* protocol.comSetOption(EnumMySQLSetOption.MYSQL_OPTION_MULTI_STATEMENTS_OFF) - case _ => ev.raiseError( - new SQLException("The batch query must be an INSERT, UPDATE, or DELETE, CALL statement.") - ) - ) + case _ => + ev.raiseError( + new SQLException("The batch query must be an INSERT, UPDATE, or DELETE, CALL statement.") + ) + ) } } <* params.set(ListMap.empty) <* batchedArgs.set(Vector.empty) From 838459eda3eb3ea285372c01be93e84292c052ac Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 13:26:39 +0900 Subject: [PATCH 60/69] Fixed addBatch --- .../connector/net/protocol/CallableStatement.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 39e4c24e2..7c8cb81bb 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -663,9 +663,17 @@ object CallableStatement: } override def addBatch(): F[Unit] = - checkClosed() *> checkNullOrEmptyQuery(sql) *> setOutParams() *> params.get.flatMap { params => - batchedArgs.update(_ :+ buildBatchQuery(sql, params)) - } *> params.set(ListMap.empty) + checkClosed() *> + checkNullOrEmptyQuery(sql) *> ( + sql.toUpperCase match + case q if q.startsWith("CALL") => + setInOutParamsOnServer(paramInfo) *> setOutParams() + case _ => ev.unit + ) *> + params.get.flatMap { params => + batchedArgs.update(_ :+ buildBatchQuery(sql, params)) + } *> + params.set(ListMap.empty) override def executeBatch(): F[List[Int]] = checkClosed() *> From 7004e71d1f3add73250bfa8ff20981f0f0790126 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 13:29:37 +0900 Subject: [PATCH 61/69] Fixed CallableStatementTest --- .../test/scala/ldbc/connector/CallableStatementTest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index b05511177..dba956a01 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -102,7 +102,7 @@ class CallableStatementTest extends CatsEffectSuite: ) } - test("") { + test("The result of retrieving the In parameter with index matches the specified value.") { assertIO( connection.use { conn => for @@ -116,7 +116,7 @@ class CallableStatementTest extends CatsEffectSuite: ) } - test("") { + test("The result of retrieving the Out parameter with index matches the specified value.") { assertIO( connection.use { conn => for @@ -130,7 +130,7 @@ class CallableStatementTest extends CatsEffectSuite: ) } - test("") { + test("The result of retrieving the Out parameter with parameter name matches the specified value.") { assertIO( connection.use { conn => for From c7e6e56e8ac18c5b0c6a42b5ebed8e01e3f86e3a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 19:26:03 +0900 Subject: [PATCH 62/69] Fixed registerOutParameter --- .../net/protocol/CallableStatement.scala | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 7c8cb81bb..080f50d5f 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -737,7 +737,38 @@ object CallableStatement: override def close(): F[Unit] = statementClosed.set(true) *> resultSetClosed.set(true) - override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = ??? + override def registerOutParameter(parameterIndex: Int, sqlType: Int): F[Unit] = + if paramInfo.numParameters > 0 then + paramInfo.parameterList.find(_.index == parameterIndex) match + case Some(param) => + (if param.jdbcType == sqlType then ev.unit + else ev.raiseError( + new SQLException( + "The type specified for the parameter does not match the type registered as a procedure." + ) + )) *> ( + if param.isOut && param.isIn then + val paramName = param.paramName.getOrElse("nullnp" + param.index) + val inOutParameterName = mangleParameterName(paramName) + + val queryBuf = new StringBuilder(4 + inOutParameterName.length + 1) + queryBuf.append("SET ") + queryBuf.append(inOutParameterName) + queryBuf.append("=") + + params.get.flatMap { params => + val sql = (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString + sendQuery(sql).flatMap { + case _: OKPacket => ev.unit + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } + } + else ev.raiseError(new SQLException("No output parameters returned by procedure.")) + ) + case None => + ev.raiseError(new SQLException(s"Parameter index of $parameterIndex is out of range (1, ${paramInfo.numParameters})")) + else ev.unit override def getString(parameterIndex: Int): F[Option[String]] = for From c78944ef62cb2079c3bba47d11cc3062d105fbfd Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 19:31:45 +0900 Subject: [PATCH 63/69] Added registerOutParameter test --- .../connector/CallableStatementTest.scala | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index dba956a01..6b2cbf172 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -7,11 +7,9 @@ package ldbc.connector import cats.* - import cats.effect.* - +import ldbc.connector.exception.SQLException import munit.CatsEffectSuite - import org.typelevel.otel4s.trace.Tracer class CallableStatementTest extends CatsEffectSuite: @@ -204,3 +202,36 @@ class CallableStatementTest extends CatsEffectSuite: 110 ) } + + test("The registration process of Out parameter succeeds.") { + assertIOBoolean( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + yield true + } + ) + } + + test("SQLException occurs if the Out parameter type of the procedure is different from the Out parameter type to be set.") { + interceptMessageIO[SQLException]("Message: The type specified for the parameter does not match the type registered as a procedure.")( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) + yield true + } + ) + } + + test("SQLException occurs if the procedure does not have an Out parameter and is preconfigured for Out.") { + interceptMessageIO[SQLException]("Message: No output parameters returned by procedure.")( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL proc3(?, ?)") + _ <- callableStatement.setInt(1, 1024) *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) + yield true + }, + ) + } From ebe95de6575f40693c791600b3e97a5839694927 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 19:32:00 +0900 Subject: [PATCH 64/69] Action sbt scalafmtAll --- .../net/protocol/CallableStatement.scala | 19 ++++++++++++------- .../connector/CallableStatementTest.scala | 19 +++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index 080f50d5f..e5c821d24 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -742,11 +742,13 @@ object CallableStatement: paramInfo.parameterList.find(_.index == parameterIndex) match case Some(param) => (if param.jdbcType == sqlType then ev.unit - else ev.raiseError( - new SQLException( - "The type specified for the parameter does not match the type registered as a procedure." - ) - )) *> ( + else + ev.raiseError( + new SQLException( + "The type specified for the parameter does not match the type registered as a procedure." + ) + ) + ) *> ( if param.isOut && param.isIn then val paramName = param.paramName.getOrElse("nullnp" + param.index) val inOutParameterName = mangleParameterName(paramName) @@ -757,7 +759,8 @@ object CallableStatement: queryBuf.append("=") params.get.flatMap { params => - val sql = (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString + val sql = + (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString sendQuery(sql).flatMap { case _: OKPacket => ev.unit case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) @@ -767,7 +770,9 @@ object CallableStatement: else ev.raiseError(new SQLException("No output parameters returned by procedure.")) ) case None => - ev.raiseError(new SQLException(s"Parameter index of $parameterIndex is out of range (1, ${paramInfo.numParameters})")) + ev.raiseError( + new SQLException(s"Parameter index of $parameterIndex is out of range (1, ${ paramInfo.numParameters })") + ) else ev.unit override def getString(parameterIndex: Int): F[Option[String]] = diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 6b2cbf172..4637b202d 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -208,18 +208,24 @@ class CallableStatementTest extends CatsEffectSuite: connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + _ <- callableStatement.setString(1, "abcdefg") *> callableStatement + .registerOutParameter(2, ldbc.connector.data.Types.INTEGER) yield true } ) } - test("SQLException occurs if the Out parameter type of the procedure is different from the Out parameter type to be set.") { - interceptMessageIO[SQLException]("Message: The type specified for the parameter does not match the type registered as a procedure.")( + test( + "SQLException occurs if the Out parameter type of the procedure is different from the Out parameter type to be set." + ) { + interceptMessageIO[SQLException]( + "Message: The type specified for the parameter does not match the type registered as a procedure." + )( connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) + _ <- callableStatement.setString(1, "abcdefg") *> callableStatement + .registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) yield true } ) @@ -230,8 +236,9 @@ class CallableStatementTest extends CatsEffectSuite: connection.use { conn => for callableStatement <- conn.prepareCall("CALL proc3(?, ?)") - _ <- callableStatement.setInt(1, 1024) *> callableStatement.registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) + _ <- callableStatement.setInt(1, 1024) *> callableStatement + .registerOutParameter(2, ldbc.connector.data.Types.VARCHAR) yield true - }, + } ) } From 3515ab7ae7039740fb5c165961c3f7f35cd31fce Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 20:55:05 +0900 Subject: [PATCH 65/69] Fixed CallableStatementTest --- .../ldbc/connector/CallableStatementTest.scala | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 4637b202d..d5d84b54c 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -17,13 +17,12 @@ class CallableStatementTest extends CatsEffectSuite: given Tracer[IO] = Tracer.noop[IO] private val connection = Connection[IO]( - host = "127.0.0.1", - port = 13306, - user = "ldbc", - password = Some("password"), - database = Some("connector_test"), - allowPublicKeyRetrieval = true - // ssl = SSL.Trusted + host = "127.0.0.1", + port = 13306, + user = "ldbc", + password = Some("password"), + database = Some("connector_test"), + ssl = SSL.Trusted ) test("The result of calling an empty procedure matches the specified value.") { From acc44b1b1892cdf8fe3606febd68afdc4c482449 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 21:10:44 +0900 Subject: [PATCH 66/69] Fixed CallableStatement bug --- .../net/protocol/CallableStatement.scala | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index e5c821d24..ea04ecaa3 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -632,7 +632,7 @@ object CallableStatement: currentResultSet.update(_ => results.headOption) *> resultSets.set(results.toList) *> ev.pure(results.nonEmpty) - } + } <* retrieveOutParams() else params.get .flatMap { params => @@ -1114,15 +1114,12 @@ object CallableStatement: queryBuf.append("=") acc *> params.get.flatMap { params => - params.get(param.index) match - case Some(parameter) => - val sql = (queryBuf.toString.toCharArray ++ parameter.sql).mkString - sendQuery(sql).flatMap { - case _: OKPacket => ev.unit - case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) - case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) - } - case None => ev.raiseError(new SQLException("Parameter not found")) + val sql = (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString + sendQuery(sql).flatMap { + case _: OKPacket => ev.unit + case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) + case _: EOFPacket => ev.raiseError(new SQLException("Unexpected EOF packet")) + } } else acc } From 43b30d12aee6a04bdfb30a045d720fdd1f4c213a Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 21:15:02 +0900 Subject: [PATCH 67/69] Added CallableStatement document --- docs/src/main/mdoc/en/09-Connector.md | 63 ++++++++++++++++++++++++++- docs/src/main/mdoc/ja/09-Connector.md | 63 ++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/docs/src/main/mdoc/en/09-Connector.md b/docs/src/main/mdoc/en/09-Connector.md index 2ef7a02d5..bdae5719b 100644 --- a/docs/src/main/mdoc/en/09-Connector.md +++ b/docs/src/main/mdoc/en/09-Connector.md @@ -776,6 +776,68 @@ connection.use { conn => This is because if you are using `PreparedStatement`, you can set multiple parameters for a single query by using the `addBatch` method after setting the query parameters. +## Stored Procedure Execution + +LDBC provides an API for executing stored procedures. + +To execute a stored procedure, use the `prepareCall` method of `Connection` to construct a `CallableStatement`. + +※ The stored procedures used are those described in the [official](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html) document. + +```sql +CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) +BEGIN + DECLARE z INT; + SET z = inOutParam + 1; + SET inOutParam = z; + + SELECT inputParam; + + SELECT CONCAT('zyxw', inputParam); +END +``` + +To execute the above stored procedure, the following would be used + +```scala +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + hasResult <- callableStatement.execute() + values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { + for + resultSet <- callableStatement.getResultSet().flatMap { + case Some(rs) => IO.pure(rs) + case None => IO.raiseError(new Exception("No result set")) + } + value <- resultSet.getString(1) + yield value + } + yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) +} +``` + +To get the value of an output parameter (a parameter you specified as OUT or INOUT when you created the stored procedure), in JDBC you must use the various `registerOutputParameter()` methods of the CallableStatement interface to specify parameters before statement execution, while LDBC will also set parameters during query execution by simply setting them using the `setXXX` method. + +However, LDBC also allows you to specify parameters using the `registerOutputParameter()` method. + +```scala +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + hasResult <- callableStatement.execute() + value <- callableStatement.getInt(2) + yield value // 2 +} +``` + +※ Note that if you specify an Out parameter with `registerOutParameter`, the value will be set at `Null` for the server if the parameter is not set with the `setXXX` method using the same index value. + ## Unsupported Feature The LDBC connector is currently an experimental feature. Therefore, the following features are not supported. @@ -783,5 +845,4 @@ We plan to provide the features as they become available. - Connection Pooling - Failover measures -- Execution of SQL Stored Procedures - etc... diff --git a/docs/src/main/mdoc/ja/09-Connector.md b/docs/src/main/mdoc/ja/09-Connector.md index 67cb3cc63..5fbfee04d 100644 --- a/docs/src/main/mdoc/ja/09-Connector.md +++ b/docs/src/main/mdoc/ja/09-Connector.md @@ -770,6 +770,68 @@ connection.use { conn => これは、`PreparedStatement`を使用している場合、クエリのパラメーターを設定した後に`addBatch`メソッドを使用することで、1つのクエリに複数のパラメーターを設定することができるためです。 +## ストアドプロシージャの実行 + +LDBCではストアドプロシージャを実行するためのAPIを提供しています。 + +ストアドプロシージャを実行するには`Connection`の`prepareCall`メソッドを使用して`CallableStatement`を構築します。 + +※ 使用するストアドプロシージャは[公式](https://dev.mysql.com/doc/connector-j/en/connector-j-usagenotes-statements-callable.html)ドキュメント記載のものを使用しています。 + +```sql +CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam INT) +BEGIN + DECLARE z INT; + SET z = inOutParam + 1; + SET inOutParam = z; + + SELECT inputParam; + + SELECT CONCAT('zyxw', inputParam); +END +``` + +上記のストアドプロシージャを実行する場合は以下のようになります。 + +```scala +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + hasResult <- callableStatement.execute() + values <- Monad[IO].whileM[List, Option[String]](callableStatement.getMoreResults()) { + for + resultSet <- callableStatement.getResultSet().flatMap { + case Some(rs) => IO.pure(rs) + case None => IO.raiseError(new Exception("No result set")) + } + value <- resultSet.getString(1) + yield value + } + yield values // List(Some("abcdefg"), Some("zyxwabcdefg")) +} +``` + +出力パラメータ(ストアド・プロシージャを作成したときにOUTまたはINOUTとして指定したパラメータ)の値を取得するには、JDBCでは、CallableStatementインターフェイスのさまざまな`registerOutputParameter()`メソッドを使用して、ステートメント実行前にパラメータを指定する必要がありますが、LDBCでは`setXXX`メソッドを使用してパラメータを設定することだけクエリ実行時にパラメーターの設定も行なってくれます。 + +ただし、LDBCでも`registerOutputParameter()`メソッドを使用してパラメータを指定することもできます。 + +```scala +connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + hasResult <- callableStatement.execute() + value <- callableStatement.getInt(2) + yield value // 2 +} +``` + +※ `registerOutParameter`でOutパラメータを指定する場合、同じindex値を使用して`setXXX`メソッドでパラメータを設定していない場合サーバーには`Null`で値が設定されることに注意してください。 + ## 未対応機能 LDBCコネクタは現在実験的な機能となります。そのため、以下の機能はサポートされていません。 @@ -777,5 +839,4 @@ LDBCコネクタは現在実験的な機能となります。そのため、以 - コネクションプーリング - フェイルオーバー対策 -- SQL ストアドプロシージャの実行 - etc... From 004dcc34b3d8bb1a2a009a2810db50249a1cabbe Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 21:16:15 +0900 Subject: [PATCH 68/69] Fixed CallableStatementTest --- .../ldbc/connector/CallableStatementTest.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index d5d84b54c..0e5935f21 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -163,6 +163,22 @@ class CallableStatementTest extends CatsEffectSuite: ) } + test("If a query is executed with the Out parameter set in advance, the execution result will match the value that was set.") { + assertIO( + connection.use { conn => + for + callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + hasResult <- callableStatement.execute() + value <- callableStatement.getInt(2) + yield value + }, + 2 + ) + } + test("The result of calling a stored function with an empty parameter argument matches the specified value.") { assertIO( connection.use { conn => From 91bcf282c2bbcb2b8332890069a878b13a4e8123 Mon Sep 17 00:00:00 2001 From: takapi327 Date: Sun, 12 May 2024 21:16:31 +0900 Subject: [PATCH 69/69] Action sbt scalafmtAll --- .../connector/net/protocol/CallableStatement.scala | 3 ++- .../ldbc/connector/CallableStatementTest.scala | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala index ea04ecaa3..547d42742 100644 --- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala +++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/CallableStatement.scala @@ -1114,7 +1114,8 @@ object CallableStatement: queryBuf.append("=") acc *> params.get.flatMap { params => - val sql = (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString + val sql = + (queryBuf.toString.toCharArray ++ params.get(param.index).fold("NULL".toCharArray)(_.sql)).mkString sendQuery(sql).flatMap { case _: OKPacket => ev.unit case error: ERRPacket => ev.raiseError(error.toException("Failed to execute query", sql)) diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala index 0e5935f21..e8049def6 100644 --- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala +++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/CallableStatementTest.scala @@ -163,16 +163,18 @@ class CallableStatementTest extends CatsEffectSuite: ) } - test("If a query is executed with the Out parameter set in advance, the execution result will match the value that was set.") { + test( + "If a query is executed with the Out parameter set in advance, the execution result will match the value that was set." + ) { assertIO( connection.use { conn => for callableStatement <- conn.prepareCall("CALL demoSp(?, ?)") - _ <- callableStatement.setString(1, "abcdefg") - _ <- callableStatement.setInt(2, 1) - _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) - hasResult <- callableStatement.execute() - value <- callableStatement.getInt(2) + _ <- callableStatement.setString(1, "abcdefg") + _ <- callableStatement.setInt(2, 1) + _ <- callableStatement.registerOutParameter(2, ldbc.connector.data.Types.INTEGER) + hasResult <- callableStatement.execute() + value <- callableStatement.getInt(2) yield value }, 2