Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.scalar.db.storage.cassandra;

import com.scalar.db.api.DistributedStorageMutationAtomicityUnitIntegrationTestBase;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

public class CassandraMutationAtomicityUnitIntegrationTest
extends DistributedStorageMutationAtomicityUnitIntegrationTestBase {

@Override
protected Properties getProperties(String testName) {
return CassandraEnv.getProperties(testName);
}

@Override
protected Map<String, String> getCreationOptions() {
return Collections.singletonMap(CassandraAdmin.REPLICATION_FACTOR, "1");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.scalar.db.storage.cosmos;

import com.scalar.db.api.DistributedStorageMutationAtomicityUnitIntegrationTestBase;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Disabled;

public class CosmosMutationAtomicityUnitIntegrationTest
extends DistributedStorageMutationAtomicityUnitIntegrationTestBase {

@Override
protected Properties getProperties(String testName) {
return CosmosEnv.getProperties(testName);
}

@Override
protected Map<String, String> getCreationOptions() {
return CosmosEnv.getCreationOptions();
}

@Disabled("This test fails. It might be a bug")
@Override
public void
mutate_MutationsWithinRecordGiven_ShouldBehaveCorrectlyBaseOnMutationAtomicityUnit() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.scalar.db.storage.dynamo;

import com.scalar.db.api.DistributedStorageMutationAtomicityUnitIntegrationTestBase;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Disabled;

public class DynamoMutationAtomicityUnitIntegrationTest
extends DistributedStorageMutationAtomicityUnitIntegrationTestBase {

@Override
protected Properties getProperties(String testName) {
return DynamoEnv.getProperties(testName);
}

@Override
protected Map<String, String> getCreationOptions() {
return DynamoEnv.getCreationOptions();
}

@Disabled("Transaction request cannot include multiple operations on one item in DynamoDB")
@Override
public void
mutate_MutationsWithinRecordGiven_ShouldBehaveCorrectlyBaseOnMutationAtomicityUnit() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.scalar.db.storage.jdbc;

import com.scalar.db.api.DistributedStorageMutationAtomicityUnitIntegrationTestBase;
import java.util.Properties;

public class JdbcDatabaseMutationAtomicityUnitIntegrationTest
extends DistributedStorageMutationAtomicityUnitIntegrationTestBase {

@Override
protected Properties getProperties(String testName) {
return JdbcEnv.getProperties(testName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.scalar.db.storage.multistorage;

import static org.assertj.core.api.Assertions.assertThat;

import com.scalar.db.api.DistributedStorageMutationAtomicityUnitIntegrationTestBase;
import com.scalar.db.api.Put;
import com.scalar.db.config.DatabaseConfig;
import com.scalar.db.exception.storage.ExecutionException;
import com.scalar.db.io.Key;
import java.util.Arrays;
import java.util.Properties;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MultiStorageMutationAtomicityUnitIntegrationTest
extends DistributedStorageMutationAtomicityUnitIntegrationTestBase {

@Override
public Properties getProperties(String testName) {
Properties properties = new Properties();
properties.setProperty(DatabaseConfig.STORAGE, "multi-storage");

// Define storages, jdbc and cassandra
properties.setProperty(MultiStorageConfig.STORAGES, "jdbc,cassandra");

Properties propertiesForJdbc = MultiStorageEnv.getPropertiesForJdbc(testName);
for (String propertyName : propertiesForJdbc.stringPropertyNames()) {
properties.setProperty(
MultiStorageConfig.STORAGES
+ ".jdbc."
+ propertyName.substring(DatabaseConfig.PREFIX.length()),
propertiesForJdbc.getProperty(propertyName));
}

Properties propertiesForCassandra = MultiStorageEnv.getPropertiesForCassandra(testName);
for (String propertyName : propertiesForCassandra.stringPropertyNames()) {
properties.setProperty(
MultiStorageConfig.STORAGES
+ ".cassandra."
+ propertyName.substring(DatabaseConfig.PREFIX.length()),
propertiesForCassandra.getProperty(propertyName));
}

// Define namespace mappings
properties.setProperty(
MultiStorageConfig.NAMESPACE_MAPPING,
NAMESPACE1 + ":jdbc," + NAMESPACE2 + ":jdbc," + NAMESPACE3 + ":cassandra");

// The default storage is jdbc
properties.setProperty(MultiStorageConfig.DEFAULT_STORAGE, "jdbc");

// Add testName as a metadata schema suffix
properties.setProperty(
DatabaseConfig.SYSTEM_NAMESPACE_NAME,
DatabaseConfig.DEFAULT_SYSTEM_NAMESPACE_NAME + "_" + testName);

return properties;
}

@Test
public void mutate_MutationsAcrossStorageGiven_ShouldBehaveCorrectlyBaseOnMutationAtomicityUnit()
throws ExecutionException {
// Arrange
Put put1 =
Put.newBuilder()
.namespace(namespace1)
.table(TABLE1)
.partitionKey(Key.ofInt(COL_NAME1, 0))
.clusteringKey(Key.ofInt(COL_NAME2, 1))
.intValue(COL_NAME3, 1)
.build();
Put put2 =
Put.newBuilder()
.namespace(namespace3)
.table(TABLE1)
.partitionKey(Key.ofInt(COL_NAME1, 1))
.clusteringKey(Key.ofInt(COL_NAME2, 2))
.intValue(COL_NAME3, 2)
.build();

// Act
Exception exception =
Assertions.catchException(() -> storage.mutate(Arrays.asList(put1, put2)));

// Assert
assertThat(exception).isInstanceOf(IllegalArgumentException.class);
}
}
8 changes: 8 additions & 0 deletions core/src/main/java/com/scalar/db/api/DistributedStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ public interface DistributedStorage extends AutoCloseable {
/**
* Mutates entries of the underlying storage with the specified list of {@link Mutation} commands.
*
* <p>Note that this method only supports mutations within the atomicity unit specified by {@link
* StorageInfo#getMutationAtomicityUnit()}. For example, if the atomicity unit of the storage is
* {@link StorageInfo.MutationAtomicityUnit#PARTITION}, the mutations must occur within the same
* partition. Also note that the maximum number of mutations that can be performed atomically is
* defined by {@link StorageInfo#getMaxAtomicMutationsCount()}.
*
* <p>To retrieve storage information, use {@link DistributedStorageAdmin#getStorageInfo(String)}.
*
* @param mutations a list of {@code Mutation} commands
* @throws ExecutionException if the operation fails
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ TableMetadata getImportTableMetadata(
void addRawColumnToTable(String namespace, String table, String columnName, DataType columnType)
throws ExecutionException;

/**
* Returns the storage information.
*
* @param namespace the namespace to get the storage information for
* @return the storage information
* @throws ExecutionException if the operation fails
*/
StorageInfo getStorageInfo(String namespace) throws ExecutionException;
Comment on lines +91 to +98
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get the StorageInfo instance of the storage through this method.


/** Closes connections to the storage. */
@Override
void close();
Expand Down
62 changes: 62 additions & 0 deletions core/src/main/java/com/scalar/db/api/StorageInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.scalar.db.api;

public interface StorageInfo {
/**
* Returns the storage name.
*
* @return the storage name
*/
String getStorageName();

/**
* Returns the mutation atomicity unit of the storage.
*
* @return the mutation atomicity unit of the storage
*/
MutationAtomicityUnit getMutationAtomicityUnit();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MutationAtomicityUnit getMutationAtomicityUnit();
MutationAtomicityGranularity getMutationAtomicityGranularity();

After some thought, I feel it's probably more proper to call it granularity, although it's a bit long.
What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini says MutationAtomicityUnit is more appropriate, but I'm not sure.

In the context of transactions, when referring to the scope within which updates can be performed atomically, **"MutationAtomicityUnit"** is the better choice.

Here's why:

* **Unit**: "Unit" directly implies a distinct, defined **scope** or **entity**. The example you provided with `RECORD`, `PARTITION`, `TABLE`, etc., clearly defines the atomic "unit" of operation. It naturally conveys the idea that mutations are performed atomically for *each* of these specified units.
* **Granularity**: "Granularity" refers to the **fineness or coarseness** of something. While related, it describes the *degree* of detail or division rather than the specific scope itself. For instance, you might discuss whether the atomicity has "fine-grained" or "coarse-grained" granularity. When you want to name the actual *thing* that is atomic, "Unit" is more precise and less ambiguous.

Therefore, "MutationAtomicityUnit" more accurately and clearly communicates the intended meaning of the atomic **scope** for mutations.

What do you think? 🙇

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also asked Gemini. It suggests Granularity.... I think it depends on how we ask.

Question:
I'm considering how to name a method, and I would appreciate your advice.

We provide a transaction manager for various kinds of databases. The transaction manager pushes down write operations into the underlying databases based on the size that the databases can atomically write.

To make each database provide the size information, I created a StorageInfo interface, created a class that implements the interface for each database, and created a method that returns the size information.
The size information is an Enum class that has the following values: STORAGE, NAMESPACE, TABLE, PARTITION, and RECORD.

I have two candidate names for the method.
One is getMutationAtomicityUnit() and the other is getMutationAtomicityGranularity().
Which do you think is better?
Do you have any other better ideas?

Answer:
I think getMutationAtomicityGranularity() is the better choice.

Here’s a breakdown of why, along with some alternative ideas.

Comparison of Your Candidates
Both of your proposed names are very good because they are descriptive and unambiguous. However, getMutationAtomicityGranularity() has a slight edge.

getMutationAtomicityGranularity(): Slightly better. 👍
In database and systems design, granularity is the standard term for referring to the scale or level at which an operation occurs (e.g., fine-grained vs. coarse-grained). Since your Enum defines a scale from RECORD (fine-grained) to STORAGE (coarse-grained), this name is a perfect technical fit.

getMutationAtomicityUnit(): Also very good.
The word unit is also accurate. It correctly describes that the method returns the single, indivisible "chunk" for atomic writes. It's simple and clear. The only minor drawback is that "granularity" is more idiomatic in this specific technical domain.

Other Ideas
If you're open to other suggestions, here are a couple of alternatives that aim for simplicity while remaining clear.

getAtomicityLevel()
This is a strong contender. It's shorter and replaces the more technical "Granularity" with the simpler "Level," which is easily understood by any developer. The context of your transaction manager implies that the atomicity relates to mutations.

getAtomicWriteScope()
Using Scope is another excellent option. It clearly communicates that the Enum return value defines the boundary or scope (e.g., the scope of a table, the scope of a partition) within which writes are atomic.

Final Recommendation
Go with getMutationAtomicityGranularity(). It's the most precise and idiomatic term for your use case.

If you feel it's too much jargon, getAtomicityLevel() is a fantastic, simpler alternative that conveys the same meaning effectively.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share the question as well?

Anyway, Unit seems also correct, so I'm fine with Unit, though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question was as follows:

In the context of transactions, which term is more appropriate to express the range in which updates can be performed atomically: MutationAtomicityUnit or MutationAtomicityGranularity?

Below is an example using MutationAtomicityUnit:

  /**
   * The mutation atomicity unit of the storage.
   *
   * <p>This enum defines the atomicity unit for mutations in the storage. It determines the scope
   * of atomicity for mutations such as put and delete.
   */
  enum MutationAtomicityUnit {
    /**
     * The atomicity unit is at the record level, meaning that mutations are performed atomically
     * for each record.
     */
    RECORD,

    /**
     * The atomicity unit is at the partition level, meaning that mutations are performed atomically
     * for each partition.
     */
    PARTITION,

    /**
     * The atomicity unit is at the table level, meaning that mutations are performed atomically for
     * each table.
     */
    TABLE,

    /**
     * The atomicity unit is at the namespace level, meaning that mutations are performed atomically
     * for each namespace.
     */
    NAMESPACE,

    /**
     * The atomicity unit is at the storage level, meaning that mutations are performed atomically
     * for the entire storage.
     */
    STORAGE
  }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the discussion, since Unit is also correct and simpler, let's go with as is.


/**
* Returns the maximum number of mutations that can be performed atomically in the storage.
*
* @return the maximum number of mutations that can be performed atomically in the storage
*/
int getMaxAtomicMutationsCount();

/**
* The mutation atomicity unit of the storage.
*
* <p>This enum defines the atomicity unit for mutations in the storage. It determines the scope
* of atomicity for mutations such as put and delete.
*/
enum MutationAtomicityUnit {
/**
* The atomicity unit is at the record level, meaning that mutations are performed atomically
* for each record.
*/
RECORD,

/**
* The atomicity unit is at the partition level, meaning that mutations are performed atomically
* for each partition.
*/
PARTITION,

/**
* The atomicity unit is at the table level, meaning that mutations are performed atomically for
* each table.
*/
TABLE,

/**
* The atomicity unit is at the namespace level, meaning that mutations are performed atomically
* for each namespace.
*/
NAMESPACE,

/**
* The atomicity unit is at the storage level, meaning that mutations are performed atomically
* for the entire storage.
*/
STORAGE
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.scalar.db.common;

import com.scalar.db.api.DistributedStorageAdmin;
import com.scalar.db.api.StorageInfo;
import com.scalar.db.api.TableMetadata;
import com.scalar.db.common.error.CoreError;
import com.scalar.db.config.DatabaseConfig;
Expand Down Expand Up @@ -373,6 +374,20 @@ public void upgrade(Map<String, String> options) throws ExecutionException {
}
}

@Override
public StorageInfo getStorageInfo(String namespace) throws ExecutionException {
if (!namespaceExists(namespace)) {
throw new IllegalArgumentException(CoreError.NAMESPACE_NOT_FOUND.buildMessage(namespace));
}

try {
return admin.getStorageInfo(namespace);
} catch (ExecutionException e) {
throw new ExecutionException(
CoreError.GETTING_STORAGE_INFO_FAILED.buildMessage(namespace), e);
}
}

@Override
public void close() {
admin.close();
Expand Down
66 changes: 66 additions & 0 deletions core/src/main/java/com/scalar/db/common/StorageInfoImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.scalar.db.common;

import com.google.common.base.MoreObjects;
import com.scalar.db.api.StorageInfo;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;

@Immutable
public class StorageInfoImpl implements StorageInfo {

private final String storageName;
private final MutationAtomicityUnit mutationAtomicityUnit;
private final int maxAtomicMutationsCount;

public StorageInfoImpl(
String storageName,
MutationAtomicityUnit mutationAtomicityUnit,
int maxAtomicMutationsCount) {
this.storageName = storageName;
this.mutationAtomicityUnit = mutationAtomicityUnit;
this.maxAtomicMutationsCount = maxAtomicMutationsCount;
}

@Override
public String getStorageName() {
return storageName;
}

@Override
public MutationAtomicityUnit getMutationAtomicityUnit() {
return mutationAtomicityUnit;
}

@Override
public int getMaxAtomicMutationsCount() {
return maxAtomicMutationsCount;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof StorageInfoImpl)) {
return false;
}
StorageInfoImpl that = (StorageInfoImpl) o;
return getMaxAtomicMutationsCount() == that.getMaxAtomicMutationsCount()
&& Objects.equals(getStorageName(), that.getStorageName())
&& getMutationAtomicityUnit() == that.getMutationAtomicityUnit();
}

@Override
public int hashCode() {
return Objects.hash(getStorageName(), getMutationAtomicityUnit(), getMaxAtomicMutationsCount());
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("storageName", storageName)
.add("mutationAtomicityUnit", mutationAtomicityUnit)
.add("maxAtomicMutationsCount", maxAtomicMutationsCount)
.toString();
}
}
Loading