mxaccesscli: support bulk array writes via <arrayAttr>[]
WriteCommand now accepts multiple positional values when the tag reference ends with '[]', bundling them into a strongly-typed array (string[], int[], bool[], etc.) before passing to MxAccess.Write. The CLR marshals the array to a COM SAFEARRAY of the matching VARTYPE, which is the shape MxAccess expects for an array attribute. Verified live on a 50-slot String[] (MESReceiver_001.MoveInPartNumbers): write 50 distinct strings A1..A50 -> ok, MxCategoryOk read [] -> ['A1','A2', ..., 'A50'] Plus a guardrail: passing multiple values without the '[]' suffix exits 2 with a clear error so a typo can't accidentally write only the first element of an indexed reference. Critical finding documented in docs/usage.md: **a bulk write resizes the array to the count provided.** Writing 25 values into a 50-slot array leaves the array at 25 elements; the trailing 25 are deallocated, not zero-filled. Verified by 50 -> 25 -> 50 round-trip on the same attribute. Discover the runtime length via 'mxa read <attr>[]' or the configured length via grdb's attributes.sql array_dimension column. Type matrix in docs/usage.md updated: - Bulk array via '[]' - read ✅ + write ✅ - Bare reference (no brackets) - read ❌ + write ❌ - Element via '[N]' - unchanged ValueCoercion.cs: adds CoerceArray(IReadOnlyList<string>, typeHint) that produces strongly-typed arrays. Default element type is inferred from the first value when --type is unspecified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MxAccess.Cli.Mx
|
||||
@@ -38,6 +39,64 @@ namespace MxAccess.Cli.Mx
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a strongly-typed array (`bool[]`, `int[]`, `string[]`, …) from
|
||||
/// a list of raw string values. The CLR marshals strongly-typed arrays
|
||||
/// to a COM SAFEARRAY of the matching VARTYPE, which is what MxAccess
|
||||
/// expects for an array attribute write.
|
||||
public static object CoerceArray(IReadOnlyList<string> values, string typeHint)
|
||||
{
|
||||
if (values == null) throw new ArgumentNullException(nameof(values));
|
||||
|
||||
// Default array element type when --type isn't specified: peek at
|
||||
// the first value's inferred type and assume the rest match. For
|
||||
// an empty list, fall back to string[].
|
||||
string effectiveHint = typeHint;
|
||||
if (string.IsNullOrEmpty(effectiveHint))
|
||||
{
|
||||
if (values.Count == 0) effectiveHint = "string";
|
||||
else
|
||||
{
|
||||
var first = InferAndCoerce(values[0]);
|
||||
effectiveHint = first switch
|
||||
{
|
||||
bool _ => "bool",
|
||||
int _ => "int",
|
||||
long _ => "long",
|
||||
double _ => "double",
|
||||
_ => "string",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (effectiveHint.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "bool": return Convert<bool>(values, s => ParseBool(s));
|
||||
case "byte": return Convert<byte>(values, s => byte.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "short": return Convert<short>(values, s => short.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "int":
|
||||
case "int32": return Convert<int>(values, s => int.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "long":
|
||||
case "int64": return Convert<long>(values, s => long.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "float":
|
||||
case "single": return Convert<float>(values, s => float.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "double": return Convert<double>(values, s => double.Parse(s, CultureInfo.InvariantCulture));
|
||||
case "string": return Convert<string>(values, s => s);
|
||||
case "time":
|
||||
case "datetime":
|
||||
return Convert<DateTime>(values, s => DateTime.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal));
|
||||
default:
|
||||
throw new ArgumentException(
|
||||
$"Unknown --type '{effectiveHint}'. Supported: bool, byte, short, int, long, float, double, string, datetime.");
|
||||
}
|
||||
}
|
||||
|
||||
private static T[] Convert<T>(IReadOnlyList<string> raw, Func<string, T> parse)
|
||||
{
|
||||
var arr = new T[raw.Count];
|
||||
for (int i = 0; i < raw.Count; i++) arr[i] = parse(raw[i]);
|
||||
return arr;
|
||||
}
|
||||
|
||||
private static object InferAndCoerce(string raw)
|
||||
{
|
||||
if (ParseBool(raw, out var b)) return b;
|
||||
|
||||
Reference in New Issue
Block a user