Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
31b668e
feat: DH-21235: Column Restrction JS API
dgodinez-dh Apr 6, 2026
aa2f17d
non-native decode
dgodinez-dh Apr 7, 2026
55145d3
update comment
dgodinez-dh Apr 7, 2026
180bc15
send restrictions as json
dgodinez-dh Apr 7, 2026
6939127
Revert "send restrictions as json"
dgodinez-dh Apr 7, 2026
75bf43e
abstract base class for validating input tables
dgodinez-dh Apr 8, 2026
89ac34c
additional validators
dgodinez-dh Apr 8, 2026
b7cb56f
Add ColumnRestriction class
dgodinez-dh Apr 8, 2026
962de5e
clean up logging
dgodinez-dh Apr 8, 2026
e7d828e
remove more logging
dgodinez-dh Apr 8, 2026
65f3327
refactor to util
dgodinez-dh Apr 8, 2026
fce1f27
register converter
dgodinez-dh Apr 8, 2026
feed9b9
Generated input table types
niloc132 Apr 9, 2026
14a5807
use generated protobug messages failing on Any
dgodinez-dh Apr 20, 2026
19e610c
fix merge
dgodinez-dh Apr 20, 2026
3822538
Revert "Generated input table types"
dgodinez-dh Apr 20, 2026
367b534
fixes
dgodinez-dh Apr 20, 2026
7166a51
more fixes
dgodinez-dh Apr 20, 2026
a8f75ec
working fix
dgodinez-dh Apr 20, 2026
5d70e42
fix decode
dgodinez-dh Apr 21, 2026
1aebf50
fix class cast exception
dgodinez-dh Apr 21, 2026
3f79ede
code cleanup
dgodinez-dh Apr 21, 2026
5a994f8
converters throw exception
dgodinez-dh Apr 21, 2026
24a801c
more cleanup
dgodinez-dh Apr 21, 2026
fb54116
spotless
dgodinez-dh Apr 21, 2026
d133755
merge latest
dgodinez-dh Apr 28, 2026
9cad852
use type instead of var
dgodinez-dh Apr 28, 2026
0c5b003
use column iterator
dgodinez-dh Apr 29, 2026
a1d704f
wrap updater method
dgodinez-dh Apr 29, 2026
e328238
remove for test only comments
dgodinez-dh Apr 29, 2026
28f86d9
groovy doc update
dgodinez-dh Apr 29, 2026
a9394c5
python docs
dgodinez-dh Apr 29, 2026
4c21aa5
fix case
dgodinez-dh Apr 29, 2026
5b5b291
format
dgodinez-dh Apr 29, 2026
1d00622
update snapshots
dgodinez-dh Apr 29, 2026
136a5b4
Apply suggestions from code review
dgodinez-dh Apr 29, 2026
3fe5ae7
python doc style update
dgodinez-dh Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,17 @@ default List<String> getValueNames() {
}

