Skip to content

Commit

Permalink
Cover text sources
Browse files Browse the repository at this point in the history
  • Loading branch information
fbiville committed Mar 5, 2024
1 parent 79c5d7a commit 7dca931
Show file tree
Hide file tree
Showing 18 changed files with 1,517 additions and 543 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
public class ImportSpecificationDeserializer {
private static final YAMLMapper MAPPER = YAMLMapper.builder()
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
.disable(MapperFeature.AUTO_DETECT_CREATORS)
.build();

private static final JsonSchema SCHEMA = JsonSchemaFactory.getInstance(VersionFlag.V202012)
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/org/neo4j/importer/v1/sources/NamedJdbcSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,25 @@
public class NamedJdbcSource extends Source {

private final String dataSource;
private final String query;
private final String sql;

@JsonCreator
public NamedJdbcSource(
@JsonProperty(value = "name", required = true) String name,
@JsonProperty(value = "data_source", required = true) String dataSource,
@JsonProperty(value = "query", required = true) String query) {
@JsonProperty(value = "sql", required = true) String sql) {

super(name, SourceType.JDBC);
this.dataSource = dataSource;
this.query = query;
this.sql = sql;
}

public String getDataSource() {
return dataSource;
}

public String getQuery() {
return query;
public String getSql() {
return sql;
}

@Override
Expand All @@ -50,19 +50,19 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
NamedJdbcSource that = (NamedJdbcSource) o;
return Objects.equals(dataSource, that.dataSource) && Objects.equals(query, that.query);
return Objects.equals(dataSource, that.dataSource) && Objects.equals(sql, that.sql);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), dataSource, query);
return Objects.hash(super.hashCode(), dataSource, sql);
}

@Override
public String toString() {
return "NamedJdbcSource{" + "dataSource='"
+ dataSource + '\'' + ", query='"
+ query + '\'' + "} "
+ sql + '\'' + "} "
+ super.toString();
}
}
9 changes: 5 additions & 4 deletions src/main/java/org/neo4j/importer/v1/sources/Source.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import java.util.Objects;

