Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP snapshots #560

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions src/main/java/liquibase/ext/neo4j/database/Neo4jDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -90,14 +92,33 @@ public boolean supportsTablespaces() {

@Override
public boolean supportsCatalogs() {
return neo4jVersion.startsWith("4") || isV5OrLater();
return isGreaterOrEqualThanMajor(4);
}

@Override
public boolean supportsSequences() {
return false;
}

@Override
public boolean supportsSchemas() {
return false;
}

@Override
public boolean supports(Class<? extends DatabaseObject> 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;
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1250,10 +1278,10 @@ public Transaction getOrBeginTransaction() {
}

// visible for testing

final Transaction getTransaction() {
return transaction;
}

final Session openSession() {
return driver.session(this.sessionConfig);
}
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends DatabaseObject> 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 extends DatabaseObject> 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<? extends DatabaseObject>[] addsTo() {
return new Class[]{Catalog.class};
}

@Override
@SuppressWarnings("unchecked")
public Class<? extends SnapshotGenerator>[] replaces() {
return new Class[] {CatalogSnapshotGenerator.class};
}

private static List<Label> retrieveLabels(Neo4jDatabase database, Catalog catalog) throws DatabaseException {
try {
return database.run(new RawSqlStatement("CALL db.labels() YIELD label RETURN label"))
.stream()
.map(row -> new Label(catalog, (String) row.get("label")))
.collect(Collectors.toList());
} catch (LiquibaseException e) {
throw new DatabaseException("Could not retrieve node labels during label snapshot", e);
}
}
}
Loading
Loading