Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,28 @@ public class CollectionIndex : AutoCollectionContext<CollectionIndex, IValue>
private readonly List<IValue> _fields = new List<IValue>();
private readonly IIndexCollectionSource _source;

private readonly IDictionary<CollectionIndexKey, HashSet<IValue>> _data =
private readonly Dictionary<CollectionIndexKey, HashSet<IValue>> _data =
new Dictionary<CollectionIndexKey, HashSet<IValue>>();

public CollectionIndex(IIndexCollectionSource source, IEnumerable<IValue> fields)
{
foreach (var field in fields)
{
if (field is ValueTable.ValueTableColumn column)
column.AddToIndex();
_fields.Add(field);
}

_source = source;
_fields.AddRange(fields);
foreach (var value in _source)
{
ElementAdded(value);
}
}
Comment on lines 27 to 41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for CollectionIndex constructor call sites and duplicate field validation

echo "=== CollectionIndex constructor call sites ==="
rg -n "new CollectionIndex\(" -A 2 -B 2 -t cs

echo ""
echo "=== CollectionIndex class definition ==="
rg -n "class CollectionIndex" -A 20 -t cs | head -40

echo ""
echo "=== AddToIndex method definition ==="
rg -n "AddToIndex" -B 2 -A 5 -t cs

echo ""
echo "=== Check _fields field initialization and usage ==="
rg -n "_fields" -B 1 -A 1 -t cs | head -50

Repository: Mr-Rm/OneScript

Length of output: 11094


🏁 Script executed:

#!/bin/bash
# Need to examine BuildFieldList and understand duplicate handling

echo "=== BuildFieldList method ==="
rg -n "BuildFieldList" -B 2 -A 15 -t cs

echo ""
echo "=== CollectionIndexKey.Extract method ==="
rg -n "Extract" -B 2 -A 20 -t cs src/OneScript.StandardLibrary/Collections/Indexes/CollectionIndexKey.cs

Repository: Mr-Rm/OneScript

Length of output: 4888


🏁 Script executed:

#!/bin/bash
# Check for tests or comments about duplicate fields in indexes

echo "=== Search for test files related to CollectionIndex ==="
fd -t f "*.cs" -i "*test*" | xargs rg -l "CollectionIndex|BuildFieldList" 2>/dev/null | head -10

echo ""
echo "=== Look for any comments about duplicates near field handling ==="
rg -n "duplicate|forbid|prevent|unique" -i -B 2 -A 2 -t cs src/OneScript.StandardLibrary/Collections/Indexes/ src/OneScript.StandardLibrary/Collections/ValueTable/

echo ""
echo "=== Check CollectionIndex constructor more carefully ==="
sed -n '27,41p' src/OneScript.StandardLibrary/Collections/Indexes/CollectionIndex.cs

Repository: Mr-Rm/OneScript

Length of output: 3384


Prevent duplicate fields in the constructor with validation.

The constructor accepts duplicate fields in the IEnumerable<IValue> fields parameter without validation. If the same column appears multiple times, AddToIndex() will be called multiple times, inflating the _indicesCount counter. While CollectionIndexKey.Extract() uses Dictionary.TryAdd() to silently ignore duplicate keys (and RemoveField() handles duplicates symmetrically), allowing duplicates creates unnecessary confusion and overhead. Add validation in BuildFieldList() to reject duplicate field names, or add a check in the constructor to throw if duplicates are detected.

🤖 Prompt for AI Agents
In src/OneScript.StandardLibrary/Collections/Indexes/CollectionIndex.cs around
lines 27-41, the constructor currently accepts duplicate fields and will call
AddToIndex() multiple times which inflates _indicesCount; update the constructor
(or BuildFieldList called by it) to validate and reject duplicate field names:
collect field identifiers (e.g. column name for ValueTableColumn or a stable key
for IValue), if a duplicate is found throw an ArgumentException (with a clear
message including the duplicate name) rather than adding it, and ensure
AddToIndex() is only invoked once per unique column (iterate the deduplicated
list when calling AddToIndex and when adding to _fields).


internal bool CanBeUsedFor(IEnumerable<IValue> searchFields)
{
return _fields.Any() && _fields.ToHashSet().IsSubsetOf(searchFields.ToHashSet());
return _fields.Count > 0 && _fields.All(f => searchFields.Contains(f));
}

private CollectionIndexKey IndexKey(PropertyNameIndexAccessor source)
Expand All @@ -55,11 +65,26 @@ internal void FieldRemoved(IValue field)
{
if (_fields.Contains(field))
{
while (_fields.Contains(field)) _fields.Remove(field);
while (_fields.Contains(field))
{
if (field is ValueTable.ValueTableColumn column)
column.DeleteFromIndex();

_fields.Remove(field);
}
Rebuild();
}
}