/**
* If there are client-side defined restrictions on this column; return them as a JSON string to be interpreted by
* the client for properly displaying the edit field.
* If there are client-side defined restrictions on this column, return them as a list of protobuf Any messages.
* These restrictions are used by the client for properly displaying and validating the edit field.
*
* <p>
* The restrictions are packed as {@code google.protobuf.Any} messages, which allows for different restriction types
* (e.g., {@code IntegerRangeRestriction}, {@code DoubleRangeRestriction}, {@code StringListRestriction}, etc.) to
* be sent to the client. The client is responsible for unpacking and interpreting these restrictions.
*
* @param columnName the column name to query
* @return a string representing the restrictions for this column, or null if no client-side restrictions are
* supplied for this column
* @return a list of protobuf Any messages representing the restrictions for this column, or null if no client-side
* restrictions are supplied for this column
*/
@Nullable
default List<Any> getColumnRestrictions(final String columnName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
//
package io.deephaven.server.table.inputtables;

import com.google.protobuf.Any;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.table.TableDefinition;
import io.deephaven.engine.util.input.InputTableStatusListener;
import io.deephaven.engine.util.input.InputTableUpdater;
import io.deephaven.util.annotations.TestUseOnly;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.List;

/**
* An abstract base class for {@link InputTableUpdater} implementations that wrap an existing input table.
*
* <p>
* This class provides a default implementation for most methods by delegating to the wrapped input table. Subclasses
* should override {@link #getColumnRestrictions(String)} and {@link #validateAddOrModify(Table)} to provide custom
* validation logic.
* </p>
*
* <p>
* <b>This class is intended for testing and demonstrating validation functionality, it is not production ready and may
* be changed or removed at any time.</b>
* </p>
*/
@TestUseOnly
public abstract class AbstractBaseValidatingInputTable implements InputTableUpdater {
protected final InputTableUpdater wrapped;

/**
* Construct a new validating input table that wraps the given input table.
*
* @param wrapped the input table to wrap
*/
protected AbstractBaseValidatingInputTable(InputTableUpdater wrapped) {
this.wrapped = wrapped;
}

@Override
public List<String> getKeyNames() {
return wrapped.getKeyNames();
}

@Override
public List<String> getValueNames() {
return wrapped.getValueNames();
}

@Override
public abstract @Nullable List<Any> getColumnRestrictions(String columnName);

@Override
public TableDefinition getTableDefinition() {
return wrapped.getTableDefinition();
}

@Override
public abstract void validateAddOrModify(Table tableToApply);

@Override
public void validateDelete(Table tableToDelete) {
wrapped.validateDelete(tableToDelete);
}

@Override
public void add(Table newData) throws IOException {
wrapped.add(newData);
}

@Override
public void addAsync(Table newData, InputTableStatusListener listener) {
wrapped.addAsync(newData, listener);
}

@Override
public void delete(Table table) throws IOException {
wrapped.delete(table);
}

@Override
public void deleteAsync(Table table, InputTableStatusListener listener) {
wrapped.deleteAsync(table, listener);
}

@Override
public boolean isKey(String columnName) {
return wrapped.isKey(columnName);
}

@Override
public boolean hasColumn(String columnName) {
return wrapped.hasColumn(columnName);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
//
package io.deephaven.server.table.inputtables;

import com.google.protobuf.Any;
import io.deephaven.engine.primitive.iterator.CloseablePrimitiveIteratorOfDouble;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.util.input.InputTableUpdater;
import io.deephaven.engine.util.input.InputTableValidationException;
import io.deephaven.engine.util.input.StructuredErrorImpl;
import io.deephaven.proto.backplane.grpc.DoubleRangeRestriction;
import io.deephaven.util.annotations.TestUseOnly;
import io.deephaven.util.mutable.MutableInt;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* This is an example of an {@link InputTableUpdater} that validates that the values in a Double column are within a
* given range.
*
* <p>
* This class wraps an existing input table, and before performing the underlying validation performs its own validation
* on the range of the column.
* </p>
*
* <p>
* <b>This class is intended for testing and demonstrating validation functionality, it is not production ready and may
* be changed or removed at any time.</b>
* </p>
*/
@TestUseOnly
public class DoubleRangeValidatingInputTable extends AbstractBaseValidatingInputTable {
private final String column;
private final double min;
private final double max;

/**
* Wrap {@code input}, which must be an input table into a new input table that validates that the values in
* {@code column} are within the range {@code ([min, max]}.
*
* @param input the table to wrap
* @param column the column to validate, must be a double type
* @param min the minimum value allowed, inclusive
* @param max the maximum value allowed, inclusive
* @return a new input table that validates the range of {@code column}
*/
public static Table make(Table input, final String column,
final double min,
final double max) {
final InputTableUpdater updater = (InputTableUpdater) input.getAttribute(Table.INPUT_TABLE_ATTRIBUTE);
final DoubleRangeValidatingInputTable validatedUpdater =
new DoubleRangeValidatingInputTable(updater, column, min, max);
return input.withAttributes(Map.of(Table.INPUT_TABLE_ATTRIBUTE, validatedUpdater));
}


private DoubleRangeValidatingInputTable(InputTableUpdater wrapped,
final String column,
final double min,
final double max) {
super(wrapped);
this.column = column;
final Class<?> dataType = getTableDefinition().getColumn(column).getDataType();
if (dataType != double.class) {
throw new IllegalArgumentException("Range column must be a double, but " + column + " is " + dataType);
}
this.min = min;
this.max = max;
}


@Override
public @Nullable List<Any> getColumnRestrictions(String columnName) {
final List<Any> columnRestrictions = wrapped.getColumnRestrictions(columnName);
if (!columnName.equals(column)) {
return columnRestrictions;
}

final List<Any> result = new ArrayList<>();
if (columnRestrictions != null) {
result.addAll(columnRestrictions);
}
final DoubleRangeRestriction rangeRestriction =
DoubleRangeRestriction.newBuilder().setMinInclusive(min).setMaxInclusive(max).build();
result.add(Any.pack(rangeRestriction, "docs.deephaven.io"));
return result;
}


@Override
public void validateAddOrModify(Table tableToApply) {
final List<InputTableValidationException.StructuredError> errors = new ArrayList<>();
final MutableInt position = new MutableInt(0);
try (final CloseablePrimitiveIteratorOfDouble vals = tableToApply.doubleColumnIterator(column)) {
vals.forEachRemaining((double val) -> {
if (val < min || val > max) {
errors.add(new StructuredErrorImpl(
"Value out of range: " + val + " must be between " + min + " and " + max + " inclusive",
column, position.get()));
}
position.increment();
});
}
if (!errors.isEmpty()) {
throw new InputTableValidationException(errors);
}

wrapped.validateAddOrModify(tableToApply);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending
//
package io.deephaven.server.table.inputtables;

import com.google.protobuf.Any;
import io.deephaven.engine.rowset.RowSequence;
import io.deephaven.engine.table.ColumnSource;
import io.deephaven.engine.table.Table;
import io.deephaven.engine.util.input.InputTableUpdater;
import io.deephaven.engine.util.input.InputTableValidationException;
import io.deephaven.engine.util.input.StructuredErrorImpl;
import io.deephaven.proto.backplane.grpc.NonEmptyRestriction;
import io.deephaven.util.annotations.TestUseOnly;
import io.deephaven.util.mutable.MutableInt;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* This is an example of an {@link InputTableUpdater} that validates that the values in a String column are not empty.
*
* <p>
* This class wraps an existing input table, and before performing the underlying validation performs its own validation
* that the column does not contain empty strings.
* </p>
*
* <p>
* <b>This class is intended for testing and demonstrating validation functionality, it is not production ready and may
* be changed or removed at any time.</b>
* </p>
*/
@TestUseOnly
public class NonEmptyValidatingInputTable extends AbstractBaseValidatingInputTable {
private final String column;

/**
* Wrap {@code input}, which must be an input table into a new input table that validates that the values in
* {@code column} are not empty.
*
* @param input the table to wrap
* @param column the column to validate, must be a String type
* @return a new input table that validates {@code column} is not empty
*/
public static Table make(Table input, final String column) {
final InputTableUpdater updater = (InputTableUpdater) input.getAttribute(Table.INPUT_TABLE_ATTRIBUTE);
final NonEmptyValidatingInputTable validatedUpdater = new NonEmptyValidatingInputTable(updater, column);
return input.withAttributes(Map.of(Table.INPUT_TABLE_ATTRIBUTE, validatedUpdater));
}


private NonEmptyValidatingInputTable(InputTableUpdater wrapped, final String column) {
super(wrapped);
this.column = column;
final Class<?> dataType = getTableDefinition().getColumn(column).getDataType();
if (dataType != String.class) {
throw new IllegalArgumentException(
"Non-empty validation only applies to String columns, but " + column + " is " + dataType);
}
}


@Override
public @Nullable List<Any> getColumnRestrictions(String columnName) {
final List<Any> columnRestrictions = wrapped.getColumnRestrictions(columnName);
if (!columnName.equals(column)) {
return columnRestrictions;
}

final List<Any> result = new ArrayList<>();
if (columnRestrictions != null) {
result.addAll(columnRestrictions);
}
final NonEmptyRestriction nonEmptyRestriction = NonEmptyRestriction.newBuilder().build();
result.add(Any.pack(nonEmptyRestriction, "docs.deephaven.io"));
return result;
}


@Override
public void validateAddOrModify(Table tableToApply) {
final List<InputTableValidationException.StructuredError> errors = new ArrayList<>();
final MutableInt position = new MutableInt(0);
final ColumnSource<String> columnSource = tableToApply.getColumnSource(column, String.class);

try (final RowSequence rowSequence =
tableToApply.getRowSet().getRowSequenceByPosition(0, tableToApply.size())) {
rowSequence.forAllRowKeys(rowKey -> {
final String value = columnSource.get(rowKey);
if (value != null && value.isEmpty()) {
errors.add(new StructuredErrorImpl(
"Value must not be empty",
column, position.get()));
}
position.increment();
});
}

if (!errors.isEmpty()) {
throw new InputTableValidationException(errors);
}

wrapped.validateAddOrModify(tableToApply);
}
}

Loading
Loading