Skip to content

Commit 3e9e24f

Browse files
committed
Second time (is not the charm)
1 parent db3da1a commit 3e9e24f

File tree

2 files changed

+75
-56
lines changed

2 files changed

+75
-56
lines changed

api/src/org/labkey/api/data/AbstractTableInfo.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.Test;
2828
import org.labkey.api.cache.CacheManager;
2929
import org.labkey.api.collections.CaseInsensitiveHashMap;
30+
import org.labkey.api.collections.CaseInsensitiveHashSet;
3031
import org.labkey.api.collections.CaseInsensitiveMapWrapper;
3132
import org.labkey.api.collections.CaseInsensitiveTreeSet;
3233
import org.labkey.api.collections.NamedObjectList;
@@ -1892,16 +1893,18 @@ public final boolean canStreamTriggers(Container c)
18921893
if (triggers.isEmpty())
18931894
return null;
18941895

1895-
var columns = triggers.stream()
1896-
.map(Trigger::getManagedColumns)
1897-
.filter(Objects::nonNull)
1898-
.flatMap(Collection::stream)
1899-
.toList();
1896+
var columns = new CaseInsensitiveHashSet();
1897+
for (var trigger : triggers)
1898+
{
1899+
var managedColumns = trigger.getManagedColumns();
1900+
if (managedColumns == null)
1901+
continue;
19001902

1901-
if (columns.isEmpty())
1902-
return null;
1903+
columns.addAll(managedColumns.insert());
1904+
columns.addAll(managedColumns.update());
1905+
}
19031906

1904-
return Sets.newCaseInsensitiveHashSet(columns);
1907+
return columns.isEmpty() ? null : columns;
19051908
}
19061909

