Skip to content

Commit 43e6946

Browse files
authored
Merge pull request #186 from SpineEventEngine/reproduce-timestamp-column-clearance-issue
[1.x] Allow to override the default mappings provided by `DsColumnMapping`
2 parents 4f56de1 + 634e46f commit 43e6946

File tree

14 files changed

+380
-34
lines changed

14 files changed

+380
-34
lines changed
1.7 KB
Binary file not shown.

.github/keys/spine-dev.json.gpg

-1.68 KB
Binary file not shown.

.github/workflows/build-on-ubuntu.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ jobs:
1919

2020
# This operation is specific to `gcloud-java` repository only.
2121
- name: Decrypt the credentials for the Spine-Dev service account
22-
run: ./scripts/decrypt.sh "$SPINE_DEV_KEY" ./.github/keys/spine-dev.json.gpg ./spine-dev.json
22+
run: ./config/scripts/decrypt.sh "$SPINE_DEV_CI_KEY" ./.github/keys/spine-dev-framework-ci.json.gpg ./spine-dev.json
2323
env:
24-
SPINE_DEV_KEY: ${{ secrets.SPINE_DEV_KEY }}
24+
SPINE_DEV_CI_KEY: ${{ secrets.SPINE_DEV_CI_KEY }}
2525

2626
# The OS-managed Google Cloud SDK does not provide a Datastore emulator.
2727
- name: Remove the OS-managed Google Cloud SDK

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ Gradle:
1616
```kotlin
1717
dependencies {
1818
// Datastore Storage support library.
19-
implementation("io.spine.gcloud:spine-datastore:1.8.0")
19+
implementation("io.spine.gcloud:spine-datastore:1.9.1")
2020

2121
// Pub/Sub messaging support library.
22-
implementation("io.spine.gcloud:spine-pubsub:1.8.0")
22+
implementation("io.spine.gcloud:spine-pubsub:1.9.1")
2323

2424
// Stackdriver Trace support library.
25-
implementation("io.spine.gcloud:spine-stackdriver-trace:1.8.0")
25+
implementation("io.spine.gcloud:spine-stackdriver-trace:1.9.1")
2626

2727
// Datastore-related test utilities (if needed).
28-
implementation("io.spine.gcloud:testutil-gcloud:1.8.0")
28+
implementation("io.spine.gcloud:testutil-gcloud:1.9.1")
2929
}
3030
```
3131

datastore/src/main/java/io/spine/server/storage/datastore/DsColumnMapping.java

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,16 @@
3939
import com.google.protobuf.ByteString;
4040
import com.google.protobuf.Message;
4141
import com.google.protobuf.Timestamp;
42+
import io.spine.annotation.SPI;
4243
import io.spine.core.Version;
4344
import io.spine.server.entity.storage.AbstractColumnMapping;
45+
import io.spine.server.entity.storage.ColumnMapping;
4446
import io.spine.server.entity.storage.ColumnTypeMapping;
4547
import io.spine.string.Stringifiers;
4648

49+
import java.util.HashMap;
50+
import java.util.Map;
51+
4752
import static com.google.cloud.Timestamp.ofTimeSecondsAndNanos;
4853

