diff --git a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/models/storage/WriteRequest.java b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/models/storage/WriteRequest.java index 1bbe6bdac..b1ac78004 100644 --- a/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/models/storage/WriteRequest.java +++ b/here-naksha-lib-core/src/main/java/com/here/naksha/lib/core/models/storage/WriteRequest.java @@ -137,4 +137,8 @@ protected WriteRequest(@NotNull FeatureCodecFactory codecFactory features.add(codec); return self(); } + + public FeatureCodecFactory getCodecFactory() { + return codecFactory; + } } diff --git a/here-naksha-lib-psql/src/main/java/com/here/naksha/lib/psql/PostgresSession.java b/here-naksha-lib-psql/src/main/java/com/here/naksha/lib/psql/PostgresSession.java index f9bd76197..2a2720a54 100644 --- a/here-naksha-lib-psql/src/main/java/com/here/naksha/lib/psql/PostgresSession.java +++ b/here-naksha-lib-psql/src/main/java/com/here/naksha/lib/psql/PostgresSession.java @@ -528,8 +528,8 @@ > Result executeWrite( stmt.setBoolean(8, min_result); stmt.setBoolean(9, err_only); final ResultSet rs = stmt.executeQuery(); - final PsqlCursor cursor = - new PsqlCursor<>(XyzFeatureCodecFactory.get(), this, stmt, rs); + final PsqlCursor cursor = + new PsqlCursor<>(writeRequest.getCodecFactory(), this, stmt, rs); try (final PreparedStatement err_stmt = prepareStatement("SELECT naksha_err_no(), naksha_err_msg();")) { final ResultSet err_rs = err_stmt.executeQuery(); err_rs.next(); diff --git a/here-naksha-lib-psql/src/main/resources/naksha_plpgsql.sql b/here-naksha-lib-psql/src/main/resources/naksha_plpgsql.sql index ad8042255..08b5a62ff 100644 --- a/here-naksha-lib-psql/src/main/resources/naksha_plpgsql.sql +++ b/here-naksha-lib-psql/src/main/resources/naksha_plpgsql.sql @@ -1270,6 +1270,7 @@ BEGIN r_geometry = NULL; r_err = NULL; + op = ops[i]; feature = features[i]; id = naksha_feature_id(feature); IF id IS NULL THEN @@ -1300,7 +1301,6 @@ BEGIN r_geometry = geo; END IF; - op = ops[i]; IF op = 'INSERT' THEN op = 'CREATE'; ELSIF op = 'UPSERT' THEN @@ -1316,7 +1316,7 @@ BEGIN BEGIN --RAISE NOTICE 'op=''%'', id=''%'', feature=''%'', uuid=''%''', op, id, feature, uuid; - IF op = 'CREATE' OR op = 'PUT' THEN + IF op = 'PUT' THEN BEGIN EXECUTE insert_stmt USING feature, geo INTO r_feature; GET DIAGNOSTICS rows_affected = ROW_COUNT; @@ -1325,6 +1325,11 @@ BEGIN op = 'UPDATE'; END; END IF; + IF op = 'CREATE' THEN + EXECUTE insert_stmt USING feature, geo INTO r_feature; + GET DIAGNOSTICS rows_affected = ROW_COUNT; + r_op = 'CREATED'; + END IF; IF op = 'UPDATE' THEN IF uuid IS NOT NULL THEN EXECUTE update_atomic_stmt USING feature, geo, id, uuid INTO r_feature; diff --git a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlCursorTest.java b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlCursorTest.java index fd797c470..5c476fed8 100644 --- a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlCursorTest.java +++ b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlCursorTest.java @@ -23,16 +23,12 @@ import static org.mockito.ArgumentMatchers.anyInt; import com.here.naksha.lib.core.models.geojson.implementation.XyzFeature; -import com.here.naksha.lib.core.models.storage.FeatureCodec; -import com.here.naksha.lib.core.models.storage.FeatureCodecFactory; import com.here.naksha.lib.core.models.storage.ForwardCursor; import com.here.naksha.lib.core.models.storage.XyzFeatureCodec; import com.here.naksha.lib.core.models.storage.XyzFeatureCodecFactory; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -58,31 +54,4 @@ void testCursorCodecChange() throws SQLException { assertTrue(forwardCursor.next()); assertEquals("feature", forwardCursor.getFeature()); } - - static class StringCodec extends FeatureCodec { - - @Override - public @NotNull StringCodec decodeParts(boolean force) { - return this; - } - - @Override - public @NotNull StringCodec encodeFeature(boolean force) { - feature = json; - return this; - } - } - - static class StringCodecFactory implements FeatureCodecFactory { - - @Override - public @NotNull StringCodec newInstance() { - return new StringCodec(); - } - - @Override - public boolean isInstance(@Nullable FeatureCodec codec) { - return codec instanceof StringCodec; - } - } } diff --git a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlStorageTests.java b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlStorageTests.java index 08564a8b7..f01fb6bdd 100644 --- a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlStorageTests.java +++ b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/PsqlStorageTests.java @@ -21,6 +21,7 @@ import static com.spatial4j.core.io.GeohashUtils.encodeLatLon; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -43,10 +44,12 @@ import com.here.naksha.lib.core.models.naksha.XyzCollection; import com.here.naksha.lib.core.models.storage.EExecutedOp; import com.here.naksha.lib.core.models.storage.EWriteOp; +import com.here.naksha.lib.core.models.storage.ErrorResult; import com.here.naksha.lib.core.models.storage.ForwardCursor; import com.here.naksha.lib.core.models.storage.POp; import com.here.naksha.lib.core.models.storage.PRef; import com.here.naksha.lib.core.models.storage.ReadFeatures; +import com.here.naksha.lib.core.models.storage.Result; import com.here.naksha.lib.core.models.storage.SOp; import com.here.naksha.lib.core.models.storage.WriteXyzCollections; import com.here.naksha.lib.core.models.storage.WriteXyzFeatures; @@ -91,7 +94,7 @@ boolean partition() { static final String SINGLE_FEATURE_ID = "TheFeature"; @Test - @Order(60) + @Order(50) @EnabledIf("runTest") void singleFeatureCreate() throws NoCursor { assertNotNull(storage); @@ -127,7 +130,7 @@ void singleFeatureCreate() throws NoCursor { } @Test - @Order(61) + @Order(51) @EnabledIf("runTest") void singleFeatureRead() throws NoCursor { assertNotNull(storage); @@ -178,7 +181,7 @@ void singleFeatureRead() throws NoCursor { } @Test - @Order(62) + @Order(52) @EnabledIf("runTest") void readByBbox() throws NoCursor { assertNotNull(storage); @@ -199,7 +202,7 @@ void readByBbox() throws NoCursor { } @Test - @Order(63) + @Order(55) @EnabledIf("runTest") void singleFeatureUpdate() throws NoCursor { assertNotNull(storage); @@ -242,7 +245,7 @@ void singleFeatureUpdate() throws NoCursor { } @Test - @Order(64) + @Order(56) @EnabledIf("runTest") void singleFeatureUpdateVerify() throws NoCursor { assertNotNull(storage); @@ -292,15 +295,15 @@ void singleFeatureUpdateVerify() throws NoCursor { } @Test - @Order(65) + @Order(57) @EnabledIf("runTest") - void singleFeatureCreateWitSameId() throws NoCursor { + void singleFeaturePutWithSameId() throws NoCursor { assertNotNull(storage); assertNotNull(session); final WriteXyzFeatures request = new WriteXyzFeatures(collectionId()); final XyzFeature feature = new XyzFeature(SINGLE_FEATURE_ID); feature.setGeometry(new XyzPoint(5.0d, 6.0d, 2.0d)); - request.add(EWriteOp.CREATE, feature); + request.add(EWriteOp.PUT, feature); try (final ForwardCursor cursor = session.execute(request).getXyzFeatureCursor()) { assertTrue(cursor.next()); @@ -312,7 +315,41 @@ void singleFeatureCreateWitSameId() throws NoCursor { } @Test - @Order(66) + @Order(60) + @EnabledIf("runTest") + void testDuplicateFeatureId() throws NoCursor { + assertNotNull(storage); + assertNotNull(session); + + // given + final WriteXyzFeatures request = new WriteXyzFeatures(collectionId()); + final XyzFeature feature = new XyzFeature(SINGLE_FEATURE_ID); + feature.setGeometry(new XyzPoint(0.0d, 0.0d, 0.0d)); + request.add(EWriteOp.CREATE, feature); + + // when + final Result result = session.execute(request); + + // then + assertInstanceOf(ErrorResult.class, result); + ErrorResult errorResult = (ErrorResult) result; + assertEquals("23505", errorResult.reason.value()); + assertEquals("The feature with the id 'TheFeature' does exist already", errorResult.message); + session.commit(true); + + // make sure feature hasn't been updated (has old geometry). + final ReadFeatures readRequest = RequestHelper.readFeaturesByIdRequest(collectionId(), SINGLE_FEATURE_ID); + try (final ForwardCursor cursor = + session.execute(request).getXyzFeatureCursor()) { + assertTrue(cursor.next()); + assertEquals( + new Coordinate(5d, 6d, 2d), + cursor.getFeature().getGeometry().getJTSGeometry().getCoordinate()); + } + } + + @Test + @Order(64) @EnabledIf("runTest") void singleFeatureDelete() throws NoCursor { assertNotNull(storage); @@ -350,7 +387,7 @@ void singleFeatureDelete() throws NoCursor { } @Test - @Order(67) + @Order(65) @EnabledIf("runTest") void singleFeatureDeleteVerify() throws SQLException, NoCursor { assertNotNull(storage); @@ -420,7 +457,7 @@ void singleFeatureDeleteVerify() throws SQLException, NoCursor { } @Test - @Order(68) + @Order(66) @EnabledIf("runTest") void singleFeaturePurge() throws NoCursor { assertNotNull(storage); @@ -451,7 +488,7 @@ void singleFeaturePurge() throws NoCursor { } @Test - @Order(69) + @Order(67) @EnabledIf("runTest") void singleFeaturePurgeVerify() throws SQLException { assertNotNull(storage); diff --git a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodec.java b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodec.java new file mode 100644 index 000000000..be3fb8859 --- /dev/null +++ b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodec.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017-2023 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.psql; + +import com.here.naksha.lib.core.models.storage.FeatureCodec; +import org.jetbrains.annotations.NotNull; + +class StringCodec extends FeatureCodec { + + @Override + public @NotNull StringCodec decodeParts(boolean force) { + json = feature; + return this; + } + + @Override + public @NotNull StringCodec encodeFeature(boolean force) { + feature = json; + return this; + } +} diff --git a/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodecFactory.java b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodecFactory.java new file mode 100644 index 000000000..3bbf69bfa --- /dev/null +++ b/here-naksha-lib-psql/src/test/java/com/here/naksha/lib/psql/StringCodecFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017-2023 HERE Europe B.V. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ +package com.here.naksha.lib.psql; + +import com.here.naksha.lib.core.models.storage.FeatureCodec; +import com.here.naksha.lib.core.models.storage.FeatureCodecFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +class StringCodecFactory implements FeatureCodecFactory { + + @Override + public @NotNull StringCodec newInstance() { + return new StringCodec(); + } + + @Override + public boolean isInstance(@Nullable FeatureCodec codec) { + return codec instanceof StringCodec; + } +}