diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 47eb952d3..6713fb431 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -444,7 +444,7 @@ jobs:
matrix:
os: [ubuntu-22.04]
scala: [3.3.8]
- java: [temurin@17, temurin@21]
+ java: [corretto@17, corretto@21]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
diff --git a/README.md b/README.md
index 297980087..81b0329a1 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
[](https://en.wikipedia.org/wiki/MIT_License)
[](https://github.com/lampepfl/dotty)
[](https://typelevel.org/projects/affiliate/)
-[](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3)
+[](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3)
[](https://central.sonatype.com/artifact/io.github.takapi327/ldbc-dsl_3)
[](https://index.scala-lang.org/takapi327/ldbc)
[](https://index.scala-lang.org/takapi327/ldbc)
@@ -30,21 +30,21 @@ ldbc is available on the JVM, Scala.js, and ScalaNative
| Module / Platform | JVM | Scala Native | Scala.js | Scaladoc |
|----------------------------------|:---:|:------------:|:--------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
-| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
-| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
-| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
-| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
-| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
-| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
-| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
-| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
-| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
-| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
-| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
-| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
-| `ldbc-testkit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit_3) |
-| `ldbc-testkit-munit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit-munit_3) |
+| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
+| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
+| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
+| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
+| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
+| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
+| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
+| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
+| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
+| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
+| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
+| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
+| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
+| `ldbc-testkit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit_3) |
+| `ldbc-testkit-munit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit-munit_3) |
## Performance
diff --git a/build.sbt b/build.sbt
index c4e2f38fc..7a19acc1f 100644
--- a/build.sbt
+++ b/build.sbt
@@ -31,8 +31,7 @@ ThisBuild / githubWorkflowBuildMatrixExclusions ++= Seq(
MatrixExclude(Map("os" -> "ubuntu-24.04", "project" -> "ldbcJS")),
MatrixExclude(Map("os" -> "ubuntu-22.04", "project" -> "ldbcNative"))
)
-ThisBuild / githubWorkflowBuildPreamble ++= List(dockerRun, brewUpdate) ++ nativeBrewInstallWorkflowSteps.value
-ThisBuild / nativeBrewInstallCond := Some("matrix.project == 'ldbcNative'")
+ThisBuild / githubWorkflowBuildPreamble ++= List(dockerRun, brewUpdate, brewInstall)
ThisBuild / githubWorkflowAddedJobs ++= Seq(sbtScripted.value, sbtCoverageReport.value)
ThisBuild / githubWorkflowBuildPostamble += dockerStop
ThisBuild / githubWorkflowBuild ~= { steps =>
@@ -48,7 +47,12 @@ ThisBuild / mimaBinaryIssueFilters ++= List(
ProblemFilters.exclude[IncompatibleMethTypeProblem]("ldbc.connector.net.packet.response.ResultSetRowPacket.decoder"),
ProblemFilters.exclude[DirectMissingMethodProblem](
"ldbc.connector.net.packet.response.BinaryProtocolResultSetRowPacket.decodeValue"
- )
+ ),
+ // New Statement methods introduced in line with MySQL Connector/J 9.7.0 (WL #17215)
+ ProblemFilters.exclude[ReversedMissingMethodProblem]("ldbc.sql.Statement.enquoteLiteral"),
+ ProblemFilters.exclude[ReversedMissingMethodProblem]("ldbc.sql.Statement.enquoteIdentifier"),
+ ProblemFilters.exclude[ReversedMissingMethodProblem]("ldbc.sql.Statement.enquoteNCharLiteral"),
+ ProblemFilters.exclude[ReversedMissingMethodProblem]("ldbc.sql.Statement.isSimpleIdentifier")
)
lazy val sql = crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -76,7 +80,7 @@ lazy val dsl = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.module("dsl", "Projects that provide a way to connect to the database")
.settings(
libraryDependencies ++= Seq(
- "org.typelevel" %%% "twiddles-core" % "0.10.0",
+ "org.typelevel" %%% "twiddles-core" % "1.1.0",
"co.fs2" %%% "fs2-core" % "3.13.0",
"org.typelevel" %%% "munit-cats-effect" % "2.2.0" % Test
)
@@ -144,8 +148,7 @@ lazy val authenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativePlat
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
lazy val connector = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
@@ -164,7 +167,7 @@ lazy val connector = crossProject(JVMPlatform, JSPlatform, NativePlatform)
"org.typelevel" %%% "otel4s-semconv-experimental" % "1.1.0",
"org.typelevel" %%% "otel4s-semconv-metrics" % "1.1.0",
"org.typelevel" %%% "otel4s-semconv-metrics-experimental" % "1.1.0",
- "org.typelevel" %%% "twiddles-core" % "0.10.0",
+ "org.typelevel" %%% "twiddles-core" % "1.1.0",
"org.typelevel" %%% "munit-cats-effect" % "2.2.0" % Test,
"org.typelevel" %%% "otel4s-sdk-testkit" % "0.19.1" % Test
),
@@ -180,8 +183,7 @@ lazy val connector = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
.dependsOn(core, authenticationPlugin)
lazy val awsAuthenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -198,12 +200,13 @@ lazy val awsAuthenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativeP
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
.dependsOn(authenticationPlugin)
lazy val plugin = LepusSbtPluginProject("ldbc-plugin", "plugin")
.settings(description := "Projects that provide sbt plug-ins")
+ .settings(addSbtPlugin("com.github.sbt" % "sbt2-compat" % "0.2.0"))
+ .settings(scalacOptions += "-Wconf:src=.*Settings\\.scala&msg=unused import:s")
.settings((Compile / sourceGenerators) += Def.task {
Generator.version(
version = version.value,
@@ -224,8 +227,7 @@ lazy val testkit = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
.dependsOn(connector)
lazy val testkitMunit = crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -239,8 +241,7 @@ lazy val testkitMunit = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
.dependsOn(testkit, dsl % Test)
lazy val zioInterop = crossProject(JVMPlatform, JSPlatform)
@@ -278,7 +279,7 @@ lazy val tests = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.defaultSettings
.jvmSettings(
Test / fork := true,
- libraryDependencies += "com.mysql" % "mysql-connector-j" % "9.6.0" % Test
+ libraryDependencies += "com.mysql" % "mysql-connector-j" % "9.7.0" % Test
)
.jvmConfigure(_ dependsOn jdbcConnector.jvm)
.jsSettings(
@@ -290,8 +291,7 @@ lazy val tests = crossProject(JVMPlatform, JSPlatform, NativePlatform)
}
}
)
- .nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
- .nativeSettings(Test / nativeBrewFormulas += "s2n")
+ .nativeSettings(Brew.nativeSettings)
.dependsOn(connector, queryBuilder, schema)
.enablePlugins(NoPublishPlugin)
@@ -304,7 +304,7 @@ lazy val benchmark = (project in file("benchmark"))
.settings(
libraryDependencies ++= Seq(
"org.scala-lang" %% "scala3-compiler" % scala3,
- "com.mysql" % "mysql-connector-j" % "9.6.0",
+ "com.mysql" % "mysql-connector-j" % "9.7.0",
"org.typelevel" %% "doobie-core" % "1.0.0-RC13",
"com.typesafe.slick" %% "slick" % "3.6.1",
"com.zaxxer" % "HikariCP" % "7.1.0"
@@ -335,7 +335,7 @@ lazy val hikariCPExample = crossProject(JVMPlatform)
.settings(
libraryDependencies ++= Seq(
"com.zaxxer" % "HikariCP" % "7.1.0",
- "com.mysql" % "mysql-connector-j" % "9.6.0"
+ "com.mysql" % "mysql-connector-j" % "9.7.0"
)
)
.dependsOn(jdbcConnector, dsl)
@@ -399,7 +399,7 @@ lazy val docs = (project in file("docs"))
mdocVariables ++= Map(
"ORGANIZATION" -> organization.value,
"SCALA_VERSION" -> scalaVersion.value,
- "MYSQL_VERSION" -> "9.6.0"
+ "MYSQL_VERSION" -> "9.7.0"
),
laikaTheme := LaikaSettings.helium.value,
laikaConfig := LaikaConfig.defaults.withRawContent,
diff --git a/docs/src/main/mdoc/README.md b/docs/src/main/mdoc/README.md
index ff03876c2..f7afdda2a 100644
--- a/docs/src/main/mdoc/README.md
+++ b/docs/src/main/mdoc/README.md
@@ -39,21 +39,21 @@ ldbc is available on the JVM, Scala.js, and ScalaNative
| Module / Platform | JVM | Scala Native | Scala.js | Scaladoc |
|----------------------------------|:---:|:------------:|:--------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
-| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
-| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
-| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
-| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
-| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
-| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
-| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
-| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
-| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
-| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
-| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
-| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
-| `ldbc-testkit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit_3) |
-| `ldbc-testkit-munit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit-munit_3) |
+| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
+| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
+| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
+| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
+| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
+| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
+| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
+| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
+| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
+| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
+| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
+| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
+| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
+| `ldbc-testkit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit_3) |
+| `ldbc-testkit-munit` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-testkit-munit_3) |
## Quick Start
diff --git a/docs/src/main/mdoc/index.md b/docs/src/main/mdoc/index.md
index 3a142898c..ad6e6b7f3 100644
--- a/docs/src/main/mdoc/index.md
+++ b/docs/src/main/mdoc/index.md
@@ -38,19 +38,19 @@ ldbc is available on the JVM, Scala.js, and ScalaNative
| Module / Platform | JVM | Scala Native | Scala.js | Scaladoc |
|----------------------------------|:---:|:------------:|:--------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
-| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
-| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
-| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
-| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
-| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
-| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
-| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
-| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
-| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
-| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
-| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
-| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
+| `ldbc-sql` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-sql_3) |
+| `ldbc-core` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-core_3) |
+| `ldbc-connector` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-connector_3) |
+| `jdbc-connector` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/jdbc-connector_3) |
+| `ldbc-dsl` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3) |
+| `ldbc-statement` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-statement_3) |
+| `ldbc-query-builder` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-query-builder_3) |
+| `ldbc-schema` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-schema_3) |
+| `ldbc-codegen` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-codegen_3) |
+| `ldbc-plugin` | ✅ | ❌ | ❌ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-plugin_2.12_1.0) |
+| `ldbc-zio-interop` | ✅ | ❌ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-zio-interop_3) |
+| `ldbc-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-authentication-plugin_3) |
+| `ldbc-aws-authentication-plugin` | ✅ | ✅ | ✅ | [](https://javadoc.io/doc/io.github.takapi327/ldbc-aws-authentication-plugin_3) |
## Quick Start
diff --git a/docs/src/main/mdoc/older-versions.md b/docs/src/main/mdoc/older-versions.md
index 6f9d59947..fdb9ec4f0 100644
--- a/docs/src/main/mdoc/older-versions.md
+++ b/docs/src/main/mdoc/older-versions.md
@@ -12,11 +12,12 @@ Older Versions
Documentation for versions 0.1 through 0.2 is no longer available on this site,
but you can still browse the documentation sources and scaladoc as linked in the table below.
-It's strongly recommended to upgrade to the latest release (0.7), which has increased functionality and robustness
+It's strongly recommended to upgrade to the latest release (0.8), which has increased functionality and robustness
and now also more API stability than those older releases.
| Release | Date | Scala Versions | sbt plugin | Scala.js | Scala Native | Doc Sources | API (Scaladoc) |
|---------|----------|----------------|------------|----------|--------------|-----------------|-----------------|
+| 0.7 | Jun 2026 | 3.3.x | 1.x | | | [Browse][doc07] | [Browse][api07] |
| 0.6 | Mar 2026 | 3.3.x | 1.x | | | [Browse][doc06] | [Browse][api06] |
| 0.5 | Dec 2025 | 3.3.x | 1.x | | | [Browse][doc05] | [Browse][api05] |
| 0.4 | Oct 2025 | 3.3.x | 1.x | | | [Browse][doc04] | [Browse][api04] |
@@ -29,7 +30,9 @@ and now also more API stability than those older releases.
[doc04]: https://takapi327.github.io/ldbc/0.4/
[doc05]: https://takapi327.github.io/ldbc/0.5/
[doc06]: https://takapi327.github.io/ldbc/0.6/
+[doc07]: https://takapi327.github.io/ldbc/0.7/
+[api07]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.7.0/index.html
[api06]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.6.0/index.html
[api05]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.5.0/index.html
[api04]: https://javadoc.io/doc/io.github.takapi327/ldbc-dsl_3/0.4.0/index.html
diff --git a/module/jdbc-connector/src/main/scala/jdbc/connector/PreparedStatementImpl.scala b/module/jdbc-connector/src/main/scala/jdbc/connector/PreparedStatementImpl.scala
index 586546ef9..ea472919b 100644
--- a/module/jdbc-connector/src/main/scala/jdbc/connector/PreparedStatementImpl.scala
+++ b/module/jdbc-connector/src/main/scala/jdbc/connector/PreparedStatementImpl.scala
@@ -108,3 +108,15 @@ private[jdbc] open class PreparedStatementImpl[F[_]: Sync](statement: java.sql.P
override def executeLargeBatch(): F[Array[Long]] =
Sync[F].blocking(statement.executeLargeBatch())
+
+ override def enquoteLiteral(value: String): F[String] =
+ Sync[F].blocking(Enquoting.enquoteLiteral(statement, value))
+
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): F[String] =
+ Sync[F].blocking(Enquoting.enquoteIdentifier(statement, identifier, alwaysQuote))
+
+ override def enquoteNCharLiteral(value: String): F[String] =
+ Sync[F].blocking(Enquoting.enquoteNCharLiteral(statement, value))
+
+ override def isSimpleIdentifier(identifier: String): F[Boolean] =
+ Sync[F].blocking(Enquoting.isSimpleIdentifier(statement, identifier))
diff --git a/module/jdbc-connector/src/main/scala/jdbc/connector/StatementImpl.scala b/module/jdbc-connector/src/main/scala/jdbc/connector/StatementImpl.scala
index 3e876a3e7..cc3b2fd7c 100644
--- a/module/jdbc-connector/src/main/scala/jdbc/connector/StatementImpl.scala
+++ b/module/jdbc-connector/src/main/scala/jdbc/connector/StatementImpl.scala
@@ -61,3 +61,46 @@ private[jdbc] case class StatementImpl[F[_]: Sync](statement: java.sql.Statement
override def executeLargeBatch(): F[Array[Long]] =
Sync[F].blocking(statement.executeLargeBatch())
+
+ override def enquoteLiteral(value: String): F[String] =
+ Sync[F].blocking(Enquoting.enquoteLiteral(statement, value))
+
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): F[String] =
+ Sync[F].blocking(Enquoting.enquoteIdentifier(statement, identifier, alwaysQuote))
+
+ override def enquoteNCharLiteral(value: String): F[String] =
+ Sync[F].blocking(Enquoting.enquoteNCharLiteral(statement, value))
+
+ override def isSimpleIdentifier(identifier: String): F[Boolean] =
+ Sync[F].blocking(Enquoting.isSimpleIdentifier(statement, identifier))
+
+/**
+ * `java.sql.Statement` gained the enquote methods in Java 9 (JDBC 4.3), while this module is
+ * compiled against the Java 8 API, so they must be invoked reflectively through the
+ * `java.sql.Statement` interface.
+ */
+private[connector] object Enquoting:
+
+ private def method(name: String, parameterTypes: Class[?]*): java.lang.reflect.Method =
+ classOf[java.sql.Statement].getMethod(name, parameterTypes*)
+
+ private lazy val enquoteLiteralMethod = method("enquoteLiteral", classOf[String])
+ private lazy val enquoteIdentifierMethod = method("enquoteIdentifier", classOf[String], java.lang.Boolean.TYPE)
+ private lazy val enquoteNCharLiteralMethod = method("enquoteNCharLiteral", classOf[String])
+ private lazy val isSimpleIdentifierMethod = method("isSimpleIdentifier", classOf[String])
+
+ private def invoke[A](statement: java.sql.Statement, method: java.lang.reflect.Method, args: AnyRef*): A =
+ try method.invoke(statement, args*).asInstanceOf[A]
+ catch case e: java.lang.reflect.InvocationTargetException => throw e.getCause
+
+ def enquoteLiteral(statement: java.sql.Statement, value: String): String =
+ invoke[String](statement, enquoteLiteralMethod, value)
+
+ def enquoteIdentifier(statement: java.sql.Statement, identifier: String, alwaysQuote: Boolean): String =
+ invoke[String](statement, enquoteIdentifierMethod, identifier, java.lang.Boolean.valueOf(alwaysQuote))
+
+ def enquoteNCharLiteral(statement: java.sql.Statement, value: String): String =
+ invoke[String](statement, enquoteNCharLiteralMethod, value)
+
+ def isSimpleIdentifier(statement: java.sql.Statement, identifier: String): Boolean =
+ invoke[java.lang.Boolean](statement, isSimpleIdentifierMethod, identifier).booleanValue()
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Constants.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Constants.scala
index 96f22efcc..900c9617f 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Constants.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/data/Constants.scala
@@ -14,4 +14,4 @@ import ldbc.connector.util.Version
object Constants:
val DRIVER_NAME: String = "MySQL Connector/L"
- val DRIVER_VERSION: Version = Version(0, 7, 1)
+ val DRIVER_VERSION: Version = Version(0, 8, 0)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLDataException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLDataException.scala
index f11e0d429..742222fb5 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLDataException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLDataException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} thrown when the SQLState class value
* is '22', or under vendor-specified conditions. This indicates
@@ -18,10 +14,9 @@ import ldbc.connector.data.Parameter
*/
class SQLDataException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLException.scala
index 97c0103f5..70c0a952f 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLException.scala
@@ -6,13 +6,10 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
import cats.syntax.all.*
import org.typelevel.otel4s.Attribute
-import ldbc.connector.data.Parameter
import ldbc.connector.util.Pretty
/**
@@ -38,12 +35,11 @@ import ldbc.connector.util.Pretty
*/
class SQLException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
) extends Exception:
/**
@@ -75,12 +71,6 @@ class SQLException(
detail.foreach(a => builder += Attribute("error.detail", a))
hint.foreach(a => builder += Attribute("error.hint", a))
- params.foreach {
- case (i, p) =>
- builder += Attribute(s"error.parameter.$i.type", p.columnDataType.name)
- builder += Attribute(s"error.parameter.$i.value", p.toString)
- }
-
builder.result()
protected def width = 80 // wrap here
@@ -117,23 +107,8 @@ class SQLException(
|""".stripMargin
}
- protected def args: String =
-
- def formatValue(s: String) =
- s"${ Console.GREEN }$s${ Console.RESET }"
-
- if params.isEmpty then ""
- else
- s"""|and the arguments were
- |
- | ${ params
- .map { case (i, p) => f"$$$i ${ p.columnDataType.name }%-10s ${ formatValue(p.toString) }" }
- .mkString("\n| ") }
- |
- |""".stripMargin
-
protected def sections: List[String] =
- List(header, statement, args)
+ List(header, statement)
final override def getMessage: String =
sections.combineAll.linesIterator
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationException.scala
index 607a8ea18..fbb3f22e3 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} thrown when the SQLState class value
* is '23', or under vendor-specified conditions.
@@ -18,10 +14,9 @@ import ldbc.connector.data.Parameter
*/
class SQLIntegrityConstraintViolationException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLInvalidAuthorizationSpecException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLInvalidAuthorizationSpecException.scala
index e2ed46e6d..dd9e45994 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLInvalidAuthorizationSpecException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLInvalidAuthorizationSpecException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} thrown when the SQLState class value
* is '28', or under vendor-specified conditions. This indicates that
@@ -18,10 +14,9 @@ import ldbc.connector.data.Parameter
*/
class SQLInvalidAuthorizationSpecException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLNonTransientException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLNonTransientException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLNonTransientException.scala
index ffbf1747d..9b8ada449 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLNonTransientException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLNonTransientException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} thrown when an instance where a retry
* of the same operation would fail unless the cause of the SQLException
@@ -17,10 +13,9 @@ import ldbc.connector.data.Parameter
*/
class SQLNonTransientException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransactionRollbackException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransactionRollbackException.scala
index 6023015b7..76c33b171 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransactionRollbackException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransactionRollbackException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} thrown when the SQLState class value
* is '40', or under vendor-specified conditions. This indicates that the
@@ -18,10 +14,9 @@ import ldbc.connector.data.Parameter
*/
class SQLTransactionRollbackException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLTransientException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLTransientException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientConnectionException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientConnectionException.scala
index 00981018b..96d320449 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientConnectionException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientConnectionException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} for the SQLState class
* value '08', or under vendor-specified conditions. This indicates
@@ -18,10 +14,9 @@ import ldbc.connector.data.Parameter
*/
class SQLTransientConnectionException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLTransientException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLTransientException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientException.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientException.scala
index 1b679a4da..05007b421 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientException.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/exception/SQLTransientException.scala
@@ -6,10 +6,6 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
-import ldbc.connector.data.Parameter
-
/**
* The subclass of {@link SQLException} is thrown in situations where a
* previously failed operation might be able to succeed when the operation is
@@ -17,10 +13,9 @@ import ldbc.connector.data.Parameter
*/
class SQLTransientException(
message: String,
- sqlState: Option[String] = None,
- vendorCode: Option[Int] = None,
- sql: Option[String] = None,
- detail: Option[String] = None,
- hint: Option[String] = None,
- params: SortedMap[Int, Parameter] = SortedMap.empty
-) extends SQLException(message, sqlState, vendorCode, sql, detail, hint, params)
+ sqlState: Option[String] = None,
+ vendorCode: Option[Int] = None,
+ sql: Option[String] = None,
+ detail: Option[String] = None,
+ hint: Option[String] = None
+) extends SQLException(message, sqlState, vendorCode, sql, detail, hint)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/packet/response/ERRPacket.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/packet/response/ERRPacket.scala
index 683221dba..f4ac06902 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/packet/response/ERRPacket.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/packet/response/ERRPacket.scala
@@ -7,8 +7,6 @@
package ldbc.connector.net.packet
package response
-import scala.collection.immutable.SortedMap
-
import scodec.*
import scodec.codecs.*
@@ -75,8 +73,7 @@ case class ERRPacket(
def toException(
sql: Option[String],
- detail: Option[String],
- params: SortedMap[Int, Parameter] = SortedMap.empty
+ detail: Option[String]
): SQLException =
sqlState match
case Some(SQLState.TRANSIENT_CONNECTION_EXCEPTION) =>
@@ -85,8 +82,7 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case Some(SQLState.DATA_EXCEPTION) =>
SQLDataException(
@@ -94,8 +90,7 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case Some(SQLState.INVALID_AUTHORIZATION_SPEC_EXCEPTION) =>
SQLInvalidAuthorizationSpecException(
@@ -103,8 +98,7 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case Some(SQLState.INTEGRITY_CONSTRAINT_VIOLATION_EXCEPTION) =>
SQLIntegrityConstraintViolationException(
@@ -112,8 +106,7 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case Some(SQLState.TRANSACTION_ROLLBACK_EXCEPTION) =>
SQLTransactionRollbackException(
@@ -121,8 +114,7 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case Some(SQLState.SYNTAX_ERROR_EXCEPTION) =>
SQLSyntaxErrorException(
@@ -146,16 +138,14 @@ case class ERRPacket(
sqlState = sqlState,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
case None =>
SQLException(
message = errorMessage,
vendorCode = Some(errorCode),
sql = sql,
- detail = detail,
- params = params
+ detail = detail
)
def toException: SQLException = toException(None, None)
diff --git a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/StatementImpl.scala b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/StatementImpl.scala
index 6eab73841..12f384ebd 100644
--- a/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/StatementImpl.scala
+++ b/module/ldbc-connector/shared/src/main/scala/ldbc/connector/net/protocol/StatementImpl.scala
@@ -26,6 +26,7 @@ import ldbc.connector.net.packet.request.*
import ldbc.connector.net.packet.response.*
import ldbc.connector.net.Protocol
import ldbc.connector.telemetry.*
+import ldbc.connector.util.StringHelper
import ldbc.connector.ResultSetImpl
private[ldbc] case class StatementImpl[F[_]: Exchange: Tracer: Sync](
@@ -432,6 +433,30 @@ object StatementImpl:
override def getFetchSize(): F[Int] = fetchSize.get
+ override def enquoteLiteral(value: String): F[String] =
+ if value == null then F.pure("NULL")
+ else F.pure(StringHelper.enquoteLiteral(value, ansiQuotesEnabled, backslashEscapesEnabled))
+
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): F[String] =
+ if identifier == null then F.raiseError(new SQLException("Value 'null' is not a valid identifier."))
+ else if StringHelper.isSimpleIdentifier(identifier) then
+ val quoteChar = if ansiQuotesEnabled then '"' else '`'
+ F.pure(if alwaysQuote then s"$quoteChar$identifier$quoteChar" else identifier)
+ else F.pure(StringHelper.enquoteIdentifier(identifier, ansiQuotesEnabled))
+
+ override def enquoteNCharLiteral(value: String): F[String] =
+ if value == null then F.pure("NULL")
+ else F.pure(StringHelper.enquoteNCharLiteral(value, backslashEscapesEnabled))
+
+ override def isSimpleIdentifier(identifier: String): F[Boolean] =
+ F.pure(StringHelper.isSimpleIdentifier(identifier))
+
+ private def sqlMode: String = serverVariables.getOrElse("sql_mode", "")
+
+ private def ansiQuotesEnabled: Boolean = sqlMode.contains("ANSI_QUOTES")
+
+ private def backslashEscapesEnabled: Boolean = !sqlMode.contains("NO_BACKSLASH_ESCAPES")
+
protected def checkClosed(): F[Unit] =
isClosed().ifM(
close() *> F.raiseError(new SQLException("No operations allowed after statement closed.")),
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 bcd9609bf..ea66e8c63 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
@@ -84,3 +84,422 @@ object StringHelper:
*/
def hasWildcards(src: String): Boolean =
indexOfIgnoreCase(0, src, "%") != -1 || indexOfIgnoreCase(0, src, "_") != -1
+
+ /**
+ * Maximum length of a MySQL identifier.
+ *
+ * @see https://dev.mysql.com/doc/refman/en/identifier-length.html
+ */
+ private val MAX_IDENTIFIER_LENGTH = 64
+
+ /**
+ * MySQL reserved words. Reserved words cannot be used as identifiers without quoting.
+ *
+ * @see https://dev.mysql.com/doc/refman/en/keywords.html
+ */
+ private val MYSQL_RESERVED_WORDS: Set[String] = Set(
+ "ACCESSIBLE",
+ "ADD",
+ "ALL",
+ "ALTER",
+ "ANALYZE",
+ "AND",
+ "AS",
+ "ASC",
+ "ASENSITIVE",
+ "BEFORE",
+ "BETWEEN",
+ "BIGINT",
+ "BINARY",
+ "BLOB",
+ "BOTH",
+ "BY",
+ "CALL",
+ "CASCADE",
+ "CASE",
+ "CHANGE",
+ "CHAR",
+ "CHARACTER",
+ "CHECK",
+ "COLLATE",
+ "COLUMN",
+ "CONDITION",
+ "CONSTRAINT",
+ "CONTINUE",
+ "CONVERT",
+ "CREATE",
+ "CROSS",
+ "CUBE",
+ "CUME_DIST",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP",
+ "CURRENT_USER",
+ "CURSOR",
+ "DATABASE",
+ "DATABASES",
+ "DAY_HOUR",
+ "DAY_MICROSECOND",
+ "DAY_MINUTE",
+ "DAY_SECOND",
+ "DEC",
+ "DECIMAL",
+ "DECLARE",
+ "DEFAULT",
+ "DELAYED",
+ "DELETE",
+ "DENSE_RANK",
+ "DESC",
+ "DESCRIBE",
+ "DETERMINISTIC",
+ "DISTINCT",
+ "DISTINCTROW",
+ "DIV",
+ "DOUBLE",
+ "DROP",
+ "DUAL",
+ "EACH",
+ "ELSE",
+ "ELSEIF",
+ "EMPTY",
+ "ENCLOSED",
+ "ESCAPED",
+ "EXCEPT",
+ "EXISTS",
+ "EXIT",
+ "EXPLAIN",
+ "FALSE",
+ "FETCH",
+ "FIRST_VALUE",
+ "FLOAT",
+ "FLOAT4",
+ "FLOAT8",
+ "FOR",
+ "FORCE",
+ "FOREIGN",
+ "FROM",
+ "FULLTEXT",
+ "FUNCTION",
+ "GENERATED",
+ "GET",
+ "GRANT",
+ "GROUP",
+ "GROUPING",
+ "GROUPS",
+ "HAVING",
+ "HIGH_PRIORITY",
+ "HOUR_MICROSECOND",
+ "HOUR_MINUTE",
+ "HOUR_SECOND",
+ "IF",
+ "IGNORE",
+ "IN",
+ "INDEX",
+ "INFILE",
+ "INNER",
+ "INOUT",
+ "INSENSITIVE",
+ "INSERT",
+ "INT",
+ "INT1",
+ "INT2",
+ "INT3",
+ "INT4",
+ "INT8",
+ "INTEGER",
+ "INTERSECT",
+ "INTERVAL",
+ "INTO",
+ "IO_AFTER_GTIDS",
+ "IO_BEFORE_GTIDS",
+ "IS",
+ "ITERATE",
+ "JOIN",
+ "JSON_TABLE",
+ "KEY",
+ "KEYS",
+ "KILL",
+ "LAG",
+ "LAST_VALUE",
+ "LATERAL",
+ "LEAD",
+ "LEADING",
+ "LEAVE",
+ "LEFT",
+ "LIKE",
+ "LIMIT",
+ "LINEAR",
+ "LINES",
+ "LOAD",
+ "LOCALTIME",
+ "LOCALTIMESTAMP",
+ "LOCK",
+ "LONG",
+ "LONGBLOB",
+ "LONGTEXT",
+ "LOOP",
+ "LOW_PRIORITY",
+ "MANUAL",
+ "MASTER_BIND",
+ "MASTER_SSL_VERIFY_SERVER_CERT",
+ "MATCH",
+ "MAXVALUE",
+ "MEDIUMBLOB",
+ "MEDIUMINT",
+ "MEDIUMTEXT",
+ "MIDDLEINT",
+ "MINUTE_MICROSECOND",
+ "MINUTE_SECOND",
+ "MOD",
+ "MODIFIES",
+ "NATURAL",
+ "NOT",
+ "NO_WRITE_TO_BINLOG",
+ "NTH_VALUE",
+ "NTILE",
+ "NULL",
+ "NUMERIC",
+ "OF",
+ "ON",
+ "OPTIMIZE",
+ "OPTIMIZER_COSTS",
+ "OPTION",
+ "OPTIONALLY",
+ "OR",
+ "ORDER",
+ "OUT",
+ "OUTER",
+ "OUTFILE",
+ "OVER",
+ "PARALLEL",
+ "PARTITION",
+ "PERCENT_RANK",
+ "PRECISION",
+ "PRIMARY",
+ "PROCEDURE",
+ "PURGE",
+ "QUALIFY",
+ "RANGE",
+ "RANK",
+ "READ",
+ "READS",
+ "READ_WRITE",
+ "REAL",
+ "RECURSIVE",
+ "REFERENCES",
+ "REGEXP",
+ "RELEASE",
+ "RENAME",
+ "REPEAT",
+ "REPLACE",
+ "REQUIRE",
+ "RESIGNAL",
+ "RESTRICT",
+ "RETURN",
+ "REVOKE",
+ "RIGHT",
+ "RLIKE",
+ "ROW",
+ "ROWS",
+ "ROW_NUMBER",
+ "SCHEMA",
+ "SCHEMAS",
+ "SECOND_MICROSECOND",
+ "SELECT",
+ "SENSITIVE",
+ "SEPARATOR",
+ "SET",
+ "SHOW",
+ "SIGNAL",
+ "SMALLINT",
+ "SPATIAL",
+ "SPECIFIC",
+ "SQL",
+ "SQLEXCEPTION",
+ "SQLSTATE",
+ "SQLWARNING",
+ "SQL_BIG_RESULT",
+ "SQL_CALC_FOUND_ROWS",
+ "SQL_SMALL_RESULT",
+ "SSL",
+ "STARTING",
+ "STORED",
+ "STRAIGHT_JOIN",
+ "SYSTEM",
+ "TABLE",
+ "TABLESAMPLE",
+ "TERMINATED",
+ "THEN",
+ "TINYBLOB",
+ "TINYINT",
+ "TINYTEXT",
+ "TO",
+ "TRAILING",
+ "TRIGGER",
+ "TRUE",
+ "UNDO",
+ "UNION",
+ "UNIQUE",
+ "UNLOCK",
+ "UNSIGNED",
+ "UPDATE",
+ "USAGE",
+ "USE",
+ "USING",
+ "UTC_DATE",
+ "UTC_TIME",
+ "UTC_TIMESTAMP",
+ "VALUES",
+ "VARBINARY",
+ "VARCHAR",
+ "VARCHARACTER",
+ "VARYING",
+ "VIRTUAL",
+ "WHEN",
+ "WHERE",
+ "WHILE",
+ "WINDOW",
+ "WITH",
+ "WRITE",
+ "XOR",
+ "YEAR_MONTH",
+ "ZEROFILL"
+ )
+
+ /**
+ * Tests whether the given character is allowed in an unquoted MySQL identifier.
+ *
+ * Permitted characters are ASCII letters and digits, '$', '_' and extended
+ * characters (U+0080 and above).
+ *
+ * @see https://dev.mysql.com/doc/refman/en/identifiers.html
+ */
+ def isValidIdentifierChar(ch: Char): Boolean =
+ (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
+ ch == '$' || ch == '_' || ch >= 0x0080
+
+ /**
+ * Checks whether the supplied string is a "simple" MySQL identifier, meaning it can be
+ * used in SQL without quoting: non-empty, at most 64 characters long, consisting only of
+ * characters permitted in unquoted identifiers, not consisting solely of digits and not a
+ * reserved word.
+ */
+ def isSimpleIdentifier(identifier: String): Boolean =
+ identifier != null && identifier.nonEmpty && identifier.length <= MAX_IDENTIFIER_LENGTH &&
+ identifier.forall(isValidIdentifierChar) &&
+ !identifier.forall(Character.isDigit) &&
+ !MYSQL_RESERVED_WORDS.contains(identifier.toUpperCase(java.util.Locale.ROOT))
+
+ /**
+ * Renders the given value as a single-quoted SQL string literal.
+ *
+ * If the value is already surrounded by valid literal delimiters and its interior quoting
+ * is well formed, it is returned unchanged. Otherwise a newly quoted literal is generated,
+ * doubling every unescaped single quote and escaping a trailing lone backslash.
+ *
+ * @param value
+ * the raw value, or an already quoted literal
+ * @param ansiQuotes
+ * whether the ANSI_QUOTES SQL mode is enabled; when disabled, a double-quoted value is
+ * also accepted as an already quoted literal
+ * @param backslashEscapes
+ * whether backslash works as an escape character (i.e. NO_BACKSLASH_ESCAPES is disabled)
+ */
+ def enquoteLiteral(value: String, ansiQuotes: Boolean, backslashEscapes: Boolean): String =
+ enquote(value, '\'', backslashEscapes, c => c == '\'' || (!ansiQuotes && c == '"'))
+
+ /**
+ * Renders the given identifier as a quoted SQL identifier.
+ *
+ * If the identifier is already surrounded by valid identifier delimiters and its interior
+ * quoting is well formed, it is returned unchanged. Otherwise it is quoted with the
+ * identifier quote character (backtick, or double quote when ANSI_QUOTES is enabled).
+ *
+ * Note that backslash never works as an escape character inside quoted identifiers.
+ *
+ * @param identifier
+ * the raw identifier, or an already quoted identifier
+ * @param ansiQuotes
+ * whether the ANSI_QUOTES SQL mode is enabled
+ */
+ def enquoteIdentifier(identifier: String, ansiQuotes: Boolean): String =
+ enquote(identifier, if ansiQuotes then '"' else '`', false, c => c == '`' || (ansiQuotes && c == '"'))
+
+ /**
+ * Renders the given value as a national character set literal (N'...').
+ *
+ * If the value is already in N'...' form with well formed quoting, it is returned
+ * unchanged (normalized to an upper case N prefix). Otherwise the whole value is quoted
+ * as a single-quoted literal and prefixed with N. Double quotes are never valid
+ * delimiters for national character set literals, regardless of the ANSI_QUOTES SQL mode.
+ *
+ * @param value
+ * the raw value, or an already quoted N'...' literal
+ * @param backslashEscapes
+ * whether backslash works as an escape character (i.e. NO_BACKSLASH_ESCAPES is disabled)
+ */
+ def enquoteNCharLiteral(value: String, backslashEscapes: Boolean): String =
+ if value.length > 2 && (value.charAt(0) == 'N' || value.charAt(0) == 'n') && value.charAt(1) == '\'' then
+ val literalPart = value.substring(1)
+ val fixed = enquote(literalPart, '\'', backslashEscapes, _ == '\'')
+ if fixed == literalPart then "N" + fixed
+ else "N" + enquote(value, '\'', backslashEscapes, _ == '\'')
+ else "N" + enquote(value, '\'', backslashEscapes, _ == '\'')
+
+ /**
+ * Tests whether the value is a well formed quoted string: surrounded by the same accepted
+ * delimiter, every interior occurrence of that delimiter doubled or backslash-escaped, and
+ * the closing delimiter not escaped.
+ */
+ private def isWellFormedQuoted(value: String, backslashEscapes: Boolean, isQuoteDelimiter: Char => Boolean): Boolean =
+ val end = value.length - 1
+ if value.length < 2 || !isQuoteDelimiter(value.charAt(0)) || value.charAt(end) != value.charAt(0) then false
+ else
+ val delimiter = value.charAt(0)
+ var i = 1
+ var ok = true
+ while ok && i < end do
+ val c = value.charAt(i)
+ if backslashEscapes && c == '\\' then
+ // A backslash just before the closing delimiter would escape it.
+ if i == end - 1 then ok = false
+ else i += 2
+ else if c == delimiter then
+ if i + 1 < end && value.charAt(i + 1) == delimiter then i += 2
+ else ok = false
+ else i += 1
+ ok
+
+ /**
+ * Quotes the value with the given quote character, doubling every unescaped occurrence of
+ * the quote character in the value. When the value is detected to be already properly
+ * quoted with an accepted delimiter, it is returned unchanged.
+ */
+ private def enquote(
+ value: String,
+ quoteChar: Char,
+ backslashEscapes: Boolean,
+ isQuoteDelimiter: Char => Boolean
+ ): String =
+ if isWellFormedQuoted(value, backslashEscapes, isQuoteDelimiter) then value
+ else
+ val sb = new StringBuilder(value.length + 2)
+ sb.append(quoteChar)
+ var i = 0
+ while i < value.length do
+ val c = value.charAt(i)
+ if backslashEscapes && c == '\\' then
+ sb.append('\\')
+ if i + 1 < value.length then
+ sb.append(value.charAt(i + 1))
+ i += 2
+ else
+ // A lone trailing backslash would escape the closing quote.
+ sb.append('\\')
+ i += 1
+ else
+ sb.append(c)
+ if c == quoteChar then sb.append(c)
+ i += 1
+ sb.append(quoteChar)
+ sb.toString
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 e8009df9b..afafb17cf 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
@@ -487,7 +487,7 @@ class ConnectionTest extends FTestPlatform:
assertIO(
connection.use(_.getMetaData().map(_.getDriverVersion())),
- "ldbc-connector-0.7.1"
+ "ldbc-connector-0.8.0"
)
}
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLDataExceptionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLDataExceptionTest.scala
index 6a9728e7b..c9573cd72 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLDataExceptionTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLDataExceptionTest.scala
@@ -6,11 +6,8 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
import org.typelevel.otel4s.Attribute
-import ldbc.connector.data.Parameter
import ldbc.connector.FTestPlatform
class SQLDataExceptionTest extends FTestPlatform:
@@ -39,19 +36,13 @@ class SQLDataExceptionTest extends FTestPlatform:
}
test("SQLDataException.fields should return correct attributes") {
- val params = SortedMap(
- 1 -> Parameter.string("test_value"),
- 2 -> Parameter.int(42)
- )
-
val exception = new SQLDataException(
message = "Data type mismatch",
sqlState = Some("22005"),
vendorCode = Some(1366),
sql = Some("INSERT INTO numbers (int_column) VALUES (?)"),
detail = Some("Cannot convert string to integer"),
- hint = Some("Provide a valid numeric value"),
- params = params
+ hint = Some("Provide a valid numeric value")
)
val fields = exception.fields
@@ -63,11 +54,6 @@ class SQLDataExceptionTest extends FTestPlatform:
assertEquals(fields.contains(Attribute("error.detail", "Cannot convert string to integer")), true)
assertEquals(fields.contains(Attribute("error.hint", "Provide a valid numeric value")), true)
- // Check parameter attributes
- assertEquals(fields.contains(Attribute("error.parameter.1.type", "CHAR")), true)
- assertEquals(fields.contains(Attribute("error.parameter.1.value", "'test_value'")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.type", "INT")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.value", "42")), true)
}
test("labeled method should format messages correctly for SQLDataException") {
@@ -86,8 +72,7 @@ class SQLDataExceptionTest extends FTestPlatform:
message = "Invalid data format",
sqlState = Some("22007"),
vendorCode = Some(1292),
- sql = Some("INSERT INTO dates (created_at) VALUES (?)"),
- params = SortedMap(1 -> Parameter.string("not-a-date"))
+ sql = Some("INSERT INTO dates (created_at) VALUES (?)")
)
val message = exception.getMessage
@@ -97,5 +82,4 @@ class SQLDataExceptionTest extends FTestPlatform:
assert(message.contains("22007"), "Message should contain the SQL state")
assert(message.contains("1292"), "Message should contain the vendor code")
assert(message.contains("INSERT INTO dates (created_at) VALUES (?)"), "Message should contain the SQL query")
- assert(message.contains("not-a-date"), "Message should contain the parameter value")
}
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLExceptionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLExceptionTest.scala
index 6c001c328..bee7811b6 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLExceptionTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLExceptionTest.scala
@@ -6,11 +6,8 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
import org.typelevel.otel4s.Attribute
-import ldbc.connector.data.Parameter
import ldbc.connector.FTestPlatform
class SQLExceptionTest extends FTestPlatform:
@@ -39,19 +36,13 @@ class SQLExceptionTest extends FTestPlatform:
}
test("SQLException.fields should return correct attributes") {
- val params = SortedMap(
- 1 -> Parameter.string("test_value"),
- 2 -> Parameter.int(42)
- )
-
val exception = new SQLException(
message = "Query error",
sqlState = Some("42S02"),
vendorCode = Some(1146),
sql = Some("SELECT * FROM non_existent_table"),
detail = Some("Table does not exist"),
- hint = Some("Create the table first"),
- params = params
+ hint = Some("Create the table first")
)
val fields = exception.fields
@@ -63,11 +54,6 @@ class SQLExceptionTest extends FTestPlatform:
assertEquals(fields.contains(Attribute("error.detail", "Table does not exist")), true)
assertEquals(fields.contains(Attribute("error.hint", "Create the table first")), true)
- // Check parameter attributes
- assertEquals(fields.contains(Attribute("error.parameter.1.type", "CHAR")), true)
- assertEquals(fields.contains(Attribute("error.parameter.1.value", "'test_value'")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.type", "INT")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.value", "42")), true)
}
test("labeled method should format messages correctly") {
@@ -86,8 +72,7 @@ class SQLExceptionTest extends FTestPlatform:
message = "Query failed",
sqlState = Some("HY000"),
vendorCode = Some(9999),
- sql = Some("SELECT * FROM users WHERE id = ?"),
- params = SortedMap(1 -> Parameter.int(123))
+ sql = Some("SELECT * FROM users WHERE id = ?")
)
val message = exception.getMessage
@@ -97,5 +82,4 @@ class SQLExceptionTest extends FTestPlatform:
assert(message.contains("HY000"), "Message should contain the SQL state")
assert(message.contains("9999"), "Message should contain the vendor code")
assert(message.contains("SELECT * FROM users WHERE id = ?"), "Message should contain the SQL query")
- assert(message.contains("123"), "Message should contain the parameter value")
}
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationExceptionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationExceptionTest.scala
index 70549c95c..cc4de1529 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationExceptionTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLIntegrityConstraintViolationExceptionTest.scala
@@ -6,11 +6,8 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
import org.typelevel.otel4s.Attribute
-import ldbc.connector.data.Parameter
import ldbc.connector.FTestPlatform
class SQLIntegrityConstraintViolationExceptionTest extends FTestPlatform:
@@ -39,19 +36,13 @@ class SQLIntegrityConstraintViolationExceptionTest extends FTestPlatform:
}
test("SQLIntegrityConstraintViolationException.fields should return correct attributes") {
- val params = SortedMap(
- 1 -> Parameter.int(101),
- 2 -> Parameter.string("test_user")
- )
-
val exception = new SQLIntegrityConstraintViolationException(
message = "Violation of unique constraint",
sqlState = Some("23505"),
vendorCode = Some(1063),
sql = Some("INSERT INTO users (id, name) VALUES (?, ?)"),
detail = Some("Key (id)=(101) already exists"),
- hint = Some("Use a different primary key value"),
- params = params
+ hint = Some("Use a different primary key value")
)
val fields = exception.fields
@@ -63,11 +54,6 @@ class SQLIntegrityConstraintViolationExceptionTest extends FTestPlatform:
assertEquals(fields.contains(Attribute("error.detail", "Key (id)=(101) already exists")), true)
assertEquals(fields.contains(Attribute("error.hint", "Use a different primary key value")), true)
- // Check parameter attributes
- assertEquals(fields.contains(Attribute("error.parameter.1.type", "INT")), true)
- assertEquals(fields.contains(Attribute("error.parameter.1.value", "101")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.type", "CHAR")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.value", "'test_user'")), true)
}
test("getMessage should include all relevant information") {
@@ -75,11 +61,7 @@ class SQLIntegrityConstraintViolationExceptionTest extends FTestPlatform:
message = "Foreign key constraint violated",
sqlState = Some("23503"),
vendorCode = Some(1452),
- sql = Some("INSERT INTO orders (user_id, product_id) VALUES (?, ?)"),
- params = SortedMap(
- 1 -> Parameter.int(999),
- 2 -> Parameter.int(101)
- )
+ sql = Some("INSERT INTO orders (user_id, product_id) VALUES (?, ?)")
)
val message = exception.getMessage
@@ -92,6 +74,4 @@ class SQLIntegrityConstraintViolationExceptionTest extends FTestPlatform:
message.contains("INSERT INTO orders (user_id, product_id) VALUES (?, ?)"),
"Message should contain the SQL query"
)
- assert(message.contains("999"), "Message should contain the first parameter value")
- assert(message.contains("101"), "Message should contain the second parameter value")
}
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLTransactionRollbackExceptionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLTransactionRollbackExceptionTest.scala
index d079245eb..865e847ee 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLTransactionRollbackExceptionTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/exception/SQLTransactionRollbackExceptionTest.scala
@@ -6,11 +6,8 @@
package ldbc.connector.exception
-import scala.collection.immutable.SortedMap
-
import org.typelevel.otel4s.Attribute
-import ldbc.connector.data.Parameter
import ldbc.connector.FTestPlatform
class SQLTransactionRollbackExceptionTest extends FTestPlatform:
@@ -39,19 +36,13 @@ class SQLTransactionRollbackExceptionTest extends FTestPlatform:
}
test("SQLTransactionRollbackException.fields should return correct attributes") {
- val params = SortedMap(
- 1 -> Parameter.string("product_id"),
- 2 -> Parameter.int(10)
- )
-
val exception = new SQLTransactionRollbackException(
message = "Deadlock found when trying to get lock",
sqlState = Some("40001"),
vendorCode = Some(1213),
sql = Some("UPDATE inventory SET quantity = quantity - ? WHERE product_id = ?"),
detail = Some("Transaction was rolled back automatically"),
- hint = Some("Try restarting the transaction"),
- params = params
+ hint = Some("Try restarting the transaction")
)
val fields = exception.fields
@@ -66,11 +57,6 @@ class SQLTransactionRollbackExceptionTest extends FTestPlatform:
assertEquals(fields.contains(Attribute("error.detail", "Transaction was rolled back automatically")), true)
assertEquals(fields.contains(Attribute("error.hint", "Try restarting the transaction")), true)
- // Check parameter attributes
- assertEquals(fields.contains(Attribute("error.parameter.1.type", "CHAR")), true)
- assertEquals(fields.contains(Attribute("error.parameter.1.value", "'product_id'")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.type", "INT")), true)
- assertEquals(fields.contains(Attribute("error.parameter.2.value", "10")), true)
}
test("getMessage should include all relevant information") {
@@ -78,11 +64,7 @@ class SQLTransactionRollbackExceptionTest extends FTestPlatform:
message = "Transaction rolled back",
sqlState = Some("40001"),
vendorCode = Some(1213),
- sql = Some("UPDATE accounts SET balance = balance - ? WHERE id = ?"),
- params = SortedMap(
- 1 -> Parameter.int(100),
- 2 -> Parameter.int(42)
- )
+ sql = Some("UPDATE accounts SET balance = balance - ? WHERE id = ?")
)
val message = exception.getMessage
@@ -95,8 +77,6 @@ class SQLTransactionRollbackExceptionTest extends FTestPlatform:
message.contains("UPDATE accounts SET balance = balance - ? WHERE id = ?"),
"Message should contain the SQL query"
)
- assert(message.contains("100"), "Message should contain the first parameter value")
- assert(message.contains("42"), "Message should contain the second parameter value")
}
test("SQLTransactionRollbackException should be a subclass of SQLTransientException") {
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/net/protocol/SharedPreparedStatementTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/net/protocol/SharedPreparedStatementTest.scala
index f6b0c4374..2570f669e 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/net/protocol/SharedPreparedStatementTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/net/protocol/SharedPreparedStatementTest.scala
@@ -349,6 +349,56 @@ class SharedPreparedStatementTest extends SharedPreparedStatement[IO], FTestPlat
IO(assertEquals(Parameter.string(input).sql, expected))
}
+ test("enquoteLiteral should quote a value and double single quotes") {
+ for
+ simple <- enquoteLiteral("abc")
+ quote <- enquoteLiteral("G'Day")
+ nul <- enquoteLiteral(null)
+ yield
+ assertEquals(simple, "'abc'")
+ assertEquals(quote, "'G''Day'")
+ assertEquals(nul, "NULL")
+ }
+
+ test("enquoteIdentifier should return simple identifiers unquoted unless alwaysQuote is set") {
+ for
+ plain <- enquoteIdentifier("abc", false)
+ quoted <- enquoteIdentifier("abc", true)
+ spaced <- enquoteIdentifier("ab cd", false)
+ reserved <- enquoteIdentifier("SELECT", false)
+ yield
+ assertEquals(plain, "abc")
+ assertEquals(quoted, "`abc`")
+ assertEquals(spaced, "`ab cd`")
+ assertEquals(reserved, "`SELECT`")
+ }
+
+ test("enquoteIdentifier should fail for a null identifier") {
+ interceptIO[SQLException](enquoteIdentifier(null, false))
+ }
+
+ test("enquoteNCharLiteral should prefix the quoted value with N") {
+ for
+ simple <- enquoteNCharLiteral("abc")
+ quote <- enquoteNCharLiteral("G'Day")
+ nul <- enquoteNCharLiteral(null)
+ yield
+ assertEquals(simple, "N'abc'")
+ assertEquals(quote, "N'G''Day'")
+ assertEquals(nul, "NULL")
+ }
+
+ test("isSimpleIdentifier should identify simple identifiers") {
+ for
+ simple <- isSimpleIdentifier("abc")
+ spaced <- isSimpleIdentifier("ab cd")
+ reserved <- isSimpleIdentifier("SELECT")
+ yield
+ assert(simple)
+ assert(!spaced)
+ assert(!reserved)
+ }
+
override def executeQuery(): IO[ResultSet[IO]] =
IO.raiseError(new UnsupportedOperationException("Not implemented for test"))
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/KeepaliveExecutorTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/KeepaliveExecutorTest.scala
index d653205a6..fca0ee1ea 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/KeepaliveExecutorTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/KeepaliveExecutorTest.scala
@@ -49,6 +49,10 @@ class KeepaliveExecutorTest extends FTestPlatform:
override def executeLargeUpdate(sql: String): F[Long] = ???
override def executeLargeUpdate(sql: String, autoGeneratedKeys: Int): F[Long] = ???
override def executeLargeBatch(): F[Array[Long]] = ???
+ override def enquoteLiteral(value: String): F[String] = ???
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): F[String] = ???
+ override def enquoteNCharLiteral(value: String): F[String] = ???
+ override def isSimpleIdentifier(identifier: String): F[Boolean] = ???
})
override def isClosed(): F[Boolean] = Temporal[F].pure(false)
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/ProxyConnectionTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/ProxyConnectionTest.scala
index a57e9ba65..3c11bd423 100644
--- a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/ProxyConnectionTest.scala
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/pool/ProxyConnectionTest.scala
@@ -107,7 +107,12 @@ class ProxyConnectionTest extends FTestPlatform:
override def getLargeUpdateCount(): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String, autoGeneratedKeys: Int): IO[Long] = IO.pure(0L)
- override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def enquoteLiteral(value: String): IO[String] = IO.pure(value)
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): IO[String] =
+ IO.pure(identifier)
+ override def enquoteNCharLiteral(value: String): IO[String] = IO.pure(value)
+ override def isSimpleIdentifier(identifier: String): IO[Boolean] = IO.pure(false)
}
// Create a custom connection that returns our mock statements
@@ -285,7 +290,12 @@ class ProxyConnectionTest extends FTestPlatform:
override def getLargeUpdateCount(): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String, autoGeneratedKeys: Int): IO[Long] = IO.pure(0L)
- override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def enquoteLiteral(value: String): IO[String] = IO.pure(value)
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): IO[String] =
+ IO.pure(identifier)
+ override def enquoteNCharLiteral(value: String): IO[String] = IO.pure(value)
+ override def isSimpleIdentifier(identifier: String): IO[Boolean] = IO.pure(false)
}
// Custom console that tracks error messages
@@ -498,7 +508,12 @@ class ProxyConnectionTest extends FTestPlatform:
override def getLargeUpdateCount(): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String): IO[Long] = IO.pure(0L)
override def executeLargeUpdate(sql: String, autoGeneratedKeys: Int): IO[Long] = IO.pure(0L)
- override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def executeLargeBatch(): IO[Array[Long]] = IO.pure(Array.empty)
+ override def enquoteLiteral(value: String): IO[String] = IO.pure(value)
+ override def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): IO[String] =
+ IO.pure(identifier)
+ override def enquoteNCharLiteral(value: String): IO[String] = IO.pure(value)
+ override def isSimpleIdentifier(identifier: String): IO[Boolean] = IO.pure(false)
}
customConn = new LdbcConnection[IO] {
diff --git a/module/ldbc-connector/shared/src/test/scala/ldbc/connector/util/StringHelperTest.scala b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/util/StringHelperTest.scala
new file mode 100644
index 000000000..9bc5751b6
--- /dev/null
+++ b/module/ldbc-connector/shared/src/test/scala/ldbc/connector/util/StringHelperTest.scala
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2023-2026 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
+
+import munit.FunSuite
+
+class StringHelperTest extends FunSuite:
+
+ private val bools = List(false, true)
+
+ test("isSimpleIdentifier should accept valid simple identifiers") {
+ assert(StringHelper.isSimpleIdentifier("a"))
+ assert(StringHelper.isSimpleIdentifier("abc"))
+ assert(StringHelper.isSimpleIdentifier("ab_cd"))
+ assert(StringHelper.isSimpleIdentifier("ab$cd"))
+ assert(StringHelper.isSimpleIdentifier("123abc"))
+ assert(StringHelper.isSimpleIdentifier("_123"))
+ assert(StringHelper.isSimpleIdentifier("$123"))
+ assert(StringHelper.isSimpleIdentifier("X" * 64))
+ }
+
+ test("isSimpleIdentifier should reject invalid identifiers") {
+ assert(!StringHelper.isSimpleIdentifier(null))
+ assert(!StringHelper.isSimpleIdentifier(""))
+ assert(!StringHelper.isSimpleIdentifier(" "))
+ assert(!StringHelper.isSimpleIdentifier("."))
+ assert(!StringHelper.isSimpleIdentifier("ab cd"))
+ assert(!StringHelper.isSimpleIdentifier("ab.cd"))
+ assert(!StringHelper.isSimpleIdentifier("ab-cd"))
+ assert(!StringHelper.isSimpleIdentifier("ab`cd"))
+ assert(!StringHelper.isSimpleIdentifier("ab\"cd"))
+ assert(!StringHelper.isSimpleIdentifier("`abc`"))
+ assert(!StringHelper.isSimpleIdentifier("123456"))
+ assert(!StringHelper.isSimpleIdentifier("\"abc\""))
+ assert(!StringHelper.isSimpleIdentifier("X" * 65))
+ }
+
+ test("isSimpleIdentifier should reject reserved words") {
+ assert(!StringHelper.isSimpleIdentifier("SELECT"))
+ assert(!StringHelper.isSimpleIdentifier("select"))
+ assert(!StringHelper.isSimpleIdentifier("update"))
+ assert(!StringHelper.isSimpleIdentifier("Table"))
+ }
+
+ test("enquoteLiteral: values without quotes") {
+ for
+ asq <- bools
+ bse <- bools
+ do
+ val ctx = s"ansiQuotes=$asq, backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteLiteral(v, asq, bse)
+ assertEquals(enq(""), "''", ctx)
+ assertEquals(enq(" "), "' '", ctx)
+ assertEquals(enq("abc"), "'abc'", ctx)
+ assertEquals(enq("abc\ndef"), "'abc\ndef'", ctx)
+ assertEquals(enq("abc\\"), if bse then "'abc\\\\'" else "'abc\\'", ctx)
+ assertEquals(enq("abc\\\\"), "'abc\\\\'", ctx)
+ assertEquals(enq("abc\\\\\\"), if bse then "'abc\\\\\\\\'" else "'abc\\\\\\'", ctx)
+ }
+
+ test("enquoteLiteral: values containing single quotes") {
+ for
+ asq <- bools
+ bse <- bools
+ do
+ val ctx = s"ansiQuotes=$asq, backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteLiteral(v, asq, bse)
+ assertEquals(enq("'"), "''''", ctx)
+ assertEquals(enq("\\'"), if bse then "'\\''" else "'\\'''", ctx)
+ assertEquals(enq("\\\\'"), "'\\\\'''", ctx)
+ assertEquals(enq("''"), "''", ctx)
+ assertEquals(enq("\\'abc'"), if bse then "'\\'abc'''" else "'\\''abc'''", ctx)
+ assertEquals(enq("\\\\'abc'"), "'\\\\''abc'''", ctx)
+ assertEquals(enq("\\\\\\'abc'"), if bse then "'\\\\\\'abc'''" else "'\\\\\\''abc'''", ctx)
+ assertEquals(enq("'abc\\'"), if bse then "'''abc\\''" else "'abc\\'", ctx)
+ assertEquals(enq("'abc\\\\'"), "'abc\\\\'", ctx)
+ assertEquals(enq("'abc\\\\\\'"), if bse then "'''abc\\\\\\''" else "'abc\\\\\\'", ctx)
+ assertEquals(enq("abc'def"), "'abc''def'", ctx)
+ assertEquals(enq("abc\\'def"), if bse then "'abc\\'def'" else "'abc\\''def'", ctx)
+ assertEquals(enq("abc\\\\'def"), "'abc\\\\''def'", ctx)
+ assertEquals(enq("abc''def"), "'abc''''def'", ctx)
+ assertEquals(enq("'abc'"), "'abc'", ctx)
+ assertEquals(enq("'abc\ndef'"), "'abc\ndef'", ctx)
+ assertEquals(enq("'abc'def'"), "'''abc''def'''", ctx)
+ assertEquals(enq("'abc\\'def'"), if bse then "'abc\\'def'" else "'''abc\\''def'''", ctx)
+ assertEquals(enq("'abc\\\\'def'"), "'''abc\\\\''def'''", ctx)
+ assertEquals(enq("'abc''def'"), "'abc''def'", ctx)
+ assertEquals(enq("abc'def\\'ghi"), if bse then "'abc''def\\'ghi'" else "'abc''def\\''ghi'", ctx)
+ assertEquals(enq("abc'def\\\\'ghi"), "'abc''def\\\\''ghi'", ctx)
+ assertEquals(enq("abc''def\\'ghi"), if bse then "'abc''''def\\'ghi'" else "'abc''''def\\''ghi'", ctx)
+ assertEquals(enq("abc''def\\\\'ghi"), "'abc''''def\\\\''ghi'", ctx)
+ assertEquals(enq("'abc\""), "'''abc\"'", ctx)
+ assertEquals(enq("'abc\"def'"), "'abc\"def'", ctx)
+ assertEquals(enq("'abc\"def''ghi\""), "'''abc\"def''''ghi\"'", ctx)
+ assertEquals(enq("'abc\"def'ghi'"), "'''abc\"def''ghi'''", ctx)
+ assertEquals(enq("'abc\"def''ghi'"), "'abc\"def''ghi'", ctx)
+ }
+
+ test("enquoteLiteral: values containing double quotes") {
+ for
+ asq <- bools
+ bse <- bools
+ do
+ val ctx = s"ansiQuotes=$asq, backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteLiteral(v, asq, bse)
+ assertEquals(enq("\""), "'\"'", ctx)
+ assertEquals(enq("\\\""), "'\\\"'", ctx)
+ assertEquals(enq("\\\\\""), "'\\\\\"'", ctx)
+ assertEquals(enq("\"\""), if asq then "'\"\"'" else "\"\"", ctx)
+ assertEquals(enq("\\\"abc\""), "'\\\"abc\"'", ctx)
+ assertEquals(enq("\\\\\"abc\""), "'\\\\\"abc\"'", ctx)
+ assertEquals(enq("\\\\\\\"abc\""), "'\\\\\\\"abc\"'", ctx)
+ assertEquals(enq("\"abc\\\""), if asq || bse then "'\"abc\\\"'" else "\"abc\\\"", ctx)
+ assertEquals(enq("\"abc\\\\\""), if asq then "'\"abc\\\\\"'" else "\"abc\\\\\"", ctx)
+ assertEquals(enq("\"abc\\\\\\\""), if asq || bse then "'\"abc\\\\\\\"'" else "\"abc\\\\\\\"", ctx)
+ assertEquals(enq("abc\"def"), "'abc\"def'", ctx)
+ assertEquals(enq("abc\\\"def"), "'abc\\\"def'", ctx)
+ assertEquals(enq("abc\\\\\"def"), "'abc\\\\\"def'", ctx)
+ assertEquals(enq("abc\"\"def"), "'abc\"\"def'", ctx)
+ assertEquals(enq("\"abc\""), if asq then "'\"abc\"'" else "\"abc\"", ctx)
+ assertEquals(enq("\"abc\ndef\""), if asq then "'\"abc\ndef\"'" else "\"abc\ndef\"", ctx)
+ assertEquals(enq("\"abc\"def\""), "'\"abc\"def\"'", ctx)
+ assertEquals(enq("\"abc\\\"def\""), if asq || !bse then "'\"abc\\\"def\"'" else "\"abc\\\"def\"", ctx)
+ assertEquals(enq("\"abc\\\\\"def\""), "'\"abc\\\\\"def\"'", ctx)
+ assertEquals(enq("\"abc\"\"def\""), if asq then "'\"abc\"\"def\"'" else "\"abc\"\"def\"", ctx)
+ assertEquals(enq("abc\"def\\\"ghi"), "'abc\"def\\\"ghi'", ctx)
+ assertEquals(enq("abc\"def\\\\\"ghi"), "'abc\"def\\\\\"ghi'", ctx)
+ assertEquals(enq("abc\"\"def\\\"ghi"), "'abc\"\"def\\\"ghi'", ctx)
+ assertEquals(enq("abc\"\"def\\\\\"ghi"), "'abc\"\"def\\\\\"ghi'", ctx)
+ assertEquals(enq("\"abc'"), "'\"abc'''", ctx)
+ assertEquals(enq("\"abc'def\""), if asq then "'\"abc''def\"'" else "\"abc'def\"", ctx)
+ assertEquals(enq("\"abc'def\"\"ghi'"), "'\"abc''def\"\"ghi'''", ctx)
+ assertEquals(enq("\"abc'def\"ghi\""), "'\"abc''def\"ghi\"'", ctx)
+ assertEquals(enq("\"abc'def\"\"ghi\""), if asq then "'\"abc''def\"\"ghi\"'" else "\"abc'def\"\"ghi\"", ctx)
+ }
+
+ test("enquoteIdentifier: backtick quoting (ANSI_QUOTES disabled)") {
+ def enq(v: String) = StringHelper.enquoteIdentifier(v, false)
+ assertEquals(enq(""), "``")
+ assertEquals(enq(" "), "` `")
+ assertEquals(enq("abc"), "`abc`")
+ assertEquals(enq("abc\ndef"), "`abc\ndef`")
+ assertEquals(enq("abc\\"), "`abc\\`")
+ assertEquals(enq("abc\\\\"), "`abc\\\\`")
+ assertEquals(enq("SELECT"), "`SELECT`")
+ assertEquals(enq("`"), "````")
+ assertEquals(enq("\\`"), "`\\```")
+ assertEquals(enq("``"), "``")
+ assertEquals(enq("\\`abc`"), "`\\``abc```")
+ assertEquals(enq("`abc\\`"), "`abc\\`")
+ assertEquals(enq("abc`def"), "`abc``def`")
+ assertEquals(enq("abc``def"), "`abc````def`")
+ assertEquals(enq("`abc`"), "`abc`")
+ assertEquals(enq("`abc`def`"), "```abc``def```")
+ assertEquals(enq("`abc``def`"), "`abc``def`")
+ assertEquals(enq("\""), "`\"`")
+ assertEquals(enq("\"\""), "`\"\"`")
+ assertEquals(enq("\"abc\""), "`\"abc\"`")
+ assertEquals(enq("abc\"def"), "`abc\"def`")
+ }
+
+ test("enquoteIdentifier: double quoting (ANSI_QUOTES enabled)") {
+ def enq(v: String) = StringHelper.enquoteIdentifier(v, true)
+ assertEquals(enq(""), "\"\"")
+ assertEquals(enq(" "), "\" \"")
+ assertEquals(enq("abc"), "\"abc\"")
+ assertEquals(enq("abc\\"), "\"abc\\\"")
+ assertEquals(enq("`"), "\"`\"")
+ assertEquals(enq("``"), "``")
+ assertEquals(enq("`abc`"), "`abc`")
+ assertEquals(enq("\""), "\"\"\"\"")
+ assertEquals(enq("\"\""), "\"\"")
+ assertEquals(enq("\"abc\""), "\"abc\"")
+ assertEquals(enq("abc\"def"), "\"abc\"\"def\"")
+ assertEquals(enq("abc\"\"def"), "\"abc\"\"\"\"def\"")
+ assertEquals(enq("\"abc\"def\""), "\"\"\"abc\"\"def\"\"\"")
+ }
+
+ test("enquoteNCharLiteral: raw values") {
+ for bse <- bools do
+ val ctx = s"backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteNCharLiteral(v, bse)
+ assertEquals(enq(""), "N''", ctx)
+ assertEquals(enq(" "), "N' '", ctx)
+ assertEquals(enq("abc"), "N'abc'", ctx)
+ assertEquals(enq("abc\ndef"), "N'abc\ndef'", ctx)
+ assertEquals(enq("abc\\"), if bse then "N'abc\\\\'" else "N'abc\\'", ctx)
+ assertEquals(enq("abc\\\\"), "N'abc\\\\'", ctx)
+ assertEquals(enq("abc\\\\\\"), if bse then "N'abc\\\\\\\\'" else "N'abc\\\\\\'", ctx)
+ assertEquals(enq("'"), "N''''", ctx)
+ assertEquals(enq("\\'"), if bse then "N'\\''" else "N'\\'''", ctx)
+ assertEquals(enq("\\\\'"), "N'\\\\'''", ctx)
+ assertEquals(enq("''"), "N''", ctx)
+ assertEquals(enq("'abc'"), "N'abc'", ctx)
+ }
+
+ test("enquoteNCharLiteral: values already in N'...' form") {
+ for bse <- bools do
+ val ctx = s"backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteNCharLiteral(v, bse)
+ assertEquals(enq("N'abc'"), "N'abc'", ctx)
+ assertEquals(enq("n'abc'"), "N'abc'", ctx)
+ assertEquals(enq("N'abc'def'"), "N'N''abc''def'''", ctx)
+ assertEquals(enq("N'abc''def'"), "N'abc''def'", ctx)
+ assertEquals(enq("N'abc\\'def'"), if bse then "N'abc\\'def'" else "N'N''abc\\''def'''", ctx)
+ assertEquals(enq("N'abc\"def'"), "N'abc\"def'", ctx)
+ assertEquals(enq("N'abc\"\"def'"), "N'abc\"\"def'", ctx)
+ assertEquals(enq("N'abc\\\"def'"), "N'abc\\\"def'", ctx)
+ }
+
+ test("enquoteNCharLiteral: double quotes are never valid NCHAR literal delimiters") {
+ for bse <- bools do
+ val ctx = s"backslashEscapes=$bse"
+ def enq(v: String) = StringHelper.enquoteNCharLiteral(v, bse)
+ assertEquals(enq("\""), "N'\"'", ctx)
+ assertEquals(enq("\\\""), "N'\\\"'", ctx)
+ assertEquals(enq("\\\\\""), "N'\\\\\"'", ctx)
+ assertEquals(enq("\"\""), "N'\"\"'", ctx)
+ assertEquals(enq("\"abc\""), "N'\"abc\"'", ctx)
+ assertEquals(enq("N\"abc\""), "N'N\"abc\"'", ctx)
+ assertEquals(enq("n\"abc\""), "N'n\"abc\"'", ctx)
+ assertEquals(enq("N\"abc\"def\""), "N'N\"abc\"def\"'", ctx)
+ assertEquals(enq("N\"abc\"\"def\""), "N'N\"abc\"\"def\"'", ctx)
+ assertEquals(enq("N\"abc\\\"def\""), "N'N\"abc\\\"def\"'", ctx)
+ assertEquals(enq("N\"abc'def\""), "N'N\"abc''def\"'", ctx)
+ assertEquals(enq("N\"abc''def\""), "N'N\"abc''''def\"'", ctx)
+ assertEquals(enq("N\"abc\\'def\""), if bse then "N'N\"abc\\'def\"'" else "N'N\"abc\\''def\"'", ctx)
+ }
diff --git a/module/ldbc-schema/src/test/scala/ldbc/schema/TableQueryTest.scala b/module/ldbc-schema/src/test/scala/ldbc/schema/TableQueryTest.scala
index 5cc39f239..50fd9e5dc 100644
--- a/module/ldbc-schema/src/test/scala/ldbc/schema/TableQueryTest.scala
+++ b/module/ldbc-schema/src/test/scala/ldbc/schema/TableQueryTest.scala
@@ -271,6 +271,11 @@ class TableQueryTest extends munit.FunSuite:
assertEquals(query.selectAll.statement, "SELECT test.`p1`, test.`p2`, test.`p3` FROM test")
}
+ test("Tuple insert encodes values through the entity mapping.") {
+ assertEquals(query.insert((1L, "p2", Some("p3"))).params.size, 3)
+ assertEquals(query.insert((1L, "p2", Some("p3")), (2L, "p2", None)).params.size, 6)
+ }
+
test("The insert query statement generated from Table is equal to the specified query statement.") {
assertEquals(
query.insert((1L, "p2", Some("p3"))).statement,
diff --git a/module/ldbc-sql/src/main/scala/ldbc/sql/Statement.scala b/module/ldbc-sql/src/main/scala/ldbc/sql/Statement.scala
index 3805b2509..f29eb61bb 100644
--- a/module/ldbc-sql/src/main/scala/ldbc/sql/Statement.scala
+++ b/module/ldbc-sql/src/main/scala/ldbc/sql/Statement.scala
@@ -415,6 +415,87 @@ trait Statement[F[_]]:
*/
def executeLargeBatch(): F[Array[Long]]
+ /**
+ * Returns a String enclosed in single quotes. Any occurrence of a
+ * single quote within the string will be replaced by two single quotes.
+ *
+ *
+ *
+ * Examples of the conversion:
+ * | Value | Result |
+ * | Hello | 'Hello' |
+ * | G'Day | 'G''Day' |
+ * | 'G''Day' | '''G''''Day''' |
+ * | I'''M | 'I''''''M' |
+ *
+ *
+ *
+ * The driver may apply additional, vendor-specific escaping rules
+ * (for example, escaping of backslash characters depending on the
+ * NO_BACKSLASH_ESCAPES SQL mode in MySQL).
+ *
+ * @param value a character string
+ * @return A string enclosed by single quotes with every single quote
+ * converted to two single quotes
+ */
+ def enquoteLiteral(value: String): F[String]
+
+ /**
+ * Returns a SQL identifier. If identifier is a simple SQL identifier:
+ *
+ * - Return the original value if
alwaysQuote is false
+ * - Return a delimited identifier if
alwaysQuote is true
+ *
+ *
+ * If identifier is not a simple SQL identifier, identifier will be
+ * enclosed in the identifier quote character used by the database
+ * (backtick ` by default in MySQL, or double quote " when the
+ * ANSI_QUOTES SQL mode is enabled).
+ *
+ * @param identifier a SQL identifier
+ * @param alwaysQuote indicates if a simple SQL identifier should be
+ * returned as a quoted identifier
+ * @return A simple SQL identifier or a delimited identifier
+ */
+ def enquoteIdentifier(identifier: String, alwaysQuote: Boolean): F[String]
+
+ /**
+ * Returns a String representing a National Character Set Literal
+ * enclosed in single quotes and prefixed with a upper case letter N.
+ * Any occurrence of a single quote within the string will be replaced
+ * by two single quotes.
+ *
+ *
+ *
+ * Examples of the conversion:
+ * | Value | Result |
+ * | Hello | N'Hello' |
+ * | G'Day | N'G''Day' |
+ * | N'Hello' | N'Hello' |
+ *
+ *
+ *
+ * @param value a character string
+ * @return the result of replacing every single quote character in the
+ * argument by two single quote characters where this entire result is
+ * then prefixed with 'N'.
+ */
+ def enquoteNCharLiteral(value: String): F[String]
+
+ /**
+ * Retrieves whether identifier is a simple SQL identifier that can be used
+ * in a SQL statement without quoting.
+ *
+ * In MySQL, a simple identifier consists of characters in the range
+ * [0-9a-zA-Z$_] or extended characters (U+0080 and above),
+ * must not consist solely of digits, must not be longer than 64 characters
+ * and must not be a reserved word.
+ *
+ * @param identifier a SQL identifier
+ * @return true if a simple SQL identifier, false otherwise
+ */
+ def isSimpleIdentifier(identifier: String): F[Boolean]
+
object Statement:
/**
diff --git a/module/ldbc-statement/src/main/scala/ldbc/statement/TableQuery.scala b/module/ldbc-statement/src/main/scala/ldbc/statement/TableQuery.scala
index fb23efa9b..81a4479a1 100644
--- a/module/ldbc-statement/src/main/scala/ldbc/statement/TableQuery.scala
+++ b/module/ldbc-statement/src/main/scala/ldbc/statement/TableQuery.scala
@@ -12,7 +12,6 @@ import scala.deriving.Mirror
import cats.data.NonEmptyList
-import ldbc.dsl.codec.Encoder
import ldbc.dsl.Parameter
import ldbc.statement.internal.QueryConcat
@@ -115,35 +114,32 @@ trait TableQuery[A, O]:
* Value to be inserted into the table
*/
inline def insert(using
- mirror: Mirror.Of[Entity]
+ mirror: Mirror.ProductOf[Entity]
)(
head: mirror.MirroredElemTypes,
tail: mirror.MirroredElemTypes*
): Insert[A] =
inline this match
case Join.On(_, _, _, _, _) => error("Join Query does not yet support Insert processing.")
- case _ => insert[mirror.MirroredElemTypes](NonEmptyList(head, tail.toList))
+ case _ => insertEntities(NonEmptyList(head, tail.toList).map(tuple => mirror.fromProduct(tuple)))
/**
* Method to construct a query to insert a table.
*
- * {{{
- * TableQuery[City]
- * .insert(NonEmptyList.one(1L, "Tokyo"))
- * }}}
+ * Values are always encoded through the entity mapping defined by the table's `*` projection,
+ * so the parameter order matches the column order even when it differs from the field order.
*
* @param values
* Value to be inserted into the table
*/
- private def insert[B <: Tuple](values: NonEmptyList[B]): Insert[A] =
- val parameterBinders: List[Parameter.Dynamic] = values.toList.flatMap { value =>
- Parameter.Dynamic.many(column.encoder.asInstanceOf[Encoder[B]].encode(value))
- }
+ private def insertEntities(values: NonEmptyList[Entity]): Insert[A] =
Insert.Impl(
table = table,
statement =
- s"INSERT INTO $name (${ column.name }) VALUES ${ values.map(tuple => s"(${ tuple.toArray.map(_ => "?").mkString(",") })").toList.mkString(",") }",
- params = params ++ parameterBinders
+ s"INSERT INTO $name (${ column.name }) VALUES ${ values.map(_ => s"(${ List.fill(column.values)("?").mkString(",") })").toList.mkString(",") }",
+ params = params ++ values.toList.flatMap { value =>
+ Parameter.Dynamic.many(column.encoder.encode(value))
+ }
)
/**
@@ -196,15 +192,7 @@ trait TableQuery[A, O]:
inline def ++=(values: NonEmptyList[Entity]): Insert[A] =
inline this match
case Join.On(_, _, _, _, _) => error("Join Query does not yet support Insert processing.")
- case _ =>
- Insert.Impl(
- table = table,
- statement =
- s"INSERT INTO $name (${ column.name }) VALUES ${ values.map(_ => s"(${ List.fill(column.values)("?").mkString(",") })").toList.mkString(",") }",
- params = params ++ values.toList.flatMap { value =>
- Parameter.Dynamic.many(column.encoder.encode(value))
- }
- )
+ case _ => insertEntities(values)
/**
* Method to construct a query to update a table.
diff --git a/plugin/src/main/scala/ldbc/sbt/Commands.scala b/plugin/src/main/scala/ldbc/sbt/Commands.scala
index cebbf0bbc..39bfe9700 100644
--- a/plugin/src/main/scala/ldbc/sbt/Commands.scala
+++ b/plugin/src/main/scala/ldbc/sbt/Commands.scala
@@ -9,14 +9,19 @@ package ldbc.sbt
import sbt._
import sbt.Keys._
+import sbtcompat.PluginCompat._
+import xsbti.FileConverter
+
object Commands {
val baseClassloaderTask = Def.task {
+ implicit val conv: FileConverter = fileConverter.value
+
val classpath = (Compile / dependencyClasspath).value
val log = streams.value.log
val parent = ClassLoader.getSystemClassLoader.getParent
log.debug("Using parent loader for base classloader: " + parent)
- new java.net.URLClassLoader(classpath.map(_.data.toURI.toURL).toArray, parent)
+ new java.net.URLClassLoader(toFiles(classpath).map(_.toURI.toURL).toArray, parent)
}
val generateBySchema = Command.command("generateBySchema") { (state: State) =>
diff --git a/plugin/src/main/scala/ldbc/sbt/Generator.scala b/plugin/src/main/scala/ldbc/sbt/Generator.scala
index 05f37653e..a70bacfc4 100644
--- a/plugin/src/main/scala/ldbc/sbt/Generator.scala
+++ b/plugin/src/main/scala/ldbc/sbt/Generator.scala
@@ -7,17 +7,19 @@
package ldbc.sbt
import java.io.FilenameFilter
+import java.lang.reflect.Method
import java.nio.file.attribute.FileTime
import java.nio.file.Files
-import scala.language.reflectiveCalls
-
import sbt._
import sbt.Keys._
import ldbc.sbt.AutoImport._
import ldbc.sbt.CustomKeys._
+import sbtcompat.PluginCompat._
+import xsbti.FileConverter
+
object Generator {
private val logger = ProcessLogger()
@@ -55,6 +57,28 @@ object Generator {
private def convertToUrls(files: Seq[File]): Array[URL] = files.map(_.toURI.toURL).toArray
+ /**
+ * Resolves `ldbc.codegen.LdbcGenerator.generate` out of the project's own classpath.
+ *
+ * A structural type would read better, but Scala 3 only permits structural calls on receivers
+ * extending `scala.reflect.Selectable`, which has no Scala 2.12 equivalent. Going through
+ * `java.lang.reflect` directly keeps one source tree compiling for both sbt 1 and sbt 2.
+ */
+ private def loadGenerator(classLoader: ClassLoader): (AnyRef, Method) = {
+ val moduleClass = classLoader.loadClass("ldbc.codegen.LdbcGenerator$")
+ val module = moduleClass.getField("MODULE$").get(null)
+ val method = moduleClass.getMethod(
+ "generate",
+ classOf[Array[File]],
+ classOf[Array[File]],
+ classOf[String],
+ classOf[String],
+ classOf[File],
+ classOf[String]
+ )
+ (module, method)
+ }
+
private var cacheMap: Map[String, FileTime] = Map.empty
private var generatedCache: Set[File] = Set.empty
@@ -87,16 +111,7 @@ object Generator {
alwaysGenerate: Boolean = false
): Def.Initialize[Task[Seq[File]]] = Def.task {
- type LdbcGenerator = {
- def generate(
- parseFiles: Array[File],
- customYamlFiles: Array[File],
- classNameFormat: String,
- propertyNameFormat: String,
- sourceManaged: File,
- packageName: String
- ): Array[File]
- }
+ implicit val conv: FileConverter = fileConverter.value
val sqlFilesInDirectory = parseDirectories.value.flatMap(file => {
if (file.isDirectory) {
@@ -111,12 +126,11 @@ object Generator {
val combinedFiles = (filtered ++ sqlFilesInDirectory).distinct
val projectClassLoader = new ProjectClassLoader(
- urls = convertToUrls((Runtime / externalDependencyClasspath).value.files),
+ urls = convertToUrls(toFiles((Runtime / externalDependencyClasspath).value)),
parent = baseClassloader.value
)
- val mainClass: Class[_] = projectClassLoader.loadClass("ldbc.codegen.LdbcGenerator$")
- val mainObject: LdbcGenerator = mainClass.getField("MODULE$").get(null).asInstanceOf[LdbcGenerator]
+ val (generator, generate) = loadGenerator(projectClassLoader)
val changed = changedHits(combinedFiles)
@@ -137,14 +151,17 @@ object Generator {
})
}
- val generated = mainObject.generate(
- executeFiles.toArray,
- customYamlFiles.value.toArray,
- classNameFormat.value.toString,
- propertyNameFormat.value.toString,
- sourceManaged.value,
- packageName.value
- )
+ val generated = generate
+ .invoke(
+ generator,
+ executeFiles.toArray,
+ customYamlFiles.value.toArray,
+ classNameFormat.value.toString,
+ propertyNameFormat.value.toString,
+ sourceManaged.value,
+ packageName.value
+ )
+ .asInstanceOf[Array[File]]
if (generated.nonEmpty) {
logger.debug("Generated files: [" + generated.map(_.getAbsoluteFile.getName).mkString(", ") + "]")
@@ -152,7 +169,7 @@ object Generator {
if (generatedCache.isEmpty) {
generatedCache = generated.toSet
- generated
+ generated.toSeq
} else {
generatedCache = generatedCache ++ generated
generatedCache.toSeq
diff --git a/plugin/src/main/scala/ldbc/sbt/Settings.scala b/plugin/src/main/scala/ldbc/sbt/Settings.scala
index 4efa25cfb..01160a045 100644
--- a/plugin/src/main/scala/ldbc/sbt/Settings.scala
+++ b/plugin/src/main/scala/ldbc/sbt/Settings.scala
@@ -9,6 +9,7 @@ package ldbc.sbt
import sbt._
import sbt.Keys._
+import sbtcompat.PluginCompat._
import AutoImport._
import CustomKeys._
@@ -16,7 +17,7 @@ object Settings {
lazy val projectSettings: Seq[Def.Setting[?]] = Def.settings(
libraryDependencies += ldbcCodegen,
- baseClassloader := Commands.baseClassloaderTask.value,
+ baseClassloader := Def.uncached(Commands.baseClassloaderTask.value),
parseFiles := List.empty,
parseDirectories := List.empty,
excludeFiles := List.empty,
@@ -25,9 +26,7 @@ object Settings {
propertyNameFormat := Format.CAMEL,
ldbcPackage := "ldbc.generated",
(Compile / sourceGenerators) += Generator.generate.taskValue,
- generateBySQLSchema := {
- Generator.alwaysGenerate.value
- },
+ generateBySQLSchema := Def.uncached(Generator.alwaysGenerate.value),
commands += Commands.generateBySchema
)
}
diff --git a/project/Brew.scala b/project/Brew.scala
new file mode 100644
index 000000000..a21223864
--- /dev/null
+++ b/project/Brew.scala
@@ -0,0 +1,86 @@
+/**
+ * This file is part of the ldbc.
+ * For the full copyright and license information,
+ * please view the LICENSE file that was distributed with this source code.
+ */
+
+import scala.scalanative.sbtplugin.ScalaNativePlugin.autoImport.*
+import scala.sys.process.Process
+import scala.util.Try
+
+import sbt.*
+import sbt.Keys.*
+
+/**
+ * Homebrew integration for the Scala Native builds.
+ *
+ * fs2-io declares `@link("s2n")` for its TLS implementation, so the Scala Native test binaries link
+ * against s2n-tls. Homebrew installs it outside clang's default search path, which means the include
+ * and library directories have to be passed explicitly.
+ *
+ * This replaces `sbt-scala-native-config-brew`, of which ldbc only used this single formula. The
+ * upstream plugin parses `brew info --json` to expand a formula's runtime dependencies; `brew deps`
+ * gives the same list without needing a JSON parser on the build classpath.
+ */
+object Brew {
+
+ /** Formulae required by the Scala Native builds. Their dependencies are resolved by `brew deps`. */
+ val formulas: Seq[String] = Seq("s2n")
+
+ val linuxBin: String = "/home/linuxbrew/.linuxbrew/bin/brew"
+
+ /**
+ * Points the Scala Native test build at the Homebrew installation of [[formulas]].
+ *
+ * `LD_LIBRARY_PATH` is extended as well, because the test binary resolves s2n-tls dynamically at
+ * run time. When Homebrew or a formula cannot be found the settings are left untouched and a
+ * warning is logged, so that a checkout without them still loads and builds on the other platforms.
+ */
+ def nativeSettings: Seq[Setting[?]] = Seq(
+ Test / nativeConfig := {
+ val config = (Test / nativeConfig).value
+ val dirs = prefixes(streams.value.log)
+ config
+ .withCompileOptions(config.compileOptions ++ subDirs(dirs, "include").map(dir => s"-I$dir"))
+ .withLinkingOptions(config.linkingOptions ++ subDirs(dirs, "lib").map(dir => s"-L$dir"))
+ },
+ Test / envVars := {
+ val env = (Test / envVars).value
+ subDirs(prefixes(streams.value.log), "lib") match {
+ case Nil => env
+ case libs => env.updated("LD_LIBRARY_PATH", (env.get("LD_LIBRARY_PATH").toList ++ libs).mkString(":"))
+ }
+ }
+ )
+
+ private def bin: Option[String] = {
+ val os = sys.props.getOrElse("os.name", "").toLowerCase
+ if (os.contains("mac")) {
+ val isArm = sys.props.getOrElse("os.arch", "").toLowerCase.contains("aarch64")
+ Some(if (isArm) "/opt/homebrew/bin/brew" else "/usr/local/bin/brew")
+ } else if (os.contains("linux")) Some(linuxBin)
+ else None
+ }
+
+ private def run(brew: String, args: String*): Option[String] =
+ Try(Process(brew +: args).!!.trim).toOption
+
+ private def prefixes(log: Logger): List[File] = bin match {
+ case None =>
+ log.warn("Homebrew is not available on this platform, Scala Native options will not be auto-configured.")
+ Nil
+ case Some(brew) =>
+ val withDeps = formulas.toList.flatMap { formula =>
+ formula :: run(brew, "deps", formula).toList.flatMap(_.linesIterator.map(_.trim).filter(_.nonEmpty))
+ }
+ val dirs = withDeps.distinct.flatMap(run(brew, "--prefix", _)).map(file).filter(_.isDirectory)
+ if (dirs.isEmpty) {
+ log.warn(s"Cannot find brew-installed ${ formulas.mkString(", ") }.")
+ log.warn("nativeConfig and LD_LIBRARY_PATH will not be auto-configured.")
+ }
+ dirs
+ }
+
+ private def subDirs(prefixes: List[File], name: String): List[String] =
+ prefixes.map(_ / name).filter(_.isDirectory).map(_.toString)
+}
diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala
index d69f098d8..8c4eb85ff 100644
--- a/project/BuildSettings.scala
+++ b/project/BuildSettings.scala
@@ -15,6 +15,7 @@ import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport.HeaderPattern.comment
import org.typelevel.sbt.TypelevelGitHubPlugin.autoImport.tlGitHubDev
+import SbtVersions.*
import ScalaVersions.*
object BuildSettings {
@@ -65,11 +66,30 @@ object BuildSettings {
)
)
- /** A project that is an sbt plugin. */
+ /**
+ * A project that is an sbt plugin.
+ *
+ * The plugin is cross-built rather than migrated: sbt 1.x loads plugins compiled against Scala
+ * 2.12 and sbt 2.x loads plugins compiled against Scala 3, so both artifacts have to be published
+ * side by side. `scalaBinaryVersion` selects which sbt the plugin is compiled against.
+ *
+ * NOTE: sbt-typelevel derives `scalaVersion` from `crossScalaVersions`, so appending with `+=`
+ * here produces `Cyclic reference involving crossScalaVersions/scalaVersion` at project load.
+ * Both keys must be assigned with `:=`.
+ */
object LepusSbtPluginProject {
def apply(name: String, dir: String): Project =
Project(name, file(dir))
- .settings(scalaVersion := scala2)
+ .settings(
+ scalaVersion := scala2,
+ crossScalaVersions := Seq(scala2, scala38),
+ pluginCrossBuild / sbtVersion := {
+ scalaBinaryVersion.value match {
+ case "2.12" => sbt1
+ case _ => sbt2
+ }
+ }
+ )
.settings(scalacOptions ++= additionalSettings)
.settings(scalacOptions --= removeSettings)
.settings(commonSettings)
diff --git a/project/LaikaSettings.scala b/project/LaikaSettings.scala
index 2557f40ec..407c161eb 100644
--- a/project/LaikaSettings.scala
+++ b/project/LaikaSettings.scala
@@ -34,9 +34,10 @@ object LaikaSettings {
val v04: Version = version(LdbcVersions.v04)
val v05: Version = version(LdbcVersions.v05)
val v06: Version = version(LdbcVersions.v06)
- val v07: Version = version(LdbcVersions.latest, "Stable")
- val current: Version = v07
- val all: Seq[Version] = Seq(v07, v06, v05, v04, v03, v02)
+ val v07: Version = version(LdbcVersions.v07)
+ val v08: Version = version(LdbcVersions.latest, "Stable")
+ val current: Version = v08
+ val all: Seq[Version] = Seq(v08, v07, v06, v05, v04, v03, v02)
val config: Versions = Versions
.forCurrentVersion(current)
diff --git a/project/Versions.scala b/project/Versions.scala
index f00d6acf2..742642d07 100644
--- a/project/Versions.scala
+++ b/project/Versions.scala
@@ -3,7 +3,8 @@
*/
object LdbcVersions {
- val latest = "0.7"
+ val latest = "0.8"
+ val v07 = "0.7"
val v06 = "0.6"
val v05 = "0.5"
val v04 = "0.4"
@@ -13,11 +14,16 @@ object LdbcVersions {
}
object ScalaVersions {
- val scala2 = "2.12.19"
+ val scala2 = "2.12.21"
val scala3 = "3.3.8"
val scala38 = "3.8.4"
}
+object SbtVersions {
+ val sbt1 = "1.13.0"
+ val sbt2 = "2.0.8"
+}
+
object JavaVersions {
val java11 = "11"
val java17 = "17"
diff --git a/project/Workflows.scala b/project/Workflows.scala
index d3dc985a3..1efe2a8cd 100644
--- a/project/Workflows.scala
+++ b/project/Workflows.scala
@@ -28,7 +28,7 @@ object Workflows {
)
),
scalas = List(scala3),
- javas = List(JavaSpec.temurin(java17), JavaSpec.temurin(java21))
+ javas = List(JavaSpec.corretto(java17), JavaSpec.corretto(java21))
)
)
@@ -38,11 +38,17 @@ object Workflows {
)
val brewUpdate: WorkflowStep.Run = WorkflowStep.Run(
- commands = List("/home/linuxbrew/.linuxbrew/bin/brew update"),
+ commands = List(s"${ Brew.linuxBin } update"),
name = Some("Update Homebrew"),
cond = Some("matrix.project == 'ldbcNative'")
)
+ val brewInstall: WorkflowStep.Run = WorkflowStep.Run(
+ commands = List(s"${ Brew.linuxBin } install ${ Brew.formulas.mkString(" ") }"),
+ name = Some("Install brew formulae (ubuntu)"),
+ cond = Some("(matrix.project == 'ldbcNative') && startsWith(matrix.os, 'ubuntu')")
+ )
+
val dockerStop: WorkflowStep.Run = WorkflowStep.Run(
commands = List("docker compose down"),
name = Some("Stop MySQL on Docker")
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 49e599c22..c05b2b8ee 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -2,11 +2,10 @@
* distributed with this source code.
*/
-addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")
-addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.7")
-addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.7")
-addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0")
-addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12")
-addSbtPlugin("com.armanbilge" % "sbt-scala-native-config-brew-github-actions" % "0.4.0")
-addSbtPlugin("com.github.sbt" % "sbt-boilerplate" % "0.8.1")
-addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
+addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.8")
+addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.7")
+addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.7")
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0")
+addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12")
+addSbtPlugin("com.github.sbt" % "sbt-boilerplate" % "0.8.1")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4")
diff --git a/tests/shared/src/test/scala/ldbc/tests/ConnectionTest.scala b/tests/shared/src/test/scala/ldbc/tests/ConnectionTest.scala
index 36653c1f9..0c368c3b6 100644
--- a/tests/shared/src/test/scala/ldbc/tests/ConnectionTest.scala
+++ b/tests/shared/src/test/scala/ldbc/tests/ConnectionTest.scala
@@ -59,6 +59,44 @@ trait ConnectionTest extends CatsEffectSuite:
assertIOBoolean(datasource().getConnection.use(_.isValid(0)))
}
+ test("Statement.enquoteLiteral quotes a string literal and doubles single quotes.") {
+ assertIO(
+ datasource().getConnection.use(_.createStatement().flatMap { stmt =>
+ for
+ simple <- stmt.enquoteLiteral("abc")
+ quoted <- stmt.enquoteLiteral("G'Day")
+ yield (simple, quoted)
+ }),
+ ("'abc'", "'G''Day'")
+ )
+ }
+
+ test("Statement.enquoteIdentifier returns a simple identifier unquoted.") {
+ assertIO(
+ datasource().getConnection.use(_.createStatement().flatMap(_.enquoteIdentifier("abc", false))),
+ "abc"
+ )
+ }
+
+ test("Statement.enquoteNCharLiteral prefixes the quoted value with N.") {
+ assertIO(
+ datasource().getConnection.use(_.createStatement().flatMap(_.enquoteNCharLiteral("G'Day"))),
+ "N'G''Day'"
+ )
+ }
+
+ test("Statement.isSimpleIdentifier distinguishes simple identifiers.") {
+ assertIO(
+ datasource().getConnection.use(_.createStatement().flatMap { stmt =>
+ for
+ simple <- stmt.isSimpleIdentifier("abc")
+ notSimple <- stmt.isSimpleIdentifier("ab cd")
+ yield (simple, notSimple)
+ }),
+ (true, false)
+ )
+ }
+
test("The allProceduresAreCallable method of DatabaseMetaData is always false.") {
assertIOBoolean(datasource().getConnection.use(_.getMetaData().map(meta => !meta.allProceduresAreCallable())))
}
@@ -125,8 +163,8 @@ trait ConnectionTest extends CatsEffectSuite:
test("The Driver version retrieved from DatabaseMetaData matches the specified value.") {
assertIO(
datasource().getConnection.use(_.getMetaData().map(_.getDriverVersion())),
- if prefix == "jdbc" then "mysql-connector-j-9.6.0 (Revision: fdef61f4af21fa9e0ac334ff0664ec754c164cc0)"
- else "ldbc-connector-0.7.1"
+ if prefix == "jdbc" then "mysql-connector-j-9.7.0 (Revision: 0aade1f13bcc98faf7dda5c02e782481eb291f62)"
+ else "ldbc-connector-0.8.0"
)
}
diff --git a/tests/shared/src/test/scala/ldbc/tests/DatabaseMetaDataTest.scala b/tests/shared/src/test/scala/ldbc/tests/DatabaseMetaDataTest.scala
index 6d1e5875e..3c839aa69 100644
--- a/tests/shared/src/test/scala/ldbc/tests/DatabaseMetaDataTest.scala
+++ b/tests/shared/src/test/scala/ldbc/tests/DatabaseMetaDataTest.scala
@@ -159,8 +159,8 @@ trait DatabaseMetaDataTest extends CatsEffectSuite:
for metaData <- conn.getMetaData()
yield metaData.getDriverVersion()
},
- if prefix == "jdbc" then "mysql-connector-j-9.6.0 (Revision: fdef61f4af21fa9e0ac334ff0664ec754c164cc0)"
- else "ldbc-connector-0.7.1"
+ if prefix == "jdbc" then "mysql-connector-j-9.7.0 (Revision: 0aade1f13bcc98faf7dda5c02e782481eb291f62)"
+ else "ldbc-connector-0.8.0"
)
}
@@ -180,7 +180,7 @@ trait DatabaseMetaDataTest extends CatsEffectSuite:
for metaData <- conn.getMetaData()
yield metaData.getDriverMinorVersion()
},
- if prefix == "jdbc" then 6 else 7
+ if prefix == "jdbc" then 7 else 8
)
}
@@ -2299,7 +2299,7 @@ trait DatabaseMetaDataTest extends CatsEffectSuite:
for metaData <- conn.getMetaData()
yield metaData.getJDBCMinorVersion()
},
- if prefix == "jdbc" then 2 else 7
+ if prefix == "jdbc" then 2 else 8
)
}