4954
/**
@@ -52,16 +57,57 @@
5257
* <p>All column values are stored as Datastore {@link Value}-s.
5358
*
5459
* <p>Users of the storage can extend this class to specify their own column mapping for the
55-
* selected types.
60+
* selected types. See {@link DsColumnMapping#customMapping() DsColumnMapping.customMapping()}.
61+
*
62+
* @see DatastoreStorageFactory.Builder#setColumnMapping(ColumnMapping)
5663
*/
5764
public class DsColumnMapping extends AbstractColumnMapping<Value<?>> {
5865

66+
private static final Map<Class<?>, ColumnTypeMapping<?, ? extends Value<?>>> defaults
67+
= ImmutableMap.of(Timestamp.class, ofTimestamp(),
68+
Version.class, ofVersion());
69+
70+
/**
71+
* {@inheritDoc}
72+
*
73+
* <p>Merges the default column mapping rules with those provided by SPI users.
74+
* In case there are duplicate mappings for some column type, the value provided
75+
* by SPI users is used.
76+
*
77+
* @apiNote This method is made {@code final}, as it is designed
78+
* to use {@code ImmutableMap.Builder}, which does not allow to override values.
79+
* Therefore, it is not possible for SPI users to provide their own mapping rules
80+
* for types such as {@code Timestamp}, for which this class already has
81+
* a default mapping. SPI users should override
82+
* {@link #customMapping() DsColumnMapping.customMapping()} instead.
83+
*/
5984
@Override
60-
protected void
85+
protected final void
6186
setupCustomMapping(
6287
ImmutableMap.Builder<Class<?>, ColumnTypeMapping<?, ? extends Value<?>>> builder) {
63-
builder.put(Timestamp.class, ofTimestamp());
64-
builder.put(Version.class, ofVersion());
88+
Map<Class<?>, ColumnTypeMapping<?, ? extends Value<?>>> merged = new HashMap<>();
89+
ImmutableMap<Class<?>, ColumnTypeMapping<?, ? extends Value<?>>> custom = customMapping();
90+
merged.putAll(defaults);
91+
merged.putAll(custom);
92+
builder.putAll(merged);
93+
}
94+
95+
/**
96+
* Returns the custom column mapping rules.
97+
*
98+
* <p>This method is designed for SPI users in order to be able to re-define
99+
* and-or append their custom mapping. As by default, {@code DsColumnMapping}
100+
* provides rules for {@link Timestamp} and {@link Version}, SPI users may
101+
* choose to either override these defaults by returning their own mapping for these types,
102+
* or supply even more mapping rules.
103+
*
104+
* <p>By default, this method returns an empty map.
105+
*
106+
* @return custom column mappings, per Java class of column
107+
*/
108+
@SPI
109+
protected ImmutableMap<Class<?>, ColumnTypeMapping<?, ? extends Value<?>>> customMapping() {
110+
return ImmutableMap.of();
65111
}
66112

67113
@Override
@@ -120,16 +166,26 @@ protected ColumnTypeMapping<Message, StringValue> ofMessage() {
120166
return o -> NullValue.of();
121167
}
122168

123-
@SuppressWarnings({"ProtoTimestampGetSecondsGetNano", "UnnecessaryLambda"})
124-
// This behavior is intended.
125-
private static ColumnTypeMapping<Timestamp, TimestampValue> ofTimestamp() {
169+
/**
170+
* Returns the default mapping from {@link Timestamp} to {@link TimestampValue}.
171+
*/
172+
@SuppressWarnings({
173+
"ProtoTimestampGetSecondsGetNano" /* In order to create exact value. */,
174+
"UnnecessaryLambda" /* For brevity.*/,
175+
"WeakerAccess" /* To allow access for SPI users. */})
176+
protected static ColumnTypeMapping<Timestamp, TimestampValue> ofTimestamp() {
126177
return timestamp -> TimestampValue.of(
127178
ofTimeSecondsAndNanos(timestamp.getSeconds(), timestamp.getNanos())
128179
);
129180
}
130181

131-
@SuppressWarnings("UnnecessaryLambda")
132-
private static ColumnTypeMapping<Version, LongValue> ofVersion() {
182+
/**
183+
* Returns the default mapping from {@link Version} to {@link LongValue}.
184+
*/
185+
@SuppressWarnings({
186+
"UnnecessaryLambda" /* For brevity.*/,
187+
"WeakerAccess" /* To allow access for SPI users. */})
188+
protected static ColumnTypeMapping<Version, LongValue> ofVersion() {
133189
return version -> LongValue.of(version.getNumber());
134190
}
135191
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2023, TeamDev. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Redistribution and use in source and/or binary forms, with or without
11+
* modification, must retain the above copyright notice and the following
12+
* disclaimer.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
27+
package io.spine.server.storage.datastore;
28+
29+
import com.google.cloud.datastore.Key;
30+
import com.google.protobuf.Timestamp;
31+
import io.spine.core.Version;
32+
import io.spine.server.ContextSpec;
33+
import io.spine.server.projection.ProjectionStorage;
34+
import io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.CustomMapping;
35+
import io.spine.test.datastore.College;
36+
import io.spine.test.datastore.CollegeId;
37+
import io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory;
38+
import org.junit.jupiter.api.DisplayName;
39+
import org.junit.jupiter.api.Test;
40+
41+
import static com.google.common.truth.Truth.assertThat;
42+
import static io.spine.server.storage.datastore.RecordId.ofEntityId;
43+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.COLLEGE_CLS;
44+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.COLLEGE_KIND;
45+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.clearAdmission;
46+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.futureFromNow;
47+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.newCollege;
48+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.newId;
49+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.someVersion;
50+
import static io.spine.server.storage.datastore.given.DsProjectionColumnsTestEnv.writeAndReadDeadline;
51+
import static io.spine.server.storage.datastore.given.TestEnvironment.singleTenantSpec;
52+
import static io.spine.testing.server.storage.datastore.TestDatastoreStorageFactory.local;
53+
54+
@DisplayName("When dealing with `Projection` columns, `DsProjectionStorage` should")
55+
final class DsProjectionColumnsTest {
56+
57+
private static final TestDatastoreStorageFactory datastoreFactory = local(new CustomMapping());
58+
59+
@Test
60+
@DisplayName("allow clearing the column values " +
61+
"if the column mapping used returns Datastore-specific `null` " +
62+
"for their values")
63+
void clearTimestampColumns() {
64+
ContextSpec spec = singleTenantSpec();
65+
ProjectionStorage<CollegeId> storage =
66+
datastoreFactory.createProjectionStorage(spec, COLLEGE_CLS);
67+
DatastoreWrapper datastore = datastoreFactory.createDatastoreWrapper(false);
68+
69+
CollegeId id = newId();
70+
Key key = datastore.keyFor(COLLEGE_KIND, ofEntityId(id));
71+
Version version = someVersion();
72+
73+
Timestamp admissionDeadline = futureFromNow();
74+
College college = newCollege(id, admissionDeadline);
75+
76+
com.google.cloud.Timestamp storedDeadline =
77+
writeAndReadDeadline(college, version, storage, datastore, key);
78+
assertThat(storedDeadline).isNotNull();
79+
80+
College collegeNoAdmission = clearAdmission(college);
81+
com.google.cloud.Timestamp presumablyEmptyDeadline =
82+
writeAndReadDeadline(collegeNoAdmission, version, storage, datastore, key);
83+
assertThat(presumablyEmptyDeadline)
84+
.isNull();
85+
}
86+
}

0 commit comments

Comments
 (0)