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,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
@@ -9,16 +11,16 @@ using MxAccess.Cli.Output;
|
||||
|
||||
namespace MxAccess.Cli.Commands
|
||||
{
|
||||
[Command("write", Description = "Write a value to a tag and wait for OnWriteComplete.")]
|
||||
[Command("write", Description = "Write a scalar or array value to a tag and wait for OnWriteComplete.")]
|
||||
public sealed class WriteCommand : ICommand
|
||||
{
|
||||
[CommandParameter(0, Name = "tag", Description = "Tag reference to write to.")]
|
||||
[CommandParameter(0, Name = "tag", Description = "Tag reference to write to. Use '<obj>.<attr>[]' to write a whole array.")]
|
||||
public string Tag { get; init; }
|
||||
|
||||
[CommandParameter(1, Name = "value", Description = "Value to write. Inferred as bool / int / double / string unless --type is set.")]
|
||||
public string RawValue { get; init; }
|
||||
[CommandParameter(1, Name = "values", Description = "Value(s) to write. One value for a scalar tag; multiple values for an array tag (the count must match the configured array dimension). Inferred as bool / int / double / string unless --type is set.")]
|
||||
public IReadOnlyList<string> Values { get; init; }
|
||||
|
||||
[CommandOption("type", Description = "Force the .NET type used for the value: bool, byte, short, int, long, float, double, string, datetime.")]
|
||||
[CommandOption("type", Description = "Force the .NET type used for the value(s): bool, byte, short, int, long, float, double, string, datetime.")]
|
||||
public string TypeHint { get; init; }
|
||||
|
||||
[CommandOption("timeout", 't', Description = "Seconds to wait for OnWriteComplete (and for the initial OnDataChange resolving the type).")]
|
||||
@@ -37,19 +39,32 @@ namespace MxAccess.Cli.Commands
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Tag))
|
||||
throw new CommandException("Tag reference is required.", 2);
|
||||
if (RawValue == null)
|
||||
throw new CommandException("Value is required.", 2);
|
||||
if (Values == null || Values.Count == 0)
|
||||
throw new CommandException("At least one value is required.", 2);
|
||||
if (TimeoutSeconds <= 0)
|
||||
throw new CommandException("--timeout must be positive.", 2);
|
||||
|
||||
var coerced = ValueCoercion.Coerce(RawValue, TypeHint);
|
||||
// Decide scalar vs. array. The tag form `<obj>.<attr>[]` is the
|
||||
// explicit array reference for whole-array writes (matches the
|
||||
// read-side bracket convention). Multiple values without `[]`
|
||||
// is an error: an indexed `[N]` reference takes one value.
|
||||
bool isArrayWrite = Tag.EndsWith("[]", StringComparison.Ordinal);
|
||||
if (!isArrayWrite && Values.Count > 1)
|
||||
throw new CommandException(
|
||||
"Multiple values supplied for a non-array reference. Use '<tag>[]' to write a whole array, or supply a single value for an indexed / scalar tag.", 2);
|
||||
|
||||
object coerced = isArrayWrite
|
||||
? ValueCoercion.CoerceArray(Values, TypeHint)
|
||||
: ValueCoercion.Coerce(Values[0], TypeHint);
|
||||
|
||||
var query = new
|
||||
{
|
||||
command = "write",
|
||||
tag = Tag,
|
||||
array = isArrayWrite,
|
||||
value = coerced,
|
||||
type = string.IsNullOrEmpty(TypeHint) ? coerced.GetType().Name : TypeHint,
|
||||
count = Values.Count,
|
||||
timeout_s = TimeoutSeconds,
|
||||
user_id = UserId,
|
||||
client = ClientName,
|
||||
@@ -61,17 +76,29 @@ namespace MxAccess.Cli.Commands
|
||||
{
|
||||
item = session.AddItem(Tag);
|
||||
|
||||
// Advise + wait for first OnDataChange to ensure the proxy has the
|
||||
// attribute type / data quality resolved. Calling Write before
|
||||
// resolution returns ArgumentException "Value does not fall within
|
||||
// the expected range".
|
||||
// Advise + wait for first OnDataChange to ensure the proxy has
|
||||
// the attribute type / data quality resolved. Without this the
|
||||
// first Write() throws ArgumentException "Value does not fall
|
||||
// within the expected range" because the proxy doesn't yet
|
||||
// know the destination type.
|
||||
//
|
||||
// Caveat: a bare-array reference (no brackets) will return
|
||||
// MxCategoryCommunicationError, Detail=1003 here — same as on
|
||||
// a `read` of that form. Tag the user-facing error so the
|
||||
// failure mode is recognizable.
|
||||
item.Advise();
|
||||
var resolveTimeout = TimeSpan.FromSeconds(TimeoutSeconds);
|
||||
if (!session.WaitForUpdate(
|
||||
u => u.Kind == MxUpdateKind.DataChange && u.ItemHandle == item.Handle,
|
||||
resolveTimeout, out _))
|
||||
resolveTimeout, out var resolveUpdate))
|
||||
{
|
||||
EmitFailure(console, query, "timeout-resolving-type");
|
||||
EmitFailure(console, query, "timeout-resolving-type", Array.Empty<MxStatusInfo>());
|
||||
Environment.ExitCode = 1;
|
||||
return default;
|
||||
}
|
||||
if (!resolveUpdate.IsOk)
|
||||
{
|
||||
EmitFailure(console, query, "type-resolution-failed", resolveUpdate.Statuses);
|
||||
Environment.ExitCode = 1;
|
||||
return default;
|
||||
}
|
||||
@@ -111,12 +138,15 @@ namespace MxAccess.Cli.Commands
|
||||
}
|
||||
else if (ok)
|
||||
{
|
||||
console.Output.WriteLine($"[OK ] write {Tag} = {coerced}");
|
||||
var rendered = isArrayWrite
|
||||
? $"[{string.Join(", ", Values)}] ({Values.Count} elements)"
|
||||
: Values[0];
|
||||
console.Output.WriteLine($"[OK ] write {Tag} = {rendered}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var err = (string)((dynamic)results[0]).error ?? "unknown";
|
||||
console.Error.WriteLine($"[ERR] write {Tag} = {coerced}: {err}");
|
||||
console.Error.WriteLine($"[ERR] write {Tag}: {err}");
|
||||
}
|
||||
|
||||
if (!ok) Environment.ExitCode = 1;
|
||||
@@ -128,12 +158,12 @@ namespace MxAccess.Cli.Commands
|
||||
return default;
|
||||
}
|
||||
|
||||
private void EmitFailure(IConsole console, object query, string error)
|
||||
private void EmitFailure(IConsole console, object query, string error, MxStatusInfo[] statuses)
|
||||
{
|
||||
if (LlmJson)
|
||||
{
|
||||
Envelope.Write(console, query, ok: false,
|
||||
results: new object[] { new { tag = Tag, ok = false, error, statuses = Array.Empty<MxStatusInfo>() } });
|
||||
results: new object[] { new { tag = Tag, ok = false, error, statuses } });
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user