internal void ExcludeFields()
{
foreach (var field in _fields)
{
if (field is ValueTable.ValueTableColumn column)
column.DeleteFromIndex();
}
}

internal void ElementAdded(PropertyNameIndexAccessor element)
{
var key = CollectionIndexKey.Extract(element, _fields);
Expand All @@ -82,10 +107,7 @@ internal void ElementRemoved(PropertyNameIndexAccessor element)
}
}

internal void Clear()
{
_data.Clear();
}
internal void Clear() => _data.Clear();

internal void Rebuild()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,30 @@ public CollectionIndexes(IIndexCollectionSource owner)
public CollectionIndex Add(string columns)
{
var newIndex = new CollectionIndex(_owner, BuildFieldList(_owner, columns));
newIndex.Rebuild();
_indexes.Add(newIndex);
return newIndex;
}

[ContextMethod("Количество", "Count")]
public override int Count()
{
return _indexes.Count();
return _indexes.Count;
}

[ContextMethod("Удалить", "Delete")]
public void Delete(IValue index)
{
_indexes.Remove(GetIndex(index));
var idx = GetIndex(index);
idx.ExcludeFields();
_indexes.Remove(idx);
}

[ContextMethod("Очистить", "Clear")]
public void Clear()
{
foreach (var idx in _indexes)
idx.ExcludeFields();

_indexes.Clear();
}

Expand Down Expand Up @@ -130,21 +134,18 @@ public CollectionIndex FindSuitableIndex(IEnumerable<IValue> searchFields)
return _indexes.FirstOrDefault(index => index.CanBeUsedFor(searchFields));
}

private static IList<IValue> BuildFieldList(IIndexCollectionSource source, string fieldList)
private static List<IValue> BuildFieldList(IIndexCollectionSource source, string fieldList)
{
var fields = new List<IValue>();
var fieldNames = fieldList.Split(',');
if (string.IsNullOrEmpty(fieldList))
return fields;

var fieldNames = fieldList.Split(',', System.StringSplitOptions.RemoveEmptyEntries);
foreach (var fieldName in fieldNames)
{
if (!string.IsNullOrWhiteSpace(fieldName))
{
var field = source.GetField(fieldName.Trim());
if (field == null)
{
throw ColumnException.WrongColumnName(fieldName);
}
fields.Add(field);
}
{
var name = fieldName.Trim();
var field = source.GetField(name) ?? throw ColumnException.WrongColumnName(fieldName);
fields.Add(field);
}

return fields;
Expand Down
44 changes: 21 additions & 23 deletions src/OneScript.StandardLibrary/Collections/ValueTable/ValueTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public ValueTable()
Columns = new ValueTableColumnCollection(this);
_rows = new List<ValueTableRow>();
Indexes = new CollectionIndexes(this);
}

}
/// <summary>
/// Коллекция колонок
/// </summary>
Expand Down Expand Up @@ -70,7 +70,8 @@ public ValueTableRow Add()
{
var row = new ValueTableRow(this);
_rows.Add(row);
Indexes.ElementAdded(row);
if (Indexes.Count() > 0)
Indexes.ElementAdded(row);
return row;
}

Expand All @@ -83,13 +84,14 @@ public ValueTableRow Add()
[ContextMethod("Вставить", "Insert")]
public ValueTableRow Insert(int index)
{
var row = new ValueTableRow(this);
if (index < 0 || index > _rows.Count)
_rows.Add(row); // для совместимости с 1С, хотя логичней было бы исключение
else
_rows.Insert(index, row);
return Add(); // для совместимости с 1С, хотя логичней было бы исключение

Indexes.ElementAdded(row);
var row = new ValueTableRow(this);
_rows.Insert(index, row);

if (Indexes.Count() > 0)
Indexes.ElementAdded(row);
return row;
}

Expand Down Expand Up @@ -157,7 +159,7 @@ public ArrayImpl UnloadColumn(IValue column)
return result;
}

private List<ValueTableColumn> GetProcessingColumnList(string columnNames, bool emptyListInCaseOfNull = false)
private List<ValueTableColumn> GetProcessingColumnList(string columnNames, bool emptyListInCaseOfNull = false)
{
var processing_list = new List<ValueTableColumn>();
if (string.IsNullOrEmpty(columnNames)) // Передали пустую строку вместо списка колонок
Expand Down Expand Up @@ -312,16 +314,18 @@ public IValue Find(IValue value, string columnNames = null)
}
}
return ValueFactory.Create();
}
}

private ValueTableColumn GetColumnOrThrow(string column_name)
=> this.Columns.FindColumnByName(column_name)
?? throw ColumnException.WrongColumnName(column_name);


