Repository: scalar-labs/scalardl
Pull request #141: Add validate-ledger for assets created by table-based generic contracts
Author: login: jnmt
Labels: name: enhancement
description: New feature or request
Created: 2025-05-14T07:03:48Z | Merged: 2025-05-28T10:07:26Z

## Description

This PR adds the validate-ledger command and APIs for assets created by table-based generic contracts.

## Related issues and/or PRs

N/A

## Changes made

See https://github.com/scalar-labs/scalardl/pull/141#issuecomment-2878956175.

## Checklist

- [x] I have commented my code, particularly in hard-to-understand areas.
- [ ] I have updated the documentation to reflect the changes.
- [x] I have considered whether similar issues could occur in other products, components, or modules if this PR is for bug fixes.
- [x] Any remaining open issues linked to this PR are documented and up-to-date (Jira, GitHub, etc.).
- [x] Tests (unit, integration, etc.) have been added for the changes.
- [x] My changes generate no new warnings.
- [x] Any dependent changes in other PRs have been merged and published.

## Additional notes (optional)

N/A

## Release notes

Added validate-ledger for assets created by table-based generic contracts.


Diff:
diff --git a/client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java b/client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
index d8f84405..1cefc80e 100644
--- a/client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
+++ b/client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
@@ -3,17 +3,26 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.scalar.dl.genericcontracts.collection.Constants.COLLECTION_ID_PREFIX;
 import static com.scalar.dl.genericcontracts.object.Constants.OBJECT_ID_PREFIX;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.ASSET_ID_SEPARATOR;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_INDEX;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_RECORD;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_TABLE;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ValueNode;
+import com.google.common.collect.ImmutableList;
 import com.scalar.dl.client.error.ClientError;
 import com.scalar.dl.client.exception.ClientException;
 import com.scalar.dl.genericcontracts.AssetType;
 import com.scalar.dl.ledger.model.ContractExecutionResult;
 import com.scalar.dl.ledger.model.LedgerValidationResult;
+import com.scalar.dl.ledger.util.JacksonSerDe;
 import java.util.List;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.json.JsonObject;
