diff --git a/src/main/java/liquibase/ext/neo4j/database/Neo4jDatabase.java b/src/main/java/liquibase/ext/neo4j/database/Neo4jDatabase.java index 3b0f3816..e4ecff63 100644 --- a/src/main/java/liquibase/ext/neo4j/database/Neo4jDatabase.java +++ b/src/main/java/liquibase/ext/neo4j/database/Neo4jDatabase.java @@ -9,6 +9,8 @@ import liquibase.executor.ExecutorService; import liquibase.statement.SqlStatement; import liquibase.statement.core.RawSqlStatement; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Catalog; import java.util.List; import java.util.Locale; @@ -57,7 +59,7 @@ protected String getDefaultDatabaseProductName() { @Override public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException { - return conn.getDatabaseProductName().equals("Neo4j"); + return conn.getDatabaseProductName().startsWith("Neo4j"); } @Override @@ -90,7 +92,12 @@ public boolean supportsTablespaces() { @Override public boolean supportsCatalogs() { - return neo4jVersion.startsWith("4") || isV5OrLater(); + return isGreaterOrEqualThanMajor(4); + } + + @Override + public boolean supportsSequences() { + return false; } @Override @@ -98,6 +105,20 @@ public boolean supportsSchemas() { return false; } + @Override + public boolean supports(Class object) { + if (Catalog.class.isAssignableFrom(object)) { + return isGreaterOrEqualThanMajor(4); + } + return object.getPackage().getName().startsWith("liquibase.ext.neo4j"); + } + + @Override + public String getDefaultCatalogName() { + return "neo4j"; + } + + @Override public boolean isCaseSensitive() { return true; @@ -109,7 +130,7 @@ public void createIndex(String name, String label, String property) throws Datab createIndexForNeo4j3(label, property); } else if (neo4jVersion.startsWith("4")) { createIndexForNeo4j4(name, label, property); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { createIndexForNeo4j5(name, label, property); } else { throw new DatabaseException(String.format( @@ -127,7 +148,7 @@ public void createUniqueConstraint(String name, String label, String property) t createUniqueConstraintForNeo4j3(label, property); } else if (neo4jVersion.startsWith("4")) { createUniqueConstraintForNeo4j4(name, label, property); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { createUniqueConstraintForNeo4j5(name, label, property); } else { throw new DatabaseException(String.format( @@ -149,7 +170,7 @@ public void createNodeKeyConstraint(String name, String label, String firstPrope createNodeKeyConstraintForNeo4j3(label, properties); } else if (neo4jVersion.startsWith("4")) { createNodeKeyConstraintForNeo4j4(name, label, properties); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { createNodeKeyConstraintForNeo4j5(name, label, properties); } else { throw new DatabaseException(String.format( @@ -167,7 +188,7 @@ public void dropIndex(String name, String label, String property) throws Databas dropIndexForNeo4j3(label, property); } else if (neo4jVersion.startsWith("4")) { dropIndexForNeo4j4(name); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { dropIndexForNeo4j5(name); } else { throw new DatabaseException(String.format( @@ -185,7 +206,7 @@ public void dropUniqueConstraint(String name, String label, String property) thr dropUniqueConstraintForNeo4j3(label, property); } else if (neo4jVersion.startsWith("4")) { dropConstraintForNeo4j4(name); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { dropConstraintForNeo4j5(name); } else { throw new DatabaseException(String.format( @@ -207,7 +228,7 @@ public void dropNodeKeyConstraint(String name, String label, String firstPropert dropNodeKeyConstraintForNeo4j3(label, properties); } else if (neo4jVersion.startsWith("4")) { dropConstraintForNeo4j4(name); - } else if (isV5OrLater()) { + } else if (isGreaterOrEqualThanMajor(5)) { dropConstraintForNeo4j5(name); } else { throw new DatabaseException(String.format( @@ -228,7 +249,11 @@ public void execute(SqlStatement statement) throws LiquibaseException { } public boolean supportsCallInTransactions() { - return neo4jVersion.startsWith("4.4") || isV5OrLater(); + return is44OrLater(); + } + + public boolean is44OrLater() { + return neo4jVersion.startsWith("4.4") || isGreaterOrEqualThanMajor(5); } public String getNeo4jVersion() { @@ -476,7 +501,8 @@ private void rollbackOnError(LiquibaseException le) { throw convertToRuntimeException(le); } - private boolean isV5OrLater() { - return Integer.parseInt(neo4jVersion.substring(0, 1), 10) >= 5; + private boolean isGreaterOrEqualThanMajor(int major) { + int nextDot = neo4jVersion.indexOf("."); + return Integer.parseInt(neo4jVersion.substring(0, nextDot), 10) >= major; } } diff --git a/src/main/java/liquibase/ext/neo4j/database/jdbc/Neo4jConnection.java b/src/main/java/liquibase/ext/neo4j/database/jdbc/Neo4jConnection.java index d0038a76..dd35f0f6 100644 --- a/src/main/java/liquibase/ext/neo4j/database/jdbc/Neo4jConnection.java +++ b/src/main/java/liquibase/ext/neo4j/database/jdbc/Neo4jConnection.java @@ -27,6 +27,7 @@ import java.sql.Savepoint; import java.sql.Statement; import java.sql.Struct; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; @@ -38,6 +39,8 @@ import static liquibase.ext.neo4j.database.jdbc.SupportedJdbcUrl.normalizeUri; class Neo4jConnection implements Connection, DatabaseMetaData { + private static final String SERVER_VERSION_QUERY = + "CALL dbms.components() YIELD name, edition, versions WHERE name = \"Neo4j Kernel\" RETURN edition, versions[0] AS version LIMIT 1"; private final String uri; private final Driver driver; @@ -46,6 +49,8 @@ class Neo4jConnection implements Connection, DatabaseMetaData { private Transaction transaction; private boolean autocommit = true; private boolean closed; + private String neo4jVersion; + private String neo4jEdition; public Neo4jConnection(String url, Properties info) { this(url, info, new DriverConfigSupplier(QueryStringParser.parseQueryString(url.replaceFirst("jdbc:neo4j:", "")), info)); @@ -154,13 +159,15 @@ public boolean nullsAreSortedAtEnd() { } @Override - public String getDatabaseProductName() { - return "Neo4j"; + public String getDatabaseProductName() throws SQLException { + readNeo4jVersionAndEdition(); + return String.format("Neo4j (%s Edition)", neo4jEdition.equals("enterprise") ? "Enterprise" : "Community"); } @Override - public String getDatabaseProductVersion() { - return null; + public String getDatabaseProductVersion() throws SQLException { + readNeo4jVersionAndEdition(); + return neo4jVersion; } @Override @@ -901,13 +908,25 @@ public int getResultSetHoldability() { } @Override - public int getDatabaseMajorVersion() { - return 0; + public int getDatabaseMajorVersion() throws SQLException { + readNeo4jVersionAndEdition(); + String[] versionComponents = neo4jVersion.split("\\."); + if (versionComponents.length <= 1) { + throw new SQLException(String.format("Unrecognized Neo4j version string: %s", neo4jVersion)); + } + String major = versionComponents[0]; + return Integer.parseInt(major, 10); } @Override - public int getDatabaseMinorVersion() { - return 0; + public int getDatabaseMinorVersion() throws SQLException { + readNeo4jVersionAndEdition(); + String[] versionComponents = neo4jVersion.split("\\."); + if (versionComponents.length <= 1) { + throw new SQLException(String.format("Unrecognized Neo4j version string: %s", neo4jVersion)); + } + String minor = versionComponents[1]; + return Integer.parseInt(minor, 10); } @Override @@ -986,8 +1005,17 @@ public void setCatalog(String catalog) { } @Override - public String getCatalog() { - return null; + public String getCatalog() throws SQLException { + try (ResultSet resultSet = this.createStatement().executeQuery("CALL db.info() YIELD name RETURN name")) { + if (!resultSet.next()) { + throw new SQLException("Could not retrieve current database name (catalog): expected 1 row, got none"); + } + String databaseName = resultSet.getString("name"); + if (resultSet.next()) { + throw new SQLException("Could not retrieve current database name (catalog): expected 1 row, got at least 2"); + } + return databaseName; + } } @Override @@ -1250,10 +1278,10 @@ public Transaction getOrBeginTransaction() { } // visible for testing + final Transaction getTransaction() { return transaction; } - final Session openSession() { return driver.session(this.sessionConfig); } @@ -1280,4 +1308,18 @@ private static void ensureResultSetConcurrency(int resultSetConcurrency) throws throw new SQLFeatureNotSupportedException("only CONCUR_READ_ONLY is supported"); } } + + private void readNeo4jVersionAndEdition() throws SQLException { + if (this.neo4jVersion != null) { + return; + } + try (ResultSet results = this.createStatement().executeQuery(SERVER_VERSION_QUERY)) { + if (!results.next()) { + throw new SQLException("Could not retrieve Neo4j version and edition"); + } + results.next(); + this.neo4jVersion = results.getString("version"); + this.neo4jEdition = results.getString("edition").toLowerCase(Locale.ENGLISH); + } + } } diff --git a/src/main/java/liquibase/ext/neo4j/snapshot/LabelSnapshotGeneratorNeo4j.java b/src/main/java/liquibase/ext/neo4j/snapshot/LabelSnapshotGeneratorNeo4j.java new file mode 100644 index 00000000..c0b42c71 --- /dev/null +++ b/src/main/java/liquibase/ext/neo4j/snapshot/LabelSnapshotGeneratorNeo4j.java @@ -0,0 +1,80 @@ +package liquibase.ext.neo4j.snapshot; + +import liquibase.database.Database; +import liquibase.exception.DatabaseException; +import liquibase.exception.LiquibaseException; +import liquibase.ext.neo4j.database.Neo4jDatabase; +import liquibase.ext.neo4j.structure.Label; +import liquibase.snapshot.DatabaseSnapshot; +import liquibase.snapshot.InvalidExampleException; +import liquibase.snapshot.SnapshotGenerator; +import liquibase.snapshot.SnapshotGeneratorChain; +import liquibase.snapshot.jvm.CatalogSnapshotGenerator; +import liquibase.statement.core.RawSqlStatement; +import liquibase.structure.DatabaseObject; +import liquibase.structure.core.Catalog; + +import java.util.List; +import java.util.stream.Collectors; + +public class LabelSnapshotGeneratorNeo4j implements SnapshotGenerator { + + @Override + public int getPriority(Class objectType, Database database) { + if (!(database instanceof Neo4jDatabase)) { + return PRIORITY_NONE; + } + if (Label.class.isAssignableFrom(objectType)) { + return PRIORITY_DEFAULT; + } + if (Catalog.class.isAssignableFrom(objectType)) { + return PRIORITY_ADDITIONAL; + } + return PRIORITY_NONE; + } + + @Override + public T snapshot(T example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) throws DatabaseException, InvalidExampleException { + Database database = snapshot.getDatabase(); + if (!(database instanceof Neo4jDatabase)) { + return chain.snapshot(example, snapshot); + } + if (!snapshot.getSnapshotControl().shouldInclude(Label.class)) { + return chain.snapshot(example, snapshot); + } + if (example instanceof Label) { + return example; + } + if (!(example instanceof Catalog)) { + return chain.snapshot(example, snapshot); + } + Catalog catalog = (Catalog) example; + Neo4jDatabase neo4j = (Neo4jDatabase) snapshot.getDatabase(); + retrieveLabels(neo4j, catalog) + .forEach(catalog::addDatabaseObject); + return example; + } + + @Override + @SuppressWarnings("unchecked") + public Class[] addsTo() { + return new Class[]{Catalog.class}; + } + + @Override + @SuppressWarnings("unchecked") + public Class[] replaces() { + return new Class[] {CatalogSnapshotGenerator.class}; + } + + private static List