@JsonTypeInfo(use = Id.NAME, property = "type")
@JsonTypeInfo(use = Id.DEDUCTION)
@JsonSubTypes({
@Type(value = BigQuerySource.class, name = "bigquery"),
@Type(value = NamedJdbcSource.class, name = "jdbc"),
@Type(value = TextSource.class, name = "text")
@Type(value = BigQuerySource.class),
@Type(value = ExternalTextSource.class),
@Type(value = InlineTextSource.class),
@Type(value = NamedJdbcSource.class),
})
public abstract class Source {

Expand Down
8 changes: 1 addition & 7 deletions src/main/java/org/neo4j/importer/v1/sources/TextSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,10 @@
*/
package org.neo4j.importer.v1.sources;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import java.util.List;
import java.util.Objects;

@JsonTypeInfo(use = Id.DEDUCTION)
@JsonSubTypes({@Type(value = ExternalTextSource.class), @Type(value = InlineTextSource.class)})
abstract class TextSource extends Source {
public abstract class TextSource extends Source {

private final List<String> header;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
*/
package org.neo4j.importer.v1.targets;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;

public class CustomQueryTarget extends Target {

private final String query;

@JsonCreator
public CustomQueryTarget(
@JsonProperty(value = "active", defaultValue = DEFAULT_ACTIVE) Boolean active,
@JsonProperty(value = "name", required = true) String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.neo4j.importer.v1.targets;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
Expand All @@ -25,6 +26,7 @@ public class NodeKeyConstraint extends NodeConstraint {
private final List<String> properties;
private final Map<String, Object> options;

@JsonCreator
public NodeKeyConstraint(
@JsonProperty(value = "name", required = true) String name,
@JsonProperty(value = "label", required = true) String label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.neo4j.importer.v1.targets;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
Expand All @@ -26,6 +27,7 @@ public class NodeUniqueConstraint extends NodeConstraint {
private final List<String> properties;
private final Map<String, Object> options;

@JsonCreator
public NodeUniqueConstraint(
@JsonProperty(value = "name", required = true) String name,
@JsonProperty(value = "label", required = true) String label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.neo4j.importer.v1.targets;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collections;
import java.util.List;
Expand All @@ -33,6 +34,7 @@ public class RelationshipTarget extends Target {
private final List<PropertyMapping> properties;
private final RelationshipSchema schema;

@JsonCreator
public RelationshipTarget(
@JsonProperty(value = "active", defaultValue = DEFAULT_ACTIVE) Boolean active,
@JsonProperty(value = "name", required = true) String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public boolean report(Builder builder) {
}

class NameCounter {
private static final String ERROR_CODE = "DUPL-001";

private final Map<String, List<String>> pathsUsingName = new LinkedHashMap<>();

Expand All @@ -85,7 +86,7 @@ public boolean reportErrorsIfAny(Builder builder) {
result.set(true);
builder.addError(
paths.get(0),
"DUPN",
ERROR_CODE,
String.format(
"Name \"%s\" is duplicated across the following paths: %s",
entry.getKey(), String.join(", ", paths)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.importer.v1.validation.plugin;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.neo4j.importer.v1.sources.Source;
import org.neo4j.importer.v1.sources.TextSource;
import org.neo4j.importer.v1.validation.SpecificationValidationResult.Builder;
import org.neo4j.importer.v1.validation.SpecificationValidator;

public class NoDuplicatedSourceHeaderColumn implements SpecificationValidator {
private static final String ERROR_CODE = "DUPL-002";

private final Map<String, DuplicatedHeader> sourcePathToDuplicates;

public NoDuplicatedSourceHeaderColumn() {
sourcePathToDuplicates = new LinkedHashMap<>();
}

@Override
public void visitSource(int index, Source source) {
if (!(source instanceof TextSource)) {
return;
}
TextSource textSource = (TextSource) source;
List<String> header = textSource.getHeader();
if (header == null) {
return;
}
String sourcePath = String.format("$.sources[%d].header", index);
getDuplicates(header).forEach(duplicate -> sourcePathToDuplicates.put(sourcePath, duplicate));
}

@Override
public boolean report(Builder builder) {
if (sourcePathToDuplicates.isEmpty()) {
return false;
}
sourcePathToDuplicates.forEach((sourcePath, duplicate) -> {
builder.addError(
sourcePath,
ERROR_CODE,
String.format(
"%s defines column \"%s\" %d times, it must be defined at most once",
sourcePath, duplicate.getName(), duplicate.getCount()));
});
return true;
}

private static List<DuplicatedHeader> getDuplicates(List<String> header) {
return header.stream().collect(groupingBy(identity(), counting())).entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.map(entry -> new DuplicatedHeader(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
}

class DuplicatedHeader {
private final String name;
private final long count;

public DuplicatedHeader(String name, long count) {
this.name = name;
this.count = count;
}

public String getName() {
return name;
}

public long getCount() {
return count;
}

@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
DuplicatedHeader that = (DuplicatedHeader) object;
return count == that.count && Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(name, count);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.importer.v1.validation.plugin;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.neo4j.importer.v1.sources.InlineTextSource;
import org.neo4j.importer.v1.sources.Source;
import org.neo4j.importer.v1.validation.SpecificationValidationResult.Builder;
import org.neo4j.importer.v1.validation.SpecificationValidator;

public class NoInconsistentInlineSourceDataValidator implements SpecificationValidator {

private static final String ERROR_CODE = "MCOL-001";

private final Map<String, CountMismatch> pathToCountMismatch;

public NoInconsistentInlineSourceDataValidator() {
pathToCountMismatch = new LinkedHashMap<>();
}

@Override
public void visitSource(int sourceIndex, Source source) {
if (!(source instanceof InlineTextSource)) {
return;
}
InlineTextSource inlineSource = (InlineTextSource) source;
int columnCount = inlineSource.getHeader().size();
List<List<Object>> data = inlineSource.getData();
String path = "$.sources[%d].data[%d]";
for (int rowIndex = 0; rowIndex < data.size(); rowIndex++) {
int rowColumnCount = data.get(rowIndex).size();
if (rowColumnCount < columnCount) {
String rowPath = String.format(path, sourceIndex, rowIndex);
pathToCountMismatch.put(rowPath, new CountMismatch(columnCount, rowColumnCount));
}
}
}

@Override
public boolean report(Builder builder) {
if (pathToCountMismatch.isEmpty()) {
return false;
}
pathToCountMismatch.forEach((path, count) -> {
builder.addError(
path,
ERROR_CODE,
String.format(
"row defines %d column(s), expected at least %d",
count.getActualCount(), count.getExpectedCount()));
});
return true;
}
}

class CountMismatch {
private final int expectedCount;
private final int actualCount;

public CountMismatch(int expectedCount, int actualCount) {
this.expectedCount = expectedCount;
this.actualCount = actualCount;
}

public int getExpectedCount() {
return expectedCount;
}

public int getActualCount() {
return actualCount;
}

@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
CountMismatch that = (CountMismatch) object;
return expectedCount == that.expectedCount && actualCount == that.actualCount;
}

@Override
public int hashCode() {
return Objects.hash(expectedCount, actualCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ org.neo4j.importer.v1.validation.plugin.NoDanglingSourceValidator
org.neo4j.importer.v1.validation.plugin.NoDanglingDependsOnValidator
org.neo4j.importer.v1.validation.plugin.NoDanglingNodeReferenceValidator
org.neo4j.importer.v1.validation.plugin.NoDependencyCycleValidator
org.neo4j.importer.v1.validation.plugin.NoDuplicatedSourceHeaderColumn
org.neo4j.importer.v1.validation.plugin.NoInconsistentInlineSourceDataValidator
Loading

0 comments on commit 7dca931

Please sign in to comment.