+import javax.json.JsonValue;
 
 /**
  * A thread-safe client that interacts with Ledger and Auditor components to register certificates,
@@ -44,6 +53,7 @@
  */
 @Immutable
 public class GenericContractClientService {
+  private static final JacksonSerDe jacksonSerDe = new JacksonSerDe(new ObjectMapper());
   private final ClientService clientService;
 
   /**
@@ -434,6 +444,301 @@ public LedgerValidationResult validateLedger(
     return clientService.validateLedger(buildAssetId(type, keys), startAge, endAge);
   }
 
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId) {
+    return clientService.validateLedger(buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)));
+  }
+
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)));
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName) {
+    return clientService.validateLedger(buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)));
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(String tableName, String columnName, String value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(
+                tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, String value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, JsonValue value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.INDEX, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, JsonValue value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.INDEX, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, ValueNode value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.INDEX, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, ValueNode value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.INDEX, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value in a JSON format
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, String value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.INDEX,
+            ImmutableList.of(
+                tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))));
+  }
+
+  /**
+   * Validates the specified index record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value in a JSON format
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexRecord(
+      String tableName, String columnName, String value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.INDEX,
+            ImmutableList.of(tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))),
+        startAge,
+        endAge);
+  }
+
   /**
    * Validates the specified asset in the ledger with the specified serialized byte array of a
    * {@code LedgerValidationRequest}.
@@ -454,9 +759,32 @@ private String buildAssetId(AssetType type, List<String> keys) {
       case COLLECTION:
         checkArgument(keys.size() == 1, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
         return COLLECTION_ID_PREFIX + keys.get(0);
+      case TABLE:
+        checkArgument(keys.size() == 1, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_TABLE + keys.get(0);
+      case RECORD:
+        checkArgument(keys.size() == 3, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_RECORD + String.join(ASSET_ID_SEPARATOR, keys);
+      case INDEX:
+        checkArgument(keys.size() == 3, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_INDEX + String.join(ASSET_ID_SEPARATOR, keys);
       default:
         throw new IllegalArgumentException(
             ClientError.SERVICE_WRONG_ASSET_TYPE_SPECIFIED.buildMessage());
     }
   }
+
+  private String toStringFrom(JsonValue value) {
+    return toStringFrom(jacksonSerDe.deserialize(value.toString()));
+  }
+
+  private String toStringFrom(JsonNode value) {
+    if (value.canConvertToExactIntegral()) {
+      return value.bigIntegerValue().toString();
+    } else if (value.isNumber()) {
+      return String.valueOf(value.doubleValue());
+    } else {
+      return value.asText();
+    }
+  }
 }
diff --git a/client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java b/client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java
index d3d52af1..5605da07 100644
--- a/client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java
+++ b/client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java
@@ -6,11 +6,8 @@
 import com.scalar.dl.client.exception.ClientException;
 import com.scalar.dl.client.service.ClientServiceFactory;
 import com.scalar.dl.client.service.GenericContractClientService;
-import com.scalar.dl.genericcontracts.AssetType;
 import com.scalar.dl.ledger.model.LedgerValidationResult;
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.Callable;
 import picocli.CommandLine;
 import picocli.CommandLine.ArgGroup;
@@ -43,6 +40,49 @@ static class Arguments {
         paramLabel = "COLLECTION_ID",
         description = "The ID of a collection created by the collection.Create contract.")
     String collectionId;
+
+    @ArgGroup(exclusive = false, multiplicity = "1")
+    TableArguments table;
+  }
+
+  static class TableArguments {
+    @CommandLine.Option(
+        names = {"--table-name"},
+        required = true,
+        paramLabel = "TABLE_NAME",
+        description = "The name of a table created by table-oriented generic contracts.")
+    String tableName;
+
+    @ArgGroup(exclusive = false, multiplicity = "0..1")
+    private PrimaryOrIndexKey key;
+  }
+
+  static class PrimaryOrIndexKey {
+    @ArgGroup(exclusive = true, multiplicity = "1")
+    private ColumnName name;
+
+    @CommandLine.Option(
+        names = {"--column-value"},
+        required = true,
+        paramLabel = "COLUMN_VALUE",
+        description = "The column value of primary key or index key as JSON string.")
+    String value;
+  }
+
+  static class ColumnName {
+    @CommandLine.Option(
+        names = {"--primary-key-column-name"},
+        paramLabel = "PRIMARY_KEY_COLUMN_NAME",
+        description =
+            "The primary key column name of a record created by table-oriented generic contracts.")
+    String primary;
+
+    @CommandLine.Option(
+        names = {"--index-key-column-name"},
+        paramLabel = "INDEX_KEY_COLUMN_NAME",
+        description =
+            "The index key column name of a record created by table-oriented generic contracts.")
+    String index;
   }
 
   @ArgGroup(exclusive = true, multiplicity = "1")
@@ -65,16 +105,28 @@ Integer call(ClientServiceFactory factory) throws Exception {
   @VisibleForTesting
   Integer call(ClientServiceFactory factory, GenericContractClientService service) {
     try {
-      AssetType type;
-      List<String> keys = new ArrayList<>();
+      LedgerValidationResult result;
       if (arguments.objectId != null) {
-        type = AssetType.OBJECT;
-        keys.add(arguments.objectId);
+        result = service.validateObject(arguments.objectId, startAge, endAge);
+      } else if (arguments.collectionId != null) {
+        result = service.validateCollection(arguments.collectionId, startAge, endAge);
       } else {
-        type = AssetType.COLLECTION;
-        keys.add(arguments.collectionId);
+        if (arguments.table.key == null) {
+          result = service.validateTableSchema(arguments.table.tableName, startAge, endAge);
+        } else {
+          String tableName = arguments.table.tableName;
+          String value = arguments.table.key.value;
+          if (arguments.table.key.name.primary != null) {
+            result =
+                service.validateRecord(
+                    tableName, arguments.table.key.name.primary, value, startAge, endAge);
+          } else {
+            result =
+                service.validateIndexRecord(
+                    tableName, arguments.table.key.name.index, value, startAge, endAge);
+          }
+        }
       }
-      LedgerValidationResult result = service.validateLedger(type, keys, startAge, endAge);
       Common.printJson(Common.getValidationResult(result));
       return 0;
     } catch (IllegalArgumentException e) {
diff --git a/client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java b/client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java
index 00b07f2e..9ac37320 100644
--- a/client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java
+++ b/client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java
@@ -2,15 +2,25 @@
 
 import static com.scalar.dl.genericcontracts.collection.Constants.COLLECTION_ID_PREFIX;
 import static com.scalar.dl.genericcontracts.object.Constants.OBJECT_ID_PREFIX;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.ASSET_ID_SEPARATOR;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_INDEX;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_RECORD;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_TABLE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.DecimalNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.TextNode;
 import com.google.common.collect.ImmutableList;
 import com.scalar.dl.genericcontracts.AssetType;
+import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
+import javax.json.Json;
 import javax.json.JsonObject;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -21,6 +31,8 @@ public class GenericContractClientServiceTest {
   private static final String ANY_ID = "id";
   private static final String ANY_NAME = "name";
   private static final String ANY_FILE_PATH = "path/to/file";
+  private static final String ANY_TABLE = "tbl";
+  private static final String ANY_COLUMN = "col";
   private static final int ANY_START_AGE = 0;
   private static final int ANY_END_AGE = 5;
   private static final JsonObject ANY_JSON_OBJECT = mock(JsonObject.class);
@@ -185,4 +197,343 @@ public void executeContract_ShouldCallClientServiceExecuteContract() {
     // Assert
     assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class);
   }
+
+  @Test
+  public void validateObject_ShouldCallClientServiceValidateLedgerWithObjectPrefix() {
+    // Arrange Act
+    service.validateObject(ANY_ID);
+    service.validateObject(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateCollection_ShouldCallClientServiceValidateLedgerWithCollectionPrefix() {
+    // Arrange Act
+    service.validateCollection(ANY_ID);
+    service.validateCollection(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateTableSchema_ShouldCallClientServiceValidateLedgerWithTablePrefix() {
+    // Arrange Act
+    service.validateTableSchema(ANY_TABLE);
+    service.validateTableSchema(ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE);
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void
+      validateRecord_JsonValueGiven_ShouldCallClientServiceValidateLedgerWithRecordPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + bigDecimalValue.doubleValue();
+
+    // Act
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(stringValue));
+    service.validateRecord(
+        ANY_TABLE, ANY_COLUMN, Json.createValue(stringValue), ANY_START_AGE, ANY_END_AGE);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(intValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(doubleValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(doubleIntegerValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(bigDecimalValue));
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
+
+  @Test
+  public void
+      validateRecord_ValueNodeGiven_ShouldCallClientServiceValidateLedgerWithRecordPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + bigDecimalValue.doubleValue();
+
+    // Act
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, TextNode.valueOf(stringValue));
+    service.validateRecord(
+        ANY_TABLE, ANY_COLUMN, TextNode.valueOf(stringValue), ANY_START_AGE, ANY_END_AGE);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, IntNode.valueOf(intValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, DoubleNode.valueOf(doubleValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, DoubleNode.valueOf(doubleIntegerValue));
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, DecimalNode.valueOf(bigDecimalValue));
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
+
+  @Test
+  public void
+      validateRecord_StringValueGiven_ShouldCallClientServiceValidateLedgerWithRecordPrefix() {
+    // Arrange
+    String stringValue = "\"val\"";
+    String expectedForStringValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "val";
+    String intValue = "1";
+    String expectedForIntValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    String doubleValue = "1.23";
+    String expectedForDoubleValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    String doubleIntegerValue = "2.0";
+    String expectedForDoubleIntegerValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    String bigDecimalValue = "1.2345678901234567890123456789";
+    String expectedForBigDecimalValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + new BigDecimal(bigDecimalValue).doubleValue();
+
+    // Act
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, stringValue);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, stringValue, ANY_START_AGE, ANY_END_AGE);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, intValue);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, doubleValue);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, doubleIntegerValue);
+    service.validateRecord(ANY_TABLE, ANY_COLUMN, bigDecimalValue);
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
+
+  @Test
+  public void
+      validateIndexEntry_JsonValueGiven_ShouldCallClientServiceValidateLedgerWithIndexPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + bigDecimalValue.doubleValue();
+
+    // Act
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(stringValue));
+    service.validateIndexRecord(
+        ANY_TABLE, ANY_COLUMN, Json.createValue(stringValue), ANY_START_AGE, ANY_END_AGE);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(intValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(doubleValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(doubleIntegerValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, Json.createValue(bigDecimalValue));
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
+
+  @Test
+  public void
+      validateIndexEntry_ValueNodeGiven_ShouldCallClientServiceValidateLedgerWithIndexPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + bigDecimalValue.doubleValue();
+
+    // Act
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, TextNode.valueOf(stringValue));
+    service.validateIndexRecord(
+        ANY_TABLE, ANY_COLUMN, TextNode.valueOf(stringValue), ANY_START_AGE, ANY_END_AGE);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, IntNode.valueOf(intValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, DoubleNode.valueOf(doubleValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, DoubleNode.valueOf(doubleIntegerValue));
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, DecimalNode.valueOf(bigDecimalValue));
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
+
+  @Test
+  public void
+      validateIndexEntry_StringValueGiven_ShouldCallClientServiceValidateLedgerWithIndexPrefix() {
+    // Arrange
+    String stringValue = "\"val\"";
+    String expectedForStringValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "val";
+    String intValue = "1";
+    String expectedForIntValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    String doubleValue = "1.23";
+    String expectedForDoubleValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    String doubleIntegerValue = "2.0";
+    String expectedForDoubleIntegerValue =
+        PREFIX_INDEX + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    String bigDecimalValue = "1.2345678901234567890123456789";
+    String expectedForBigDecimalValue =
+        PREFIX_INDEX
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + new BigDecimal(bigDecimalValue).doubleValue();
+
+    // Act
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, stringValue);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, stringValue, ANY_START_AGE, ANY_END_AGE);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, intValue);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, doubleValue);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, doubleIntegerValue);
+    service.validateIndexRecord(ANY_TABLE, ANY_COLUMN, bigDecimalValue);
+
+    // Assert
+    verify(clientService).validateLedger(expectedForStringValue);
+    verify(clientService).validateLedger(expectedForStringValue, ANY_START_AGE, ANY_END_AGE);
+    verify(clientService).validateLedger(expectedForIntValue);
+    verify(clientService).validateLedger(expectedForDoubleValue);
+    verify(clientService).validateLedger(expectedForDoubleIntegerValue);
+    verify(clientService).validateLedger(expectedForBigDecimalValue);
+  }
 }
diff --git a/client/src/test/java/com/scalar/dl/client/tool/GenericContractLedgerValidationTest.java b/client/src/test/java/com/scalar/dl/client/tool/GenericContractLedgerValidationTest.java
index c2be4824..bce9f366 100644
--- a/client/src/test/java/com/scalar/dl/client/tool/GenericContractLedgerValidationTest.java
+++ b/client/src/test/java/com/scalar/dl/client/tool/GenericContractLedgerValidationTest.java
@@ -1,20 +1,20 @@
 package com.scalar.dl.client.tool;
 
 import static com.scalar.dl.client.tool.CommandLineTestUtils.createDefaultClientPropertiesFile;
+import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
 import static org.mockito.Mockito.*;
 
-import com.google.common.collect.ImmutableList;
 import com.scalar.dl.client.config.ClientConfig;
 import com.scalar.dl.client.config.GatewayClientConfig;
 import com.scalar.dl.client.exception.ClientException;
 import com.scalar.dl.client.service.ClientServiceFactory;
 import com.scalar.dl.client.service.GenericContractClientService;
-import com.scalar.dl.genericcontracts.AssetType;
 import com.scalar.dl.ledger.model.LedgerValidationResult;
 import com.scalar.dl.ledger.service.StatusCode;
 import java.io.File;
 import java.nio.file.Path;
+import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -23,6 +23,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import picocli.CommandLine;
+import picocli.CommandLine.MissingParameterException;
 
 public class GenericContractLedgerValidationTest {
   @Mock private ClientServiceFactory factory;
@@ -49,18 +50,16 @@ void withoutAges() {
             new String[] {
               "--properties=PROPERTIES_FILE", "--object-id=OBJECT_ID",
             };
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.OBJECT, keys, 0, Integer.MAX_VALUE))
-            .thenReturn(result);
+        when(clientService.validateObject("OBJECT_ID", 0, Integer.MAX_VALUE)).thenReturn(result);
 
         // Act
         int exitCode = command.call(factory, clientService);
 
         // Assert
         assertThat(exitCode).isEqualTo(0);
-        verify(clientService).validateLedger(AssetType.OBJECT, keys, 0, Integer.MAX_VALUE);
+        verify(clientService).validateObject("OBJECT_ID", 0, Integer.MAX_VALUE);
       }
 
       @Test
@@ -74,17 +73,16 @@ void withAges() {
               "--start-age=10",
               "--end-age=20",
             };
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.OBJECT, keys, 10, 20)).thenReturn(result);
+        when(clientService.validateObject("OBJECT_ID", 10, 20)).thenReturn(result);
 
         // Act
         int exitCode = command.call(factory, clientService);
 
         // Assert
         assertThat(exitCode).isEqualTo(0);
-        verify(clientService).validateLedger(AssetType.OBJECT, keys, 10, 20);
+        verify(clientService).validateObject("OBJECT_ID", 10, 20);
       }
     }
 
@@ -99,10 +97,9 @@ void withoutAges() {
             new String[] {
               "--properties=PROPERTIES_FILE", "--collection-id=COLLECTION_ID",
             };
-        ImmutableList<String> keys = ImmutableList.of("COLLECTION_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.COLLECTION, keys, 0, Integer.MAX_VALUE))
+        when(clientService.validateCollection("COLLECTION_ID", 0, Integer.MAX_VALUE))
             .thenReturn(result);
 
         // Act
@@ -110,7 +107,7 @@ void withoutAges() {
 
         // Assert
         assertThat(exitCode).isEqualTo(0);
-        verify(clientService).validateLedger(AssetType.COLLECTION, keys, 0, Integer.MAX_VALUE);
+        verify(clientService).validateCollection("COLLECTION_ID", 0, Integer.MAX_VALUE);
       }
 
       @Test
@@ -124,17 +121,143 @@ void withAges() {
               "--start-age=10",
               "--end-age=20",
             };
-        ImmutableList<String> keys = ImmutableList.of("COLLECTION_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.COLLECTION, keys, 10, 20)).thenReturn(result);
+        when(clientService.validateCollection("COLLECTION_ID", 10, 20)).thenReturn(result);
+
+        // Act
+        int exitCode = command.call(factory, clientService);
+
+        // Assert
+        assertThat(exitCode).isEqualTo(0);
+        verify(clientService).validateCollection("COLLECTION_ID", 10, 20);
+      }
+    }
+
+    @Nested
+    @DisplayName("with --table-name only")
+    class withTableOnly {
+      @Test
+      @DisplayName("with ages")
+      void withAges() {
+        // Arrange
+        String[] args =
+            new String[] {
+              "--properties=PROPERTIES_FILE",
+              "--table-name=TABLE_NAME",
+              "--start-age=10",
+              "--end-age=20",
+            };
+        GenericContractLedgerValidation command = parseArgs(args);
+        LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
+        when(clientService.validateTableSchema("TABLE_NAME", 10, 20)).thenReturn(result);
 
         // Act
         int exitCode = command.call(factory, clientService);
 
         // Assert
         assertThat(exitCode).isEqualTo(0);
-        verify(clientService).validateLedger(AssetType.COLLECTION, keys, 10, 20);
+        verify(clientService).validateTableSchema("TABLE_NAME", 10, 20);
+      }
+    }
+
+    @Nested
+    @DisplayName("with --primary-key-column-name")
+    class withPrimaryKeyColumnName {
+      @Test
+      @DisplayName("with value")
+      void withTextValue() {
+        // Arrange
+        String[] args =
+            new String[] {
+              "--properties=PROPERTIES_FILE",
+              "--table-name=TABLE_NAME",
+              "--primary-key-column-name=COLUMN_NAME",
+              "--column-value=VALUE",
+              "--start-age=10",
+              "--end-age=20",
+            };
+        GenericContractLedgerValidation command = parseArgs(args);
+        LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
+        when(clientService.validateRecord("TABLE_NAME", "COLUMN_NAME", "VALUE", 10, 20))
+            .thenReturn(result);
+
+        // Act
+        int exitCode = command.call(factory, clientService);
+
+        // Assert
+        assertThat(exitCode).isEqualTo(0);
+        verify(clientService).validateRecord("TABLE_NAME", "COLUMN_NAME", "VALUE", 10, 20);
+      }
+
+      @Test
+      @DisplayName("without value")
+      void throwsMissingParameterException() {
+        // Arrange
+        String[] args =
+            new String[] {
+              "--properties=PROPERTIES_FILE",
+              "--table-name=TABLE_NAME",
+              "--primary-key-column-name=COLUMN_NAME",
+              "--start-age=10",
+              "--end-age=20",
+            };
+
+        // Act
+        Throwable thrown = catchThrowable(() -> parseArgs(args));
+
+        // Assert
+        Assertions.assertThat(thrown).isExactlyInstanceOf(MissingParameterException.class);
+      }
+    }
+
+    @Nested
+    @DisplayName("with --index-key-column-name")
+    class withIndexKeyColumnName {
+      @Test
+      @DisplayName("with value")
+      void withTextValue() {
+        // Arrange
+        String[] args =
+            new String[] {
+              "--properties=PROPERTIES_FILE",
+              "--table-name=TABLE_NAME",
+              "--index-key-column-name=COLUMN_NAME",
+              "--column-value=VALUE",
+              "--start-age=10",
+              "--end-age=20",
+            };
+        GenericContractLedgerValidation command = parseArgs(args);
+        LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
+        when(clientService.validateIndexRecord("TABLE_NAME", "COLUMN_NAME", "VALUE", 10, 20))
+            .thenReturn(result);
+
+        // Act
+        int exitCode = command.call(factory, clientService);
+
+        // Assert
+        assertThat(exitCode).isEqualTo(0);
+        verify(clientService).validateIndexRecord("TABLE_NAME", "COLUMN_NAME", "VALUE", 10, 20);
+      }
+
+      @Test
+      @DisplayName("without value")
+      void throwsMissingParameterException() {
+        // Arrange
+        String[] args =
+            new String[] {
+              "--properties=PROPERTIES_FILE",
+              "--table-name=TABLE_NAME",
+              "--index-key-column-name=COLUMN_NAME",
+              "--start-age=10",
+              "--end-age=20",
+            };
+
+        // Act
+        Throwable thrown = catchThrowable(() -> parseArgs(args));
+
+        // Assert
+        Assertions.assertThat(thrown).isExactlyInstanceOf(MissingParameterException.class);
       }
     }
 
@@ -157,10 +280,9 @@ public void createClientServiceWithGatewayClientConfig(@TempDir Path tempDir)
               "--end-age=10",
             };
 
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.OBJECT, keys, 0, 10)).thenReturn(result);
+        when(clientService.validateObject("OBJECT_ID", 0, 10)).thenReturn(result);
         when(factory.createForGenericContract(any(GatewayClientConfig.class)))
             .thenReturn(clientService);
 
@@ -187,10 +309,9 @@ public void createClientServiceWithClientConfig(@TempDir Path tempDir) throws Ex
               propertiesOption, "--object-id=OBJECT_ID", "--start-age=0", "--end-age=10",
             };
 
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         LedgerValidationResult result = new LedgerValidationResult(StatusCode.OK, null, null);
-        when(clientService.validateLedger(AssetType.OBJECT, keys, 0, 10)).thenReturn(result);
+        when(clientService.validateObject("OBJECT_ID", 0, 10)).thenReturn(result);
         when(factory.createForGenericContract(any(ClientConfig.class))).thenReturn(clientService);
 
         // Act
@@ -217,18 +338,17 @@ void throwsIllegalArgumentExceptionThenReturns1() {
               "--end-age=0",
             };
 
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
         doThrow(IllegalArgumentException.class)
             .when(clientService)
-            .validateLedger(AssetType.OBJECT, keys, 10, 0);
+            .validateObject("OBJECT_ID", 10, 0);
 
         // Act
         int exitCode = command.call(factory, clientService);
 
         // Assert
         assertThat(exitCode).isEqualTo(1);
-        verify(clientService).validateLedger(AssetType.OBJECT, keys, 10, 0);
+        verify(clientService).validateObject("OBJECT_ID", 10, 0);
       }
 
       @Test
@@ -243,19 +363,18 @@ void throwsClientExceptionThenReturns1() {
               "--end-age=10",
             };
 
-        ImmutableList<String> keys = ImmutableList.of("OBJECT_ID");
         GenericContractLedgerValidation command = parseArgs(args);
 
         doThrow(new ClientException("", StatusCode.RUNTIME_ERROR))
             .when(clientService)
-            .validateLedger(AssetType.OBJECT, keys, 0, 10);
+            .validateObject("OBJECT_ID", 0, 10);
 
         // Act
         int exitCode = command.call(factory, clientService);
 
         // Assert
         assertThat(exitCode).isEqualTo(1);
-        verify(clientService).validateLedger(AssetType.OBJECT, keys, 0, 10);
+        verify(clientService).validateObject("OBJECT_ID", 0, 10);
       }
     }
   }
diff --git a/common/src/main/java/com/scalar/dl/genericcontracts/AssetType.java b/common/src/main/java/com/scalar/dl/genericcontracts/AssetType.java
index e835d1b9..b9c10fe9 100644
--- a/common/src/main/java/com/scalar/dl/genericcontracts/AssetType.java
+++ b/common/src/main/java/com/scalar/dl/genericcontracts/AssetType.java
@@ -3,4 +3,7 @@
 public enum AssetType {
   OBJECT,
   COLLECTION,
+  TABLE,
+  RECORD,
+  INDEX,
 }
diff --git a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java
index 256e9cbc..e041b91a 100644
--- a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java
+++ b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java
@@ -1049,4 +1049,37 @@ public void validateLedger_CollectionGiven_ShouldReturnCorrectResult() {
     assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
     assertThat(actual.getAuditorProof().isPresent()).isFalse();
   }
+
+  @Test
+  public void validateObject_ObjectGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    prepareObject();
+
+    // Act
+    LedgerValidationResult actual = clientService.validateObject(SOME_OBJECT_ID);
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(OBJECT_ID_PREFIX + SOME_OBJECT_ID);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(2);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateCollection_CollectionGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    prepareCollection();
+
+    // Act
+    LedgerValidationResult actual = clientService.validateCollection(SOME_COLLECTION_ID);
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId())
+        .isEqualTo(COLLECTION_ID_PREFIX + SOME_COLLECTION_ID);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
 }
diff --git a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractTableEndToEndTest.java b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractTableEndToEndTest.java
index ad0a869c..d747a01d 100644
--- a/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractTableEndToEndTest.java
+++ b/generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractTableEndToEndTest.java
@@ -1,5 +1,9 @@
 package com.scalar.dl.genericcontracts;
 
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.ASSET_ID_SEPARATOR;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_INDEX;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_RECORD;
+import static com.scalar.dl.genericcontracts.table.v1_0_0.Constants.PREFIX_TABLE;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -21,6 +25,8 @@
 import com.scalar.dl.client.exception.ClientException;
 import com.scalar.dl.genericcontracts.table.v1_0_0.Constants;
 import com.scalar.dl.ledger.model.ContractExecutionResult;
+import com.scalar.dl.ledger.model.LedgerValidationResult;
+import com.scalar.dl.ledger.service.StatusCode;
 import com.scalar.dl.ledger.util.JacksonSerDe;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -30,6 +36,7 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
+import javax.json.Json;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -2003,4 +2010,188 @@ public void getHistory_InvalidKeyTypeGiven_ShouldReturnEmpty() {
         .isExactlyInstanceOf(ClientException.class)
         .hasMessage(Constants.INVALID_KEY_TYPE);
   }
+
+  @Test
+  public void validateTableSchema_TableGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    createTable(TABLE_NAME_1, COLUMN_NAME_1, KEY_TYPE_NUMBER, ImmutableMap.of());
+
+    // Act
+    LedgerValidationResult actual = clientService.validateTableSchema(TABLE_NAME_1);
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(PREFIX_TABLE + TABLE_NAME_1);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateRecord_JsonValueGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    double value = 1.0;
+    createTable(
+        TABLE_NAME_1,
+        COLUMN_NAME_1,
+        KEY_TYPE_NUMBER,
+        ImmutableMap.of(COLUMN_NAME_2, KEY_TYPE_NUMBER));
+    insertRecord(TABLE_NAME_1, 1, 1);
+    String expected =
+        PREFIX_RECORD
+            + String.join(ASSET_ID_SEPARATOR, ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_1, "1"));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateRecord(TABLE_NAME_1, COLUMN_NAME_1, Json.createValue(value));
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateRecord_ValueNodeGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    createTable(
+        TABLE_NAME_1,
+        COLUMN_NAME_1,
+        KEY_TYPE_NUMBER,
+        ImmutableMap.of(COLUMN_NAME_2, KEY_TYPE_NUMBER));
+    insertRecord(TABLE_NAME_1, 1, 1);
+    String expected =
+        PREFIX_RECORD
+            + String.join(ASSET_ID_SEPARATOR, ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_1, "1"));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateRecord(TABLE_NAME_1, COLUMN_NAME_1, IntNode.valueOf(1));
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateRecord_StringGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    String value = "val";
+    createTable(TABLE_NAME_1, COLUMN_NAME_1, KEY_TYPE_STRING, ImmutableMap.of());
+    JsonNode record = prepareRecord(ImmutableMap.of(COLUMN_NAME_1, TextNode.valueOf(value)));
+    clientService.executeContract(CONTRACT_ID_INSERT, prepareInsert(TABLE_NAME_1, record));
+    String expected =
+        PREFIX_RECORD
+            + String.join(ASSET_ID_SEPARATOR, ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_1, value));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateRecord(TABLE_NAME_1, COLUMN_NAME_1, "\"" + value + "\"");
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateIndexRecord_JsonValueGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    int value = 1;
+    createTable(
+        TABLE_NAME_1,
+        COLUMN_NAME_1,
+        KEY_TYPE_NUMBER,
+        ImmutableMap.of(COLUMN_NAME_2, KEY_TYPE_NUMBER));
+    JsonNode record =
+        prepareRecord(
+            ImmutableMap.of(
+                COLUMN_NAME_1, IntNode.valueOf(1), COLUMN_NAME_2, IntNode.valueOf(value)));
+    clientService.executeContract(CONTRACT_ID_INSERT, prepareInsert(TABLE_NAME_1, record));
+    String expected =
+        PREFIX_INDEX
+            + String.join(
+                ASSET_ID_SEPARATOR,
+                ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_2, String.valueOf(value)));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateIndexRecord(TABLE_NAME_1, COLUMN_NAME_2, Json.createValue(value));
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateIndexRecord_ValueNodeGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    createTable(
+        TABLE_NAME_1,
+        COLUMN_NAME_1,
+        KEY_TYPE_NUMBER,
+        ImmutableMap.of(COLUMN_NAME_2, KEY_TYPE_NUMBER));
+    clientService.executeContract(
+        CONTRACT_ID_INSERT,
+        prepareInsert(
+            TABLE_NAME_1,
+            prepareRecord(
+                ImmutableMap.of(
+                    COLUMN_NAME_1, IntNode.valueOf(1), COLUMN_NAME_2, DoubleNode.valueOf(1.23)))));
+    String expected =
+        PREFIX_INDEX
+            + String.join(
+                ASSET_ID_SEPARATOR, ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_2, "1.23"));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateIndexRecord(TABLE_NAME_1, COLUMN_NAME_2, DoubleNode.valueOf(1.23));
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
+
+  @Test
+  public void validateIndexRecord_StringGiven_ShouldReturnCorrectResult() {
+    // Arrange
+    String value = "val";
+    createTable(
+        TABLE_NAME_1,
+        COLUMN_NAME_1,
+        KEY_TYPE_NUMBER,
+        ImmutableMap.of(COLUMN_NAME_2, KEY_TYPE_STRING));
+    JsonNode record =
+        prepareRecord(
+            ImmutableMap.of(
+                COLUMN_NAME_1, IntNode.valueOf(1), COLUMN_NAME_2, TextNode.valueOf(value)));
+    clientService.executeContract(CONTRACT_ID_INSERT, prepareInsert(TABLE_NAME_1, record));
+    String expected =
+        PREFIX_INDEX
+            + String.join(ASSET_ID_SEPARATOR, ImmutableList.of(TABLE_NAME_1, COLUMN_NAME_2, value));
+
+    // Act
+    LedgerValidationResult actual =
+        clientService.validateIndexRecord(TABLE_NAME_1, COLUMN_NAME_2, "\"" + value + "\"");
+
+    // Assert
+    assertThat(actual.getCode()).isEqualTo(StatusCode.OK);
+    assertThat(actual.getLedgerProof().isPresent()).isTrue();
+    assertThat(actual.getLedgerProof().get().getId()).isEqualTo(expected);
+    assertThat(actual.getLedgerProof().get().getAge()).isEqualTo(0);
+    assertThat(actual.getAuditorProof().isPresent()).isFalse();
+  }
 }


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements the validate-ledger command and associated APIs for assets created by table-based generic contracts. The changes include new tests for table schema, record, and index validations; updates to the ledger validation command-line tool to support separate methods for object, collection, table, record, and index assets; and an extension of the AssetType enum to include TABLE, RECORD, and INDEX.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractTableEndToEndTest.java | Added end-to-end tests for table schema, record, and index validations. |
| generic-contracts/src/integration-test/java/com/scalar/dl/genericcontracts/GenericContractObjectAndCollectionEndToEndTest.java | Added end-to-end tests for object and collection validations. |
| common/src/main/java/com/scalar/dl/genericcontracts/AssetType.java | Extended enum with TABLE, RECORD, and INDEX asset types. |
| client/src/test/java/com/scalar/dl/client/tool/GenericContractLedgerValidationTest.java | Updated tests to use dedicated validation methods instead of a generic validation call. |
| client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java | Added tests for ledger validation methods using new asset type prefixes. |
| client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java | Modified the command-line tool to branch based on the asset type and perform JSON deserialization for table-based assets. |
| client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java | Added ledger validation methods for table, record, and index assets along with a helper for JSON node conversion. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java:60**
* [nitpick] The field 'key' in TableArguments might be made more descriptive (e.g. 'primaryOrIndexKey') to improve code clarity.
```
private PrimaryOrIndexKey key;
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T07:23:46Z
user: login: jnmt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T07:26:16Z
user: login: jnmt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T07:29:47Z
user: login: jnmt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T02:43:02Z
user: login: feeblefakie
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T03:32:22Z
user: login: choplin
body: LGTM!
state: APPROVED
author_association: NONE
submitted_at: 2025-05-23T06:24:41Z
user: login: jnmt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T06:17:41Z
user: login: jnmt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T06:19:39Z
user: login: feeblefakie
body: LGTM! Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T10:07:15Z

Review comments:
diff_hunk: @@ -185,4 +196,141 @@ public void executeContract_ShouldCallClientServiceExecuteContract() {
     // Assert
     assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class);
   }
+
+  @Test
+  public void validateObjectAsset_ShouldCallClientServiceValidateLedgerWithObjectPrefix() {
+    // Arrange Act
+    service.validateObject(ANY_ID);
+    service.validateObject(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateCollectionAsset_ShouldCallClientServiceValidateLedgerWithCollectionPrefix() {
+    // Arrange Act
+    service.validateCollection(ANY_ID);
+    service.validateCollection(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateTableAsset_ShouldCallClientServiceValidateLedgerWithTablePrefix() {
+    // Arrange Act
+    service.validateTableSchema(ANY_TABLE);
+    service.validateTableSchema(ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE);
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateRecordAsset_ShouldCallClientServiceValidateLedgerWithRecordPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
path: client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java
user: login: Copilot
body: [nitpick] Consider double-checking the conversion of BigDecimal to a double string to ensure no precision loss occurs during ledger proof generation.
created_at: 2025-05-14T07:23:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,17 +107,31 @@ Integer call(ClientServiceFactory factory) throws Exception {
 
   @VisibleForTesting
   Integer call(ClientServiceFactory factory, GenericContractClientService service) {
+    JacksonSerDe serde = new JacksonSerDe(new ObjectMapper());
path: client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java
user: login: Copilot
body: [nitpick] Consider caching or reusing the JacksonSerDe (or its underlying ObjectMapper) instead of creating a new instance on each call to improve performance.
```suggestion
    JacksonSerDe serde = SERDE;
```
created_at: 2025-05-14T07:23:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,17 +107,31 @@ Integer call(ClientServiceFactory factory) throws Exception {
 
   @VisibleForTesting
   Integer call(ClientServiceFactory factory, GenericContractClientService service) {
+    JacksonSerDe serde = new JacksonSerDe(new ObjectMapper());
path: client/src/main/java/com/scalar/dl/client/tool/GenericContractLedgerValidation.java
user: login: jnmt
body: This is not a daemon, so it's not the case.
created_at: 2025-05-14T07:26:16Z
author_association: MEMBER
diff_hunk: @@ -185,4 +196,141 @@ public void executeContract_ShouldCallClientServiceExecuteContract() {
     // Assert
     assertThat(thrown).isExactlyInstanceOf(IllegalArgumentException.class);
   }
+
+  @Test
+  public void validateObjectAsset_ShouldCallClientServiceValidateLedgerWithObjectPrefix() {
+    // Arrange Act
+    service.validateObject(ANY_ID);
+    service.validateObject(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(OBJECT_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateCollectionAsset_ShouldCallClientServiceValidateLedgerWithCollectionPrefix() {
+    // Arrange Act
+    service.validateCollection(ANY_ID);
+    service.validateCollection(ANY_ID, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID);
+    verify(clientService).validateLedger(COLLECTION_ID_PREFIX + ANY_ID, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateTableAsset_ShouldCallClientServiceValidateLedgerWithTablePrefix() {
+    // Arrange Act
+    service.validateTableSchema(ANY_TABLE);
+    service.validateTableSchema(ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+
+    // Assert
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE);
+    verify(clientService).validateLedger(PREFIX_TABLE + ANY_TABLE, ANY_START_AGE, ANY_END_AGE);
+  }
+
+  @Test
+  public void validateRecordAsset_ShouldCallClientServiceValidateLedgerWithRecordPrefix() {
+    // Arrange
+    String stringValue = "val";
+    String expectedForStringValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + stringValue;
+    int intValue = 1;
+    String expectedForIntValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + intValue;
+    double doubleValue = 1.23;
+    String expectedForDoubleValue =
+        PREFIX_RECORD
+            + ANY_TABLE
+            + ASSET_ID_SEPARATOR
+            + ANY_COLUMN
+            + ASSET_ID_SEPARATOR
+            + doubleValue;
+    double doubleIntegerValue = 2.0;
+    String expectedForDoubleIntegerValue =
+        PREFIX_RECORD + ANY_TABLE + ASSET_ID_SEPARATOR + ANY_COLUMN + ASSET_ID_SEPARATOR + "2";
+    BigDecimal bigDecimalValue = new BigDecimal("1.2345678901234567890123456789");
+    String expectedForBigDecimalValue =
path: client/src/test/java/com/scalar/dl/client/service/GenericContractClientServiceTest.java
user: login: jnmt
body: This is intended for testing. The big decimal values are converted to double values when going through the contracts, and the asset ID also uses double values, losing precision.
created_at: 2025-05-14T07:29:47Z
author_association: MEMBER
diff_hunk: @@ -454,9 +759,32 @@ private String buildAssetId(AssetType type, List<String> keys) {
       case COLLECTION:
         checkArgument(keys.size() == 1, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
         return COLLECTION_ID_PREFIX + keys.get(0);
+      case TABLE:
+        checkArgument(keys.size() == 1, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_TABLE + keys.get(0);
+      case RECORD:
+        checkArgument(keys.size() == 3, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_RECORD + String.join(ASSET_ID_SEPARATOR, keys);
+      case INDEX:
+        checkArgument(keys.size() == 3, ClientError.SERVICE_WRONG_KEYS_SPECIFIED.buildMessage());
+        return PREFIX_INDEX + String.join(ASSET_ID_SEPARATOR, keys);
       default:
         throw new IllegalArgumentException(
             ClientError.SERVICE_WRONG_ASSET_TYPE_SPECIFIED.buildMessage());
     }
   }
+
+  private String toStringFrom(JsonValue value) {
+    return toStringFrom(jacksonSerDe.deserialize(value.toString()));
+  }
+
+  private String toStringFrom(JsonNode value) {
path: client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
user: login: jnmt
body: Since the numeric value conversion is based on the logic in the Jackson-based table generic contracts, we need to convert JsonObejct and String values to JsonNode ones to get correct asset IDs, although they sometimes lose precision; e.g., a large decimal value of JsonObject/String will be handled as double in Jackson. We should notice it in the documents somewhere.
created_at: 2025-05-19T02:43:02Z
author_association: MEMBER
diff_hunk: @@ -434,6 +444,301 @@ public LedgerValidationResult validateLedger(
     return clientService.validateLedger(buildAssetId(type, keys), startAge, endAge);
   }
 
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId) {
+    return clientService.validateLedger(buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)));
+  }
+
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)));
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName) {
+    return clientService.validateLedger(buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)));
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(String tableName, String columnName, String value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(
+                tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, String value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified index entry in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexEntry(
path: client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
user: login: feeblefakie
body: ```suggestion
  public LedgerValidationResult validateIndexRecord(
```
Sorry, I suggested `Index Entry`, but `Index Record` might be better and more consistent.
What do you think?
created_at: 2025-05-21T03:17:38Z
author_association: MEMBER
diff_hunk: @@ -434,6 +444,301 @@ public LedgerValidationResult validateLedger(
     return clientService.validateLedger(buildAssetId(type, keys), startAge, endAge);
   }
 
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId) {
+    return clientService.validateLedger(buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)));
+  }
+
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)));
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName) {
path: client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
user: login: feeblefakie
body: It might be less error-prone to separate the validation methods for table-oriented and the other.
Let's discuss it.
created_at: 2025-05-21T03:32:19Z
author_association: MEMBER
diff_hunk: @@ -434,6 +444,301 @@ public LedgerValidationResult validateLedger(
     return clientService.validateLedger(buildAssetId(type, keys), startAge, endAge);
   }
 
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId) {
+    return clientService.validateLedger(buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)));
+  }
+
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)));
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName) {
+    return clientService.validateLedger(buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)));
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.TABLE, ImmutableList.of(tableName)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, JsonValue value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, ValueNode value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD, ImmutableList.of(tableName, columnName, toStringFrom(value))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(String tableName, String columnName, String value) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(
+                tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))));
+  }
+
+  /**
+   * Validates the specified record in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName a primary key column name
+   * @param value a primary key column value in a JSON format
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateRecord(
+      String tableName, String columnName, String value, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(
+            AssetType.RECORD,
+            ImmutableList.of(tableName, columnName, toStringFrom(jacksonSerDe.deserialize(value)))),
+        startAge,
+        endAge);
+  }
+
+  /**
+   * Validates the specified index entry in the ledger.
+   *
+   * @param tableName a table name
+   * @param columnName an index key column name
+   * @param value an index key column value
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateIndexEntry(
path: client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
user: login: jnmt
body: @feeblefakie Right, this basically validates an index record (asset) that consists of several index entries. Fixed in 1ee6da9. PTAL again!

Note: we still use "index entry/entries" in several places of Scan/Update contracts, but I think they are consistent since the index record (asset) = index entries = multiple "index entry".
created_at: 2025-05-28T06:17:41Z
author_association: MEMBER
diff_hunk: @@ -434,6 +444,301 @@ public LedgerValidationResult validateLedger(
     return clientService.validateLedger(buildAssetId(type, keys), startAge, endAge);
   }
 
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId) {
+    return clientService.validateLedger(buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)));
+  }
+
+  /**
+   * Validates the specified object in the ledger.
+   *
+   * @param objectId an object ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateObject(String objectId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.OBJECT, ImmutableList.of(objectId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)));
+  }
+
+  /**
+   * Validates the specified collection in the ledger.
+   *
+   * @param collectionId a collection ID
+   * @param startAge an age to be validated from (inclusive)
+   * @param endAge an age to be validated to (inclusive)
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateCollection(String collectionId, int startAge, int endAge) {
+    return clientService.validateLedger(
+        buildAssetId(AssetType.COLLECTION, ImmutableList.of(collectionId)), startAge, endAge);
+  }
+
+  /**
+   * Validates the schema of the specified table in the ledger.
+   *
+   * @param tableName a table name
+   * @return {@link LedgerValidationResult}
+   * @throws ClientException if a request fails for some reason
+   */
+  public LedgerValidationResult validateTableSchema(String tableName) {
path: client/src/main/java/com/scalar/dl/client/service/GenericContractClientService.java
user: login: jnmt
body: @feeblefakie Thank you for the suggestion! As discussed, let's keep it as is for now and continue discussing introducing a dedicated client service like `TableOrientedClientService`.
created_at: 2025-05-28T06:19:39Z
author_association: MEMBER

PR comments:
user: login: jnmt
created_at: 2025-05-14T07:05:42Z
body: This pull request introduces enhancements to the `GenericContractClientService` and its associated tools and tests, focusing on adding validation methods for various asset types in the ledger. The changes include new methods for validating objects, collections, tables, records, and index entries, along with updates to the command-line tool and corresponding tests.

### Enhancements to Validation Methods:

* **New Validation Methods for Asset Types**: Added methods to validate objects, collections, table schemas, records, and index entries in the ledger. These methods support both basic validation and validation within a specified age range. (`GenericContractClientService.java`)
* **Asset ID Construction**: Extended the `buildAssetId` method to support new asset types (TABLE, RECORD, INDEX) with appropriate prefixes and validation logic for key sizes. (`GenericContractClientService.java`)

### Updates to Command-Line Tool:

* **Command-Line Argument Enhancements**: Updated the `GenericContractLedgerValidation` tool to accept new arguments for table-related validations, including table name, primary key, and index key details. (`GenericContractLedgerValidation.java`)
* **Validation Logic in Command Execution**: Modified the `call` method to handle the new validation methods for tables, records, and index entries based on the provided arguments. (`GenericContractLedgerValidation.java`)

### Test Coverage Improvements:

* **Unit Tests for New Validation Methods**: Added unit tests to verify the behavior of the new validation methods for objects, collections, tables, records, and index entries. (`GenericContractClientServiceTest.java`)
* **Command-Line Tool Tests**: Updated tests for `GenericContractLedgerValidation` to validate the new functionality and ensure proper argument handling. (`GenericContractLedgerValidationTest.java`)

These changes significantly enhance the functionality and robustness of the ledger validation capabilities, ensuring comprehensive support for table-oriented assets and improving test coverage.
author_association: MEMBER
user: login: jnmt
created_at: 2025-05-19T02:35:58Z
body: I noticed that the client service should have provided not only Jackson-specific (with `ValueNode`) interfaces but also ones for non-Jackson users, the same as other interfaces like `executeContract()`. So, I added them in 720ffe6.
author_association: MEMBER