Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/maven/com.puppycrawl.tools-checks…
Browse files Browse the repository at this point in the history
…tyle-10.21.0
  • Loading branch information
michael-simons authored Dec 16, 2024
2 parents 069df45 + 0d263f0 commit e5e1d2e
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 140 deletions.
14 changes: 14 additions & 0 deletions benchkit/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- Can't use -dangling-doc-comments in Java 17, see https://docs.oracle.com/en/java/javase/23/docs/specs/man/javac.html,
so we enable all and print the warnings only for this module. -->
<configuration combine.self="override">
<forceLegacyJavacApi>true</forceLegacyJavacApi>
<showWarnings>true</showWarnings>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
16 changes: 11 additions & 5 deletions docs/src/main/asciidoc/modules/ROOT/pages/metadata.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ From the SQL 1992 standard (find an archived copy http://www.contrib.andrew.cmu.

> (4.12) Catalogs are named collections of schemas in an SQL-environment. An SQL-environment contains zero or more catalogs. A catalog contains one or more schemas, but always contains a schema named INFORMATION_SCHEMA that contains the views and domains of the Information Schema.

This driver does not support catalogs (see https://github.com/neo4j/neo4j-jdbc/discussions/55[discussion 55]), so any metadata result set will return literal `null` when asked for the catalog of a database object.
No metadata method supports filtering on a non-null catalog parameter and no catalog specifier can be used in a query.
Future developments might use catalogs to describe composite databases, in essence listing the constituents of the composite database defined in the connection.
This driver only supports a single catalog, which is equal to the Neo4j database to which the driver is connected to.
Filtering on a catalog pattern is not supported in metadata queries, all values except literal `null`, the empty or blank string or the name of the current database will lead to an exception.

The same standard defines schemas as follows:

Expand All @@ -36,8 +35,8 @@ Labels will be reported as table objects with the `TABLE_TYPE` being literal `TA

=== Summary

* Catalog: Always `null`; filtering on anything non-null yields no results.
* Schema: Always `public`; filtering on `public` and literal will yield result, anything else won't.
* Catalog: Always equals to the current database; filtering on anything else except an empty string will error.
* Schema: Always `public`; filtering on `public` and literal `null` will yield result, anything else won't.
* Table descriptors: Reported as `TABLE` in the `TABLE_TYPE` column.

== Labels to tables
Expand All @@ -55,3 +54,10 @@ This driver therefore compute node types in a similar way:
** This is in line with the default SQL-to-Cypher translation
* Node type combinations will map to table names composed as `label1_label2`, sorting the labels alphabetically to make them independent of the order Neo4j returns them
* Property sets for these node type combinations will then be computed

== Primary keys

The driver uses available constraint information and will figure out if there is a single, unique constraint on a label.
If that's the case, the constrained property will be assumed to be the primary key.
This will also work for unique constraints over multiple properties, in SQL lingo composite primary keys.
If there is no unique constraint or more than one, we assume the `v$id` virtual columns for the `elementId` value to be primary keys.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.neo4j.jdbc.it.cp;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
Expand All @@ -32,12 +33,16 @@
import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.neo4j.jdbc.Neo4jConnection;
import org.neo4j.jdbc.Neo4jPreparedStatement;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -51,7 +56,7 @@ class DatabaseMetadataIT extends IntegrationTestBase {
private Connection connection;

DatabaseMetadataIT() {
super("neo4j:5.13.0-enterprise");
super("neo4j:5.26.0-enterprise");
}

@BeforeAll
Expand Down Expand Up @@ -105,14 +110,36 @@ void indexInfo(String table, boolean unique, List<IndexInfo> expected) throws SQ
assertThat(result).containsExactlyInAnyOrderElementsOf(expected);
}

@AfterEach
void dropConstraintsAndIndexes() throws SQLException {
dropConstraint0("SHOW CONSTRAINTS YIELD name", "DROP CONSTRAINT $constraint");
dropConstraint0("SHOW INDEXES YIELD name", "DROP INDEX $constraint");
}

private void dropConstraint0(String getConstraintsStatement, String dropConstraintsStatement) throws SQLException {
this.connection.setAutoCommit(false);
try (var stmt = this.connection.createStatement();
var results = stmt.executeQuery(getConstraintsStatement);
var stmt2 = this.connection.prepareStatement(dropConstraintsStatement)
.unwrap(Neo4jPreparedStatement.class)) {

while (results.next()) {
stmt2.setString("constraint", results.getString("name"));
stmt2.addBatch();
}
stmt2.executeBatch();
}
this.connection.setAutoCommit(true);
}

@Test
void getAllProcedures() throws SQLException {
try (var results = this.connection.getMetaData().getProcedures(null, null, null)) {
var resultCount = 0;
while (results.next()) {
resultCount++;
assertThat(results.getString(3)).isNotNull();
assertThat(results.getString(1)).isNull(); // Catalog
assertThat(results.getString(1)).isEqualTo(((Neo4jConnection) this.connection).getDatabaseName());
assertThat(results.getString(2)).isEqualTo("public"); // Schema
assertThat(results.getInt("PROCEDURE_TYPE")).isEqualTo(DatabaseMetaData.procedureResultUnknown);
}
Expand All @@ -127,7 +154,7 @@ void getAllFunctions() throws SQLException {
while (results.next()) {
resultCount++;
assertThat(results.getString(3)).isNotNull();
assertThat(results.getString(1)).isNull(); // Catalog
assertThat(results.getString(1)).isEqualTo(((Neo4jConnection) this.connection).getDatabaseName());
assertThat(results.getString(2)).isEqualTo("public"); // Schema
assertThat(results.getInt("FUNCTION_TYPE")).isEqualTo(DatabaseMetaData.functionResultUnknown);
}
Expand Down Expand Up @@ -197,25 +224,29 @@ void executeQueryWithoutResult(String query) throws SQLException {
@Test
void getAllCatalogsShouldReturnAnEmptyResultSet() throws SQLException {
var catalogRs = this.connection.getMetaData().getCatalogs();
assertThat(catalogRs.next()).isFalse();
assertThat(catalogRs.next()).isTrue();
assertThat(catalogRs.getString("TABLE_CAT")).isEqualTo("neo4j");
}

@Test
void getAllSchemasShouldReturnPublic() throws SQLException {
var schemasRs = this.connection.getMetaData().getSchemas();

if (schemasRs.next()) {
assertThat(schemasRs.getString(1)).isEqualTo("public");
}
assertThat(schemasRs.next()).isTrue();
assertThat(schemasRs.getString("TABLE_SCHEM")).isEqualTo("public");
assertThat(schemasRs.getString("TABLE_CATALOG")).isEqualTo("neo4j");
assertThat(schemasRs.next()).isFalse();
}

@Test
void getAllSchemasAskingForPublicShouldReturnPublic() throws SQLException {
var schemasRs = this.connection.getMetaData().getSchemas(null, "public");
@ParameterizedTest
@ValueSource(strings = { "", "%", " %", "% ", "public", "Public ", "%pub%" })
void getAllSchemasAskingForPublicShouldReturnPublic(String schemaPattern) throws SQLException {
var schemasRs = this.connection.getMetaData().getSchemas(null, schemaPattern);

if (schemasRs.next()) {
assertThat(schemasRs.getString(1)).isEqualTo("public");
}
assertThat(schemasRs.next()).isTrue();
assertThat(schemasRs.getString("TABLE_SCHEM")).isEqualTo("public");
assertThat(schemasRs.getString("TABLE_CATALOG")).isEqualTo("neo4j");
assertThat(schemasRs.next()).isFalse();
}

@Test
Expand All @@ -233,17 +264,17 @@ void testGetUser() throws SQLException {
}

@Test
void testGetDatabaseProductNameShouldReturnNeo4j() throws SQLException {
void getDatabaseProductNameShouldWork() throws SQLException {
var productName = this.connection.getMetaData().getDatabaseProductName();

assertThat(productName).isEqualTo("Neo4j Kernel-enterprise-5.13.0");
assertThat(productName).isEqualTo("Neo4j Kernel-enterprise-5.26.0");
}

@Test
void getDatabaseProductVersionShouldReturnTestContainerVersion() throws SQLException {
var productName = this.connection.getMetaData().getDatabaseProductVersion();
void getDatabaseProductVersionShouldWork() throws SQLException {
var productVersion = this.connection.getMetaData().getDatabaseProductVersion();

assertThat(productName).isEqualTo("5.13.0");
assertThat(productVersion).isEqualTo("5.26.0");
}

@Test
Expand Down Expand Up @@ -948,7 +979,8 @@ void getIndexInfoWithConstraint() throws Exception {
assertThat(resultSet.getInt("TYPE")).isEqualTo(3);
assertThat(resultSet.getInt("ORDINAL_POSITION")).isOne();
assertThat(resultSet.getString("COLUMN_NAME")).isEqualTo("uuid");
assertThat(resultSet.getObject("TABLE_CAT")).isNull();
assertThat(resultSet.getObject("TABLE_CAT"))
.isEqualTo(((Neo4jConnection) this.connection).getDatabaseName());
assertThat(resultSet.getObject("TABLE_SCHEM")).isEqualTo("public");
assertThat(resultSet.getObject("ASC_OR_DESC")).isEqualTo("A");
assertThat(resultSet.getObject("CARDINALITY")).isNull();
Expand Down Expand Up @@ -980,7 +1012,8 @@ void getIndexInfoWithBacktickLabels() throws Exception {
assertThat(resultSet.getInt("TYPE")).isEqualTo(3);
assertThat(resultSet.getInt("ORDINAL_POSITION")).isOne();
assertThat(resultSet.getString("COLUMN_NAME")).isEqualTo("uuid");
assertThat(resultSet.getObject("TABLE_CAT")).isNull();
assertThat(resultSet.getObject("TABLE_CAT"))
.isEqualTo(((Neo4jConnection) this.connection).getDatabaseName());
assertThat(resultSet.getObject("TABLE_SCHEM")).isEqualTo("public");
assertThat(resultSet.getObject("ASC_OR_DESC")).isEqualTo("A");
assertThat(resultSet.getObject("CARDINALITY")).isNull();
Expand Down Expand Up @@ -1083,7 +1116,8 @@ void getIndexInfoWithIndex() throws Exception {
assertThat(resultSet.getInt("TYPE")).isEqualTo(3);
assertThat(resultSet.getInt("ORDINAL_POSITION")).isOne();
assertThat(resultSet.getString("COLUMN_NAME")).isEqualTo("uuid");
assertThat(resultSet.getObject("TABLE_CAT")).isNull();
assertThat(resultSet.getObject("TABLE_CAT"))
.isEqualTo(((Neo4jConnection) this.connection).getDatabaseName());
assertThat(resultSet.getObject("TABLE_SCHEM")).isEqualTo("public");
assertThat(resultSet.getObject("ASC_OR_DESC")).isEqualTo("A");
assertThat(resultSet.getObject("CARDINALITY")).isNull();
Expand All @@ -1098,6 +1132,85 @@ void getIndexInfoWithIndex() throws Exception {
}
}

@Test
void catalogEqualsToDatabaseNameIsOk() {
assertThatNoException().isThrownBy(() -> this.connection.getMetaData().getTables("neo4j", null, null, null));
}

@Test
void primaryKeysWithoutUniqueConstraints() throws SQLException, IOException {

TestUtils.createMovieGraph(this.connection);

var primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Movie");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Movie", "v$id", 1, "Movie_elementId");
assertThat(primaryKeys.next()).isFalse();

primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Person_ACTED_IN_Movie");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Person_ACTED_IN_Movie", "v$id", 1, "Person_ACTED_IN_Movie_elementId");
assertThat(primaryKeys.next()).isFalse();
}

@Test
void primaryKeysForNonExistingTable() throws SQLException {
var primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Foobar");
assertThat(primaryKeys.next()).isFalse();
}

@Test
void primaryKeysWithUniqueConstraints() throws SQLException, IOException {

TestUtils.createMovieGraph(this.connection);

try (var stmt = this.connection.createStatement()) {
stmt.execute("CREATE CONSTRAINT movie_title FOR (n:Movie) REQUIRE n.title IS UNIQUE");
stmt.execute("CREATE CONSTRAINT movie_random_col FOR (n:Movie) REQUIRE n.whatever IS UNIQUE");
stmt.execute("CREATE CONSTRAINT person_id FOR (n:Person) REQUIRE n.id IS UNIQUE");
stmt.execute(
"CREATE CONSTRAINT acted_in_id IF NOT EXISTS FOR ()-[r:ACTED_IN]-() REQUIRE r.engagement_id IS UNIQUE");
}

var primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Movie");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Movie", "v$id", 1, "Movie_elementId");
assertThat(primaryKeys.next()).isFalse();

primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Person_ACTED_IN_Movie");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Person_ACTED_IN_Movie", "engagement_id", 1, "acted_in_id");
assertThat(primaryKeys.next()).isFalse();
}

private static void assertPrimaryKey(ResultSet primaryKeys, String tableName, String columnName, int seq,
String name) throws SQLException {
assertThat(primaryKeys.getString("TABLE_SCHEM")).isEqualTo("public");
assertThat(primaryKeys.getString("TABLE_CATALOG")).isEqualTo("neo4j");
assertThat(primaryKeys.getString("TABLE_NAME")).isEqualTo(tableName);
assertThat(primaryKeys.getString("COLUMN_NAME")).isEqualTo(columnName);
assertThat(primaryKeys.getInt("KEY_SEQ")).isEqualTo(seq);
assertThat(primaryKeys.getString("PK_NAME")).isEqualTo(name);
}

@Test
void primaryKeysWithMoreThanOneColumn() throws SQLException, IOException {

TestUtils.createMovieGraph(this.connection);

try (var stmt = this.connection.createStatement()) {
stmt.execute(
"CREATE CONSTRAINT movie_title_per_year FOR (n:Movie) REQUIRE (n.title, n.released) IS UNIQUE");
}

var primaryKeys = this.connection.getMetaData().getPrimaryKeys("neo4j", null, "Movie");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Movie", "title", 1, "movie_title_per_year");
assertThat(primaryKeys.next()).isTrue();
assertPrimaryKey(primaryKeys, "Movie", "released", 2, "movie_title_per_year");
assertThat(primaryKeys.next()).isFalse();
}

record IndexInfo(String tableName, boolean nonUnique, String indexName, int type, int ordinalPosition,
String columnName, String ascOrDesc) {
IndexInfo(ResultSet resultset) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
*/
package org.neo4j.jdbc.it.cp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;

Expand Down Expand Up @@ -60,6 +64,23 @@ static Connection getConnection(Neo4jContainer<?> neo4j) throws SQLException {
return getConnection(neo4j, false);
}

static void createMovieGraph(Connection connection) throws SQLException, IOException {
try (var stmt = connection.createStatement();
var reader = new BufferedReader(new InputStreamReader(
Objects.requireNonNull(TestUtils.class.getResourceAsStream("/movies.cypher"))))) {
var sb = new StringBuilder();
var buffer = new char[2048];
var l = 0;
while ((l = reader.read(buffer, 0, buffer.length)) > 0) {
sb.append(buffer, 0, l);
}
var statements = sb.toString().split(";");
for (String statement : statements) {
stmt.execute("/*+ NEO4J FORCE_CYPHER */ " + statement);
}
}
}

static Connection getConnection(Neo4jContainer<?> neo4j, boolean translate) throws SQLException {
var url = "jdbc:neo4j://%s:%d".formatted(neo4j.getHost(), neo4j.getMappedPort(7687));
var driver = DriverManager.getDriver(url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@
*/
package org.neo4j.jdbc.it.cp;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

import org.jooq.impl.ParserException;
Expand Down Expand Up @@ -197,20 +194,7 @@ record MovieAndRole(String title, List<String> roles) {
void joins() throws IOException, SQLException {

try (var connection = getConnection(true, false)) {
try (var stmt = connection.createStatement();
var reader = new BufferedReader(new InputStreamReader(
Objects.requireNonNull(TranslationIT.class.getResourceAsStream("/movies.cypher"))))) {
var sb = new StringBuilder();
var buffer = new char[2048];
var l = 0;
while ((l = reader.read(buffer, 0, buffer.length)) > 0) {
sb.append(buffer, 0, l);
}
var statements = sb.toString().split(";");
for (String statement : statements) {
stmt.execute("/*+ NEO4J FORCE_CYPHER */ " + statement);
}
}
TestUtils.createMovieGraph(connection);

try (var statement = connection.createStatement(); var rs = statement.executeQuery("""
SELECT name AS name, title AS title FROM Person p JOIN Movie m on (m = p.ACTED_IN)
Expand Down
Loading

0 comments on commit e5e1d2e

Please sign in to comment.