GH-2474 - Allow escaped properties in custom sort order.

Fixes #2474.
This commit is contained in:
Michael Simons
2022-02-07 12:30:00 +01:00
parent b02f0eb401
commit 58d0978814
7 changed files with 327 additions and 6 deletions

View File

@@ -490,13 +490,18 @@ public enum CypherGenerator {
if (LOOKS_LIKE_A_FUNCTION.matcher(property).matches()) {
expression = Cypher.raw(property);
} else if (property.contains(".")) {
String[] path = property.split("\\.");
if (path.length != 2) {
throw new IllegalArgumentException(String.format(
"Cannot handle order property `%s`, it must be a simple property or one-hop path.",
property));
int firstDot = property.indexOf('.');
String tail = property.substring(firstDot + 1);
if (tail.isEmpty() || property.lastIndexOf(".") != firstDot) {
if (tail.trim().matches("`.+`")) {
tail = tail.replaceFirst("`(.+)`", "$1");
} else {
throw new IllegalArgumentException(String.format(
"Cannot handle order property `%s`, it must be a simple property or one-hop path.",
property));
}
}
expression = Cypher.property(path[0], path[1]);
expression = Cypher.property(property.substring(0, firstDot), tail);
} else {
expression = Cypher.name(property);
}

View File

@@ -176,6 +176,13 @@ class CypherGeneratorTest {
.withMessageMatching("Cannot handle order property `.*`, it must be a simple property or one-hop path\\.");
}
@Test // GH-2474
void shouldNotFailOnMultipleEscapedHops() {
Optional<String> fragment = Optional.ofNullable(CypherGenerator.INSTANCE.createOrderByFragment(Sort.by("n.`a.b.c`")));
assertThat(fragment).hasValue("ORDER BY n.`a.b.c` ASC");
}
@CsvSource(delimiterString = "|", value = {
"apoc.text.clean(department.name) |false| ORDER BY apoc.text.clean(department.name) ASC",
"apoc.text.clean(department.name) |true | ORDER BY apoc.text.clean(department.name) DESC",

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* 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
*
* https://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.springframework.data.neo4j.integration.issues.gh2474;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;
/**
* @author Stephen Jackson
*/
@Node
@Data
public class CityModel {
@Id
@GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class)
private UUID cityId;
@Relationship(value = "MAYOR")
private PersonModel mayor;
@Relationship(value = "CITIZEN")
private List<PersonModel> citizens = new ArrayList<>();
@Relationship(value = "EMPLOYEE")
private List<JobRelationship> cityEmployees = new ArrayList<>();
private String name;
@Property("exotic.property")
private String exoticProperty;
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* 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
*
* https://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.springframework.data.neo4j.integration.issues.gh2474;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
/**
* @author Stephen Jackson
* @author Michael J. Simons
*/
public interface CityModelRepository extends Neo4jRepository<CityModel, UUID> {
@Query(""
+ "MATCH (n:CityModel)"
+ "RETURN n :#{orderBy(#sort)}")
List<CityModel> customQuery(Sort sort);
long deleteAllByExoticProperty(String property);
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* 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
*
* https://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.springframework.data.neo4j.integration.issues.gh2474;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author Stephen Jackson
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
public class GH2474IT {
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
@Autowired
Driver driver;
@Autowired
BookmarkCapture bookmarkCapture;
@Autowired
CityModelRepository cityModelRepository;
@BeforeEach
void setupData() {
cityModelRepository.deleteAll();
CityModel aachen = new CityModel();
aachen.setName("Aachen");
aachen.setExoticProperty("Cars");
CityModel utrecht = new CityModel();
utrecht.setName("Utrecht");
utrecht.setExoticProperty("Bikes");
cityModelRepository.saveAll(Arrays.asList(aachen, utrecht));
}
@Test
public void testStoreExoticProperty() {
CityModel cityModel = new CityModel();
cityModel.setName("The Jungle");
cityModel.setExoticProperty("lions");
cityModel = cityModelRepository.save(cityModel);
CityModel reloaded = cityModelRepository.findById(cityModel.getCityId())
.orElseThrow(RuntimeException::new);
assertThat(reloaded.getExoticProperty()).isEqualTo("lions");
long cnt = cityModelRepository.deleteAllByExoticProperty("lions");
assertThat(cnt).isOne();
}
@Test
public void testSortOnExoticProperty() {
Sort sort = Sort.by(Sort.Order.asc("exoticProperty"));
List<CityModel> cityModels = cityModelRepository.findAll(sort);
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
}
@Test
public void testSortOnExoticPropertyCustomQuery_MakeSureIUnderstand() {
Sort sort = Sort.by(Sort.Order.asc("n.name"));
List<CityModel> cityModels = cityModelRepository.customQuery(sort);
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Cars", "Bikes");
}
@Test
public void testSortOnExoticPropertyCustomQuery() {
Sort sort = Sort.by(Sort.Order.asc("n.`exotic.property`"));
List<CityModel> cityModels = cityModelRepository.customQuery(sort);
assertThat(cityModels).extracting(CityModel::getExoticProperty).containsExactly("Bikes", "Cars");
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories
static class Config extends AbstractNeo4jConfig {
@Bean
public BookmarkCapture bookmarkCapture() {
return new BookmarkCapture();
}
@Override
public PlatformTransactionManager transactionManager(
Driver driver, DatabaseSelectionProvider databaseNameProvider) {
BookmarkCapture bookmarkCapture = bookmarkCapture();
return new Neo4jTransactionManager(driver, databaseNameProvider,
Neo4jBookmarkManager.create(bookmarkCapture));
}
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* 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
*
* https://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.springframework.data.neo4j.integration.issues.gh2474;
import lombok.Data;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
/**
* @author Stephen Jackson
*/
@RelationshipProperties
@Data
public class JobRelationship {
@Id
@GeneratedValue
private Long id;
@TargetNode
private PersonModel person;
private String jobTitle;
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2011-2022 the original author or authors.
*
* 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
*
* https://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.springframework.data.neo4j.integration.issues.gh2474;
import lombok.Data;
import java.util.UUID;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
/**
* @author Stephen Jackson
*/
@Node
@Data
public class PersonModel {
@Id
@GeneratedValue(generatorClass = GeneratedValue.UUIDGenerator.class)
private UUID personId;
private String address;
private String name;
private String favoriteFood;
}