private bool CheckFilterCriteria(ValueTableRow Row, StructureImpl Filter)
{
foreach (var kv in Filter)
{
var Column = Columns.FindColumnByName(kv.Key.ToString());
if (Column == null)
throw ColumnException.WrongColumnName(kv.Key.ToString());

var Column = GetColumnOrThrow(kv.Key.ToString());
IValue current = Row.Get(Column);
if (!current.StrictEquals(kv.Value))
return false;
Expand All @@ -344,7 +348,7 @@ public ArrayImpl FindRows(StructureImpl filter)

var mapped = ColumnsMap(filter);
var suitableIndex = Indexes.FindSuitableIndex(mapped.Keys());
var dataToScan = suitableIndex != null ? suitableIndex.GetData(mapped) : _rows;
var dataToScan = suitableIndex?.GetData(mapped) ?? _rows;

foreach (var element in dataToScan)
{
Expand All @@ -361,10 +365,7 @@ private MapImpl ColumnsMap(StructureImpl filter)
var result = new MapImpl();
foreach (var kv in filter)
{
var key = Columns.FindColumnByName(kv.Key.ToString());
if (key == null)
throw ColumnException.WrongColumnName(kv.Key.ToString());

var key = GetColumnOrThrow(kv.Key.ToString());
result.Insert(key, kv.Value);
}

Expand Down Expand Up @@ -653,10 +654,7 @@ private List<ValueTableSortRule> GetSortRules(string Columns)
if (description.Length > 2)
throw RuntimeException.InvalidNthArgumentValue(1);

var sortColumn = this.Columns.FindColumnByName(description[0]);
if (sortColumn == null)
throw ColumnException.WrongColumnName(description[0]);

var sortColumn = GetColumnOrThrow(description[0]);
var rule = new ValueTableSortRule
{
Column = sortColumn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ namespace OneScript.StandardLibrary.Collections.ValueTable
public class ValueTableColumn : AutoContext<ValueTableColumn>
{
private string _title;
private string _name;
private TypeDescription _valueType;
private string _name;
private readonly TypeDescription _valueType;
private readonly WeakReference _owner;
private readonly int _id;

public ValueTableColumn(ValueTableColumnCollection owner, int id, string name, string title, TypeDescription type, int width)

private int _indicesCount = 0;
public bool IsIndexable => _indicesCount != 0;
public void AddToIndex() { _indicesCount++; }
public void DeleteFromIndex() { if (_indicesCount != 0) _indicesCount--; }
Comment on lines +27 to +30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Asymmetric AddToIndex/DeleteFromIndex can cause index-count drift.

AddToIndex() unconditionally increments _indicesCount, but DeleteFromIndex() stops at zero. If these methods are not called in perfectly matching pairs—for example, if an index is added twice to the same column or deletion fails midway—the count can drift upward. This will cause IsIndexable to remain true even after all legitimate indexes are removed, leading to unnecessary index operations on every row update (as seen in ValueTableRow.Set() at lines 89-93).

Additionally, the increment and decrement operations are not thread-safe, which could cause race conditions if indexes are modified concurrently.

🔎 Proposed fix: Add symmetry validation and thread safety
 private int _indicesCount = 0;
+private readonly object _lockObject = new object();
+
 public bool IsIndexable => _indicesCount != 0;
-public void AddToIndex() { _indicesCount++; }
-public void DeleteFromIndex() { if (_indicesCount != 0) _indicesCount--; }
+
+public void AddToIndex() 
+{ 
+    lock (_lockObject)
+    {
+        _indicesCount++; 
+    }
+}
+
+public void DeleteFromIndex() 
+{ 
+    lock (_lockObject)
+    {
+        if (_indicesCount == 0)
+            throw new InvalidOperationException("Cannot delete from index: no indexes are tracking this column.");
+        _indicesCount--; 
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private int _indicesCount = 0;
public bool IsIndexable => _indicesCount != 0;
public void AddToIndex() { _indicesCount++; }
public void DeleteFromIndex() { if (_indicesCount != 0) _indicesCount--; }
private int _indicesCount = 0;
private readonly object _lockObject = new object();
public bool IsIndexable => _indicesCount != 0;
public void AddToIndex()
{
lock (_lockObject)
{
_indicesCount++;
}
}
public void DeleteFromIndex()
{
lock (_lockObject)
{
if (_indicesCount == 0)
throw new InvalidOperationException("Cannot delete from index: no indexes are tracking this column.");
_indicesCount--;
}
}
🤖 Prompt for AI Agents
In src/OneScript.StandardLibrary/Collections/ValueTable/ValueTableColumn.cs
around lines 27-30, the AddToIndex/DeleteFromIndex pair can let _indicesCount
drift upward and is not thread-safe; change both methods to use thread-safe
atomic operations (System.Threading.Interlocked) and enforce symmetry by
preventing _indicesCount from going negative or growing incorrectly: use
Interlocked.Increment in AddToIndex; in DeleteFromIndex use an atomic loop
(CompareExchange) to decrement only if current > 0 or perform
Interlocked.Decrement and if the result is negative, correct it back to zero
with Interlocked.Exchange (and optionally log the unexpected state). This
ensures counts remain non-negative and safe under concurrency so IsIndexable
reflects the true state.


public ValueTableColumn(ValueTableColumnCollection owner, string name, string title, TypeDescription type, int width)
{
_name = name;
_title = title;
_valueType = type ?? new TypeDescription();
Width = width;
_id = id;

_owner = new WeakReference(owner);
}
Expand Down Expand Up @@ -65,7 +68,6 @@ public string Name
_title = value;

_name = value;

}
}
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,40 @@ namespace OneScript.StandardLibrary.Collections.ValueTable
[ContextClass("КоллекцияКолонокТаблицыЗначений", "ValueTableColumnCollection")]
public class ValueTableColumnCollection : AutoContext<ValueTableColumnCollection>, ICollectionContext<ValueTableColumn>, IDebugPresentationAcceptor
{
private static readonly StringComparer _namesComparer = StringComparer.OrdinalIgnoreCase;

private readonly List<ValueTableColumn> _columns = new List<ValueTableColumn>();
private readonly StringComparer _namesComparer = StringComparer.OrdinalIgnoreCase;
private readonly ValueTable _owner;
private int maxColumnId = 0;

public ValueTableColumnCollection(ValueTable owner)
{
_owner = owner;
}

public ValueTableColumn AddUnchecked(string name, string title, TypeDescription type = null)
{
var column = new ValueTableColumn(this, name, title, type, 0);
_columns.Add(column);
return column;
}

public ValueTableColumn AddUnchecked(string name, string title = null)
{
var column = new ValueTableColumn(this, name, title ?? name, null, 0);
_columns.Add(column);
return column;
}


private void CheckColumnName(string name)
{
if (!Utils.IsValidIdentifier(name))
throw ColumnException.WrongColumnName(name);

if (FindColumnByName(name) != null)
throw ColumnException.DuplicatedColumnName(name);
}

/// <summary>
/// Добавляет колонку в таблицу значений
/// </summary>
Expand All @@ -46,13 +70,9 @@ public ValueTableColumnCollection(ValueTable owner)
[ContextMethod("Добавить", "Add")]
public ValueTableColumn Add(string name, TypeDescription type = null, string title = null, int width = 0)
{
if (!Utils.IsValidIdentifier(name))
throw ColumnException.WrongColumnName(name);
CheckColumnName(name);

if (FindColumnByName(name) != null)
throw ColumnException.DuplicatedColumnName(name);

var column = new ValueTableColumn(this, ++maxColumnId, name, title, type, width);
var column = new ValueTableColumn(this, name, title, type, width);
_columns.Add(column);

return column;
Expand All @@ -70,13 +90,9 @@ public ValueTableColumn Add(string name, TypeDescription type = null, string tit
[ContextMethod("Вставить", "Insert")]
public ValueTableColumn Insert(int index, string name, TypeDescription type = null, string title = null, int width = 0)
{
if (!Utils.IsValidIdentifier(name))
throw ColumnException.WrongColumnName(name);

if (FindColumnByName(name) != null)
throw ColumnException.DuplicatedColumnName(name);
CheckColumnName(name);

ValueTableColumn column = new ValueTableColumn(this, ++maxColumnId, name, title, type, width);
ValueTableColumn column = new ValueTableColumn(this, name, title, type, width);
_columns.Insert(index, column);

return column;
Expand Down Expand Up @@ -113,10 +129,7 @@ public int Count()
[ContextMethod("Найти", "Find")]
public IValue Find(string name)
{
ValueTableColumn Column = FindColumnByName(name);
if (Column == null)
return ValueFactory.Create();
return Column;
return FindColumnByName(name) ?? ValueFactory.Create();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@ public ValueTableRow(ValueTable owner)
_owner = owner;
}

public int Count()
{
return _owner.Columns.Count();
}

public int Count() => _owner.Columns.Count();

public int Count(IBslProcess process) => Count();

/// <summary>
Expand Down Expand Up @@ -89,9 +86,14 @@ public void Set(IValue index, IValue value)

public void Set(ValueTableColumn column, IValue value)
{
_owner.Indexes.ElementRemoved(this);
_data[column] = column.ValueType.AdjustValue(value);
_owner.Indexes.ElementAdded(this);
if (column.IsIndexable)
{
_owner.Indexes.ElementRemoved(this);
_data[column] = column.ValueType.AdjustValue(value);
_owner.Indexes.ElementAdded(this);
}
else
_data[column] = column.ValueType.AdjustValue(value);
}

public void OnOwnerColumnRemoval(ValueTableColumn column)
Expand Down
Loading