19071910
private Collection<Trigger> _triggers = null;
@@ -1978,15 +1981,6 @@ public void fireRowTrigger(Container c, User user, TriggerType type, boolean bef
19781981

19791982
for (Trigger script : triggers)
19801983
{
1981-
var managed = before ? script.getManagedColumns() : null;
1982-
1983-
// Inject sentinel for each declared column absent from the row; the trigger must handle it
1984-
if (newRow != null && managed != null && before)
1985-
{
1986-
for (var col : managed)
1987-
newRow.putIfAbsent(col, Trigger.COLUMN_SENTINEL);
1988-
}
1989-
19901984
// Snapshot keys after injection — trigger may update values but must not alter the key set
19911985
var keysBeforeTrigger = newRow != null && before ? Set.copyOf(newRow.keySet()) : null;
19921986

@@ -1996,6 +1990,11 @@ public void fireRowTrigger(Container c, User user, TriggerType type, boolean bef
19961990

19971991
if (newRow != null && before)
19981992
{
1993+
var managed = script.getManagedColumns();
1994+
Set<String> managedCols = null;
1995+
if (managed != null)
1996+
managedCols = managed.getColumns(type);
1997+
19991998
// Verify the trigger did not add or remove columns that are not managed
20001999
if (!newRow.keySet().equals(keysBeforeTrigger))
20012000
{
@@ -2006,10 +2005,15 @@ public void fireRowTrigger(Container c, User user, TriggerType type, boolean bef
20062005
removed.removeAll(newRow.keySet());
20072006

20082007
// managed column removals are intentional
2009-
if (managed != null)
2008+
if (managedCols != null)
20102009
{
2011-
added.removeAll(managed);
2012-
removed.removeAll(managed);
2010+
added.removeAll(managedCols);
2011+
removed.removeAll(managedCols);
2012+
if (managed.ignored() != null)
2013+
{
2014+
added.removeAll(managed.ignored());
2015+
removed.removeAll(managed.ignored());
2016+
}
20132017
}
20142018

20152019
if (!added.isEmpty() || !removed.isEmpty())
@@ -2024,12 +2028,12 @@ public void fireRowTrigger(Container c, User user, TriggerType type, boolean bef
20242028
}
20252029
}
20262030

2027-
// Verify the trigger handled every declared column it was responsible for
2028-
if (managed != null)
2031+
// Verify that update triggers handle every managed column
2032+
if (managedCols != null && type == TriggerType.UPDATE)
20292033
{
2030-
for (var col : managed)
2034+
for (var col : managedCols)
20312035
{
2032-
if (newRow.get(col) == Trigger.COLUMN_SENTINEL)
2036+
if (!newRow.containsKey(col))
20332037
errors.addFieldError(col, "Trigger '" + script.getName() + "' declared column '" + col + "' in getManagedColumns() but did not set a value for it. Set null to clear or provide a value.");
20342038
}
20352039
}

api/src/org/labkey/api/data/triggers/Trigger.java

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
package org.labkey.api.data.triggers;
1717

18+
import org.jetbrains.annotations.NotNull;
1819
import org.jetbrains.annotations.Nullable;
1920
import org.json.JSONObject;
21+
import org.labkey.api.collections.Sets;
2022
import org.labkey.api.data.Container;
2123
import org.labkey.api.data.TableInfo;
2224
import org.labkey.api.query.BatchValidationException;
@@ -38,20 +40,6 @@
3840
*/
3941
public interface Trigger
4042
{
41-
/**
42-
* Sentinel value placed into the row map for each column declared by {@link #getManagedColumns()}
43-
* when that column is absent from the incoming row. The trigger must replace this with a real value
44-
* or {@code null} before returning; leaving the sentinel in place is a validation error.
45-
*/
46-
Object COLUMN_SENTINEL = new Object()
47-
{
48-
@Override
49-
public String toString()
50-
{
51-
return "<trigger managed column - not yet handled>";
52-
}
53-
};
54-
5543
/** The trigger name. */
5644
default String getName()
5745
{
@@ -104,41 +92,68 @@ default List<TableInfo.TriggerMethod> getEvents()
10492
}
10593
}
10694

95+
record ManagedColumns(@NotNull Set<String> insert, @NotNull Set<String> update, @Nullable Set<String> ignored)
96+
{
97+
public static ManagedColumns all(@NotNull Set<String> all)
98+
{
99+
return new ManagedColumns(all, all, null);
100+
}
101+
102+
public static ManagedColumns all(@NotNull String... all)
103+
{
104+
return all(Sets.newCaseInsensitiveHashSet(all));
105+
}
106+
107+
public @Nullable Set<String> getColumns(TableInfo.TriggerType type)
108+
{
109+
if (type == TableInfo.TriggerType.INSERT)
110+
return insert;
111+
if (type == TableInfo.TriggerType.UPDATE)
112+
return update;
113+
return null;
114+
}
115+
}
116+
107117
/**
108118
* Returns the set of column names this trigger will read or write during row processing.
109119
* <p>
110-
* For each row where a declared column is absent from the input, the data iterator initializes
111-
* that column to {@link #COLUMN_SENTINEL} before the trigger fires. The trigger must then
112-
* explicitly set each such column to {@code null} or a real value; failure to do so produces
120+
* For each row where a declared column is absent from the input, the trigger must
121+
* explicitly set each such column to null or a real value; failure to do so produces
113122
* a validation error naming this trigger and the unhandled column.
114123
* <p>
115124
* Columns that do not exist in the target table's schema (virtual/passthrough columns) may be
116125
* declared here and will work correctly — the database writer ignores them.
117126
*/
118-
default @Nullable Set<String> getManagedColumns()
127+
default @Nullable ManagedColumns getManagedColumns()
119128
{
120129
return null;
121130
}
122131

123-
/**
124-
* Convenience method for trigger implementations: replaces any {@link #COLUMN_SENTINEL} values
125-
* in managed columns with {@code null}. Call this in early-return paths where the trigger will
126-
* not produce a value for one or more of its declared columns.
127-
*/
128-
default void clearManagedColumns(@Nullable Map<String, Object> row)
132+
default void setInsertManagedColumns(@NotNull Map<String, Object> newRow)
133+
{
134+
setManagedColumns(newRow, null, TableInfo.TriggerType.INSERT);
135+
}
136+
137+
default void setUpdateManagedColumns(@NotNull Map<String, Object> newRow, @NotNull Map<String, Object> oldRow)
129138
{
130-
if (row == null)
139+
if (oldRow == null)
140+
throw new IllegalArgumentException("oldRow must be non-null for UPDATE triggers");
141+
142+
setManagedColumns(newRow, oldRow, TableInfo.TriggerType.UPDATE);
143+
}
144+
145+
private void setManagedColumns(Map<String, Object> newRow, Map<String, Object> oldRow, TableInfo.TriggerType type)
146+
{
147+
var managedCols = getManagedColumns();
148+
if (managedCols == null)
131149
return;
132150

133-
var managedColumns = getManagedColumns();
134-
if (managedColumns == null || managedColumns.isEmpty())
151+
var cols = managedCols.getColumns(type);
152+
if (cols == null)
135153
return;
136154

137-
for (String col : managedColumns)
138-
{
139-
if (row.get(col) == COLUMN_SENTINEL)
140-
row.remove(col);
141-
}
155+
for (var col : cols)
156+
newRow.putIfAbsent(col, oldRow == null ? null : oldRow.get(col));
142157
}
143158

144159
/**

0 commit comments

Comments
 (0)