This is an automated email from the ASF dual-hosted git repository.
github-actions[bot] pushed a commit to branch site
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/site by this push:
new df10fc4c49 [CALCITE-7908] ModelHandler should apply the model
class-name filter to the JDBC driver auto-detected from jdbcUrl
df10fc4c49 is described below
commit df10fc4c49da41489659ac367ae3335c78dc2674
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Fri Oct 30 16:51:50 2026 +0000
[CALCITE-7908] ModelHandler should apply the model class-name filter to the
JDBC driver auto-detected from jdbcUrl
---
.../org/apache/calcite/model/ModelHandler.java | 17 ++-
.../org/apache/calcite/model/ModelHandlerTest.java | 153 +++++++++++++++++++++
site/_docs/history.md | 4 +-
site/_docs/model.md | 3 +
4 files changed, 174 insertions(+), 3 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/model/ModelHandler.java
b/core/src/main/java/org/apache/calcite/model/ModelHandler.java
index 46661065cc..e87e4b7159 100644
--- a/core/src/main/java/org/apache/calcite/model/ModelHandler.java
+++ b/core/src/main/java/org/apache/calcite/model/ModelHandler.java
@@ -56,6 +56,9 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayDeque;
import java.util.Collections;
@@ -358,10 +361,20 @@ protected Map<String, Object> operandMap(@Nullable
JsonSchema jsonSchema,
public void visit(JsonJdbcSchema jsonSchema) {
final SchemaPlus parentSchema = currentMutableSchema("jdbc schema");
- classNameFilter.check(jsonSchema.jdbcDriver);
+ String driverClassName = jsonSchema.jdbcDriver;
+ if (driverClassName == null || driverClassName.trim().isEmpty()) {
+ // No driver class in the model; check the one DriverManager picks for
the URL
+ try {
+ driverClassName =
DriverManager.getDriver(jsonSchema.jdbcUrl).getClass().getName();
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ classNameFilter.check(driverClassName);
+
final DataSource dataSource =
JdbcSchema.dataSource(jsonSchema.jdbcUrl,
- jsonSchema.jdbcDriver,
+ driverClassName,
jsonSchema.jdbcUser,
jsonSchema.jdbcPassword);
final JdbcSchema schema;
diff --git a/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java
b/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java
index 4cebda1454..86f7007da5 100644
--- a/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java
+++ b/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java
@@ -200,4 +200,157 @@ public class ModelHandlerTest {
assertThat(ClassNameFilter.append("", ""), is(""));
}
+ // ----- Non-inline model-file handling ---------------------------------
+ //
+ // The following tests cover two related aspects of the non-inline branch
+ // of the ModelHandler constructor:
+ //
+ // 1. The client-facing IOException on a bad model file carries only
+ // the path, not the file's contents and not whether the file
+ // exists. Legitimate operators find the detail in the log.
+ // 2. When calcite.model.baseDirectory is set, model paths that
+ // resolve outside it are rejected.
+ //
+ // The inline: branch is intentionally untouched: inline text is
+ // supplied by the caller, and detailed parse messages remain
+ // load-bearing for model authors.
+
+ @Test void testModelFileNoRestrictionWhenBaseDirectoryUnset() {
+ assertThat(ModelHandler.modelFile("", "anywhere/model.json").getPath(),
+ is(new File("anywhere/model.json").getPath()));
+ }
+
+ @Test void testModelFileRelativePathResolvesUnderBaseDirectory(
+ @TempDir Path tempDir) {
+ final File file =
+ ModelHandler.modelFile(tempDir.toString(), "sub/model.json");
+ assertThat(file.toPath().startsWith(tempDir), is(true));
+ assertThat(file.getName(), is("model.json"));
+ }
+
+ @Test void testModelFileRelativeEscapeRejected(@TempDir Path tempDir) {
+ SecurityException e =
+ assertThrows(SecurityException.class, () ->
+ ModelHandler.modelFile(tempDir.resolve("base").toString(),
+ "../escape/model.json"));
+ assertThat(e.getMessage(),
+ containsString("calcite.model.baseDirectory"));
+ }
+
+ @Test void testModelFileAbsolutePathOutsideBaseDirectoryRejected(
+ @TempDir Path tempDir) {
+ final Path base = tempDir.resolve("base");
+ final Path outside = tempDir.resolve("outside/model.json");
+ assertThrows(SecurityException.class, () ->
+ ModelHandler.modelFile(base.toString(), outside.toString()));
+ }
+
+ @Test void testModelFileAbsolutePathInsideBaseDirectoryAllowed(
+ @TempDir Path tempDir) {
+ final Path inside = tempDir.resolve("model.json");
+ final File file =
+ ModelHandler.modelFile(tempDir.toString(), inside.toString());
+ assertThat(file.toPath(), is(inside));
+ }
+
+ /** A malformed model file surfaces a path-only message; source-byte
+ * fragments Jackson would otherwise quote in the parse error do not
+ * reach the client through the IOException chain. */
+ @Test void testMalformedModelFileReportsPathOnly(@TempDir Path tempDir) {
+ final String marker = "distinct-first-line-marker";
+ Path modelFile = tempDir.resolve("scratch.json");
+ try {
+ Files.write(modelFile, (marker + " not json
content").getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ SchemaPlus root = CalciteSchema.createRootSchema(false, false).plus();
+ IOException e =
+ assertThrows(IOException.class,
+ () -> new ModelHandler(root, modelFile.toString()));
+ assertThat(e.getMessage(), containsString(modelFile.toString()));
+ assertThat(e.getMessage(), containsString("see log of")); // Full info in
log
+ // Neither the top-level message nor any cause in the chain names the
+ // marker string from the file
+ Throwable t = e;
+ while (t != null) {
+ assertThat(String.valueOf(t.getMessage()),
+ not(containsString(marker)));
+ t = t.getCause();
+ }
+ }
+
+ /** A missing model file produces the same shape of message as a
+ * malformed one: the client cannot distinguish "does not exist" from
+ * "exists but unreadable" from "exists but invalid" via the error
+ * string. */
+ @Test void testMissingModelFileReportsGenericPathOnlyError(
+ @TempDir Path tempDir) {
+ final Path modelFile = tempDir.resolve("no-such-model.json");
+ SchemaPlus root = CalciteSchema.createRootSchema(false, false).plus();
+ IOException e =
+ assertThrows(IOException.class,
+ () -> new ModelHandler(root, modelFile.toString()));
+ assertThat(e.getMessage(), containsString(modelFile.toString()));
+ assertThat(e.getMessage(), containsString("see log of")); // Full info in
log
+ // The message must not carry the "FileNotFound"/"No such file"
+ // signatures that would otherwise distinguish existence
+ Throwable t = e;
+ while (t != null) {
+ assertThat(String.valueOf(t.getMessage()),
+ not(containsString("FileNotFound")));
+ assertThat(String.valueOf(t.getMessage()),
+ not(containsString("No such file")));
+ t = t.getCause();
+ }
+ }
+
+ /** Inline-model errors keep the detailed Jackson message: inline text
+ * comes from the caller, so echoing it back leaks nothing. */
+ @Test void testInlineModelErrorsKeepDetail() {
+ SchemaPlus root = CalciteSchema.createRootSchema(false, false).plus();
+ IOException e =
+ assertThrows(IOException.class,
+ () -> new ModelHandler(root, "inline:{ not valid json"));
+ // The inline branch throws the underlying Jackson IOException
+ // unchanged (or a wrapped one whose chain still names the parser).
+ // Assert only that we did not swap in the path-only message.
+ assertThat(String.valueOf(e.getMessage()),
+ not(containsString("see log of")));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7908">[CALCITE-7908]
+ * ModelHandler should apply the model class-name filter to the JDBC driver
+ * auto-detected from jdbcUrl</a>. */
+ @Test void testJdbcSchemaWithoutDriverChecksDetectedDriver() {
+ final String model = "inline:{"
+ + " version: '1.0',"
+ + " defaultSchema: 'SCOTT',"
+ + " schemas: [ {"
+ + " type: 'jdbc',"
+ + " name: 'SCOTT',"
+ + " jdbcUser: 'SA',"
+ + " jdbcPassword: '',"
+ + " jdbcUrl: 'jdbc:hsqldb:res:scott',"
+ + " jdbcSchema: 'SCOTT'"
+ + " } ]"
+ + "}";
+ // The model has no jdbcDriver, so the filter must see the driver
+ // that DriverManager picks for the URL
+ final SchemaPlus root = CalciteSchema.createRootSchema(false,
false).plus();
+ Throwable e =
+ assertThrows(RuntimeException.class, () ->
+ new ModelHandler(root, model, ClassNameFilter.of("",
"org.apache.calcite.")));
+ while (e != null && !(e instanceof SecurityException)) {
+ e = e.getCause();
+ }
+ assertThat("expected SecurityException in chain", e, notNullValue());
+ assertThat(e.getMessage(), containsString("org.hsqldb.jdbc.JDBCDriver"));
+
+ final SchemaPlus root2 = CalciteSchema.createRootSchema(false,
false).plus();
+ assertDoesNotThrow(() ->
+ new ModelHandler(root2, model, ClassNameFilter.of("", "org.hsqldb.")));
+ }
+
}
diff --git a/site/_docs/history.md b/site/_docs/history.md
index 0eb0fe88c3..511cfd6b01 100644
--- a/site/_docs/history.md
+++ b/site/_docs/history.md
@@ -84,7 +84,9 @@ #### Breaking Changes
* [<a
href="https://issues.apache.org/jira/browse/CALCITE-7713">CALCITE-7713</a>]
Class loading from model files has been disabled by default. Any attempt to
load
classes from model files will lead to `SecurityException` unless an appropriate
-pattern is set in `calcite.model.classes.allowed` system property.
+pattern is set in `calcite.model.classes.allowed` system property. This also
applies
+to the JDBC driver that `DriverManager` chooses for `jdbcUrl` when a JDBC
schema
+does not specify `jdbcDriver`.
* [<a
href="https://issues.apache.org/jira/browse/CALCITE-7716">CALCITE-7716</a>]
`UUID` values are now ordered as unsigned 128-bit values, as SQL requires.
diff --git a/site/_docs/model.md b/site/_docs/model.md
index 567313d973..5485d0597c 100644
--- a/site/_docs/model.md
+++ b/site/_docs/model.md
@@ -249,6 +249,9 @@ #### YAML
`jdbcDriver` (optional string) is the name of the JDBC driver class. If not
specified, uses whichever class the JDBC DriverManager chooses.
+Note: the class must pass the
+[`calcite.model.classes.allowed`]({{ site.apiRoot
}}/org/apache/calcite/config/CalciteSystemProperty.html#MODEL_CLASSES_ALLOWED)
+system property.
`jdbcUrl` (optional string) is the JDBC connect string, for example
"jdbc:mysql://localhost/foodmart".