@@ -6,33 +6,55 @@ namespace SqlServerSimulator.Parser.Expressions;
66/// <summary>
77/// Instance-method call on an <c>xml</c> value: <c>expr.value(…)</c>,
88/// <c>expr.nodes(…)</c>, <c>expr.query(…)</c>, <c>expr.exist(…)</c>, or
9- /// <c>expr.modify(…)</c>. Parses cleanly (so CREATE VIEW / CREATE
10- /// PROCEDURE bodies that reference XML methods can be stored verbatim);
11- /// raises <see cref="NotSupportedException"/> at <see cref="Run"/> time
12- /// with a wording naming the method, matching the skip-with-diagnostic
13- /// stance documented in <c>docs/claude/xml.md</c>.
9+ /// <c>expr.modify(…)</c>.
1410/// </summary>
1511/// <remarks>
16- /// The closed accept-list (<c>value</c>, <c>nodes</c>, <c>query</c>,
17- /// <c>exist</c>, <c>modify</c>) is checked before falling through to the
18- /// existing multipart-Reference path so a column literally named (e.g.)
19- /// <c>value</c> followed by <c>.MethodName(...)</c> won't collide.
12+ /// <para>
13+ /// <c>value</c> evaluates its XQuery path against the target xml through
14+ /// <see cref="XmlQueryEngine"/> and casts the selected node's string value to
15+ /// the requested SQL type (its second argument, a string literal). <c>nodes</c>
16+ /// produces a rowset and is only valid in a FROM / APPLY source position; the
17+ /// parser (<see cref="Selection"/>) intercepts the parsed <see cref="XmlMethodCall"/>
18+ /// there via <see cref="IsNodes"/> / <see cref="Target"/> / <see cref="XQuery"/>
19+ /// and builds a correlated source — reaching <see cref="Run"/> for <c>nodes</c>
20+ /// means it appeared in scalar position, which is unsupported.
21+ /// </para>
22+ /// <para>
23+ /// <c>query</c> / <c>exist</c> / <c>modify</c> parse cleanly (so CREATE VIEW /
24+ /// CREATE PROCEDURE bodies referencing them store verbatim) but raise
25+ /// <see cref="NotSupportedException"/> at <see cref="Run"/> time, the
26+ /// skip-with-diagnostic stance documented in <c>docs/claude/xml.md</c>.
27+ /// </para>
2028/// </remarks>
2129internal sealed class XmlMethodCall : Expression
2230{
23- private readonly Expression target ;
31+ /// <summary>The xml-valued expression the method is invoked on.</summary>
32+ public readonly Expression Target ;
33+
2434 private readonly string methodName ;
35+ private readonly string ? xquery ;
36+ private readonly SqlType valueType ;
37+ private readonly int ? valueMaxLength ;
2538
26- private XmlMethodCall ( Expression target , string methodName )
39+ private XmlMethodCall ( Expression target , string methodName , string ? xquery , SqlType valueType , int ? valueMaxLength )
2740 {
28- this . target = target ;
41+ this . Target = target ;
2942 this . methodName = methodName ;
43+ this . xquery = xquery ;
44+ this . valueType = valueType ;
45+ this . valueMaxLength = valueMaxLength ;
3046 }
3147
48+ /// <summary>True when this is a <c>.nodes()</c> call (rowset-producing).</summary>
49+ public bool IsNodes => this . methodName . Equals ( "nodes" , StringComparison . Ordinal ) ;
50+
51+ /// <summary>The XQuery path argument (prolog + body), captured at parse time.</summary>
52+ public string XQuery => this . xquery ?? throw new InvalidOperationException ( "XML method has no captured XQuery argument." ) ;
53+
3254 /// <summary>
3355 /// Returns true if <paramref name="name"/> matches one of the five XML
3456 /// instance method names. Used by the expression parser to take the
35- /// throws-at-execute path instead of multipart-reference dispatch.
57+ /// method-call path instead of multipart-reference dispatch.
3658 /// </summary>
3759 public static bool IsKnownMethodName ( string name ) =>
3860 name . Equals ( "value" , StringComparison . Ordinal )
@@ -43,47 +65,146 @@ public static bool IsKnownMethodName(string name) =>
4365
4466 /// <summary>
4567 /// Parses <c>expr.MethodName(args)</c>. Cursor enters on <c>(</c>; on
46- /// return cursor sits on the closing <c>)</c>. Arguments parse fully
47- /// (so name resolution surfaces eagerly per the simulator's idiom)
48- /// but they're discarded — runtime evaluation throws.
68+ /// return cursor sits on the closing <c>)</c>. The first argument (XQuery
69+ /// path) and, for <c>value</c>, the second (target SQL type) are captured
70+ /// as compile-time string literals; a non-literal argument raises
71+ /// <see cref="NotSupportedException"/> (dynamic XQuery isn't modeled).
4972 /// </summary>
5073 public static XmlMethodCall Parse ( Expression target , string methodName , ParserContext context )
5174 {
75+ var isValue = methodName . Equals ( "value" , StringComparison . Ordinal ) ;
76+ var isNodesOrValueOrQueryOrExist = isValue
77+ || methodName . Equals ( "nodes" , StringComparison . Ordinal )
78+ || methodName . Equals ( "query" , StringComparison . Ordinal )
79+ || methodName . Equals ( "exist" , StringComparison . Ordinal ) ;
80+
5281 context . MoveNextRequired ( ) ;
82+ string ? xquery = null ;
83+ SqlType valueType = SqlType . Xml ;
84+ int ? valueMaxLength = null ;
5385 if ( context . Token is not Operator { Character : ')' } )
5486 {
55- _ = Expression . Parse ( context ) ;
87+ var firstArg = Expression . Parse ( context ) ;
88+ if ( isNodesOrValueOrQueryOrExist )
89+ xquery = ConstantString ( firstArg , context , "XML method path" ) ;
90+
5691 while ( context . Token is Operator { Character : ',' } )
5792 {
5893 context . MoveNextRequired ( ) ;
59- _ = Expression . Parse ( context ) ;
94+ var nextArg = Expression . Parse ( context ) ;
95+ if ( isValue )
96+ ( valueType , valueMaxLength ) = ResolveValueType ( ConstantString ( nextArg , context , "value() target type" ) , context . Batch ) ;
6097 }
6198 if ( context . Token is not Operator { Character : ')' } )
6299 throw SimulatedSqlException . SyntaxErrorNear ( context ) ;
63100 }
64- return new XmlMethodCall ( target , methodName ) ;
101+ return new XmlMethodCall ( target , methodName , xquery , valueType , valueMaxLength ) ;
65102 }
66103
67- public override SqlValue Run ( RuntimeContext runtime ) =>
68- throw new NotSupportedException (
69- $ "XML instance method '.{ this . methodName } ()' is not modeled.") ;
104+ public override SqlValue Run ( RuntimeContext runtime )
105+ {
106+ // .nodes() is rowset-producing (handled in FROM/APPLY parse, never
107+ // here) and .modify() is XML-DML, neither is reachable as a scalar.
108+ if ( this . methodName . Equals ( "nodes" , StringComparison . Ordinal ) || this . methodName . Equals ( "modify" , StringComparison . Ordinal ) )
109+ throw new NotSupportedException ( $ "XML instance method '.{ this . methodName } ()' is not modeled.") ;
110+
111+ var input = this . Target . Run ( runtime ) ;
112+ switch ( this . methodName )
113+ {
114+ case "exist" :
115+ return input . IsNull ? SqlValue . Null ( SqlType . Bit ) : SqlValue . FromBoolean ( XmlQueryEngine . EvaluateExists ( input . AsString , this . xquery ! ) ) ;
116+ case "query" :
117+ return input . IsNull ? SqlValue . Null ( SqlType . Xml ) : SqlValue . FromXml ( XmlQueryEngine . EvaluateQuery ( input . AsString , this . xquery ! ) ) ;
118+ default :
119+ if ( input . IsNull )
120+ return SqlValue . Null ( this . valueType ) ;
121+ var selected = XmlQueryEngine . EvaluateScalar ( input . AsString , this . xquery ! ) ;
122+ return selected is null
123+ ? SqlValue . Null ( this . valueType )
124+ : Cast . ApplyCoercion ( SqlValue . FromString ( SqlType . NVarchar , selected ) , this . valueType , this . valueMaxLength ) ;
125+ }
126+ }
70127
71128 /// <summary>
72- /// Static result type, used by projection schema inference. Returns
73- /// <c>xml</c> for the methods that produce xml (<c>query</c>,
74- /// <c>nodes</c>) and <c>bit</c> for <c>exist</c>; <c>value</c> returns
75- /// the requested target type but the simulator stubs it as
76- /// nvarchar(MAX) since we never actually evaluate it. <c>modify</c>
77- /// is statement-level in real SQL Server and has no result type;
78- /// surfaces as xml here for static-typing safety since it can't be
79- /// reached at execute anyway.
129+ /// Static result type, used by projection schema inference: <c>value</c>
130+ /// returns its requested target type; <c>exist</c> returns <c>bit</c>;
131+ /// <c>nodes</c> / <c>query</c> / <c>modify</c> surface as <c>xml</c>.
80132 /// </summary>
81133 public override SqlType GetSqlType ( BatchContext batch , Func < MultiPartName , SqlType > resolveColumnType ) =>
82- this . methodName . Equals ( "exist " , StringComparison . Ordinal )
83- ? SqlType . Bit
84- : this . methodName . Equals ( "value " , StringComparison . Ordinal )
85- ? NVarcharSqlType . Get ( - 1 , batch . CurrentDatabase . Collation , Coercibility . CoercibleDefault )
134+ this . methodName . Equals ( "value " , StringComparison . Ordinal )
135+ ? this . valueType
136+ : this . methodName . Equals ( "exist " , StringComparison . Ordinal )
137+ ? SqlType . Bit
86138 : SqlType . Xml ;
87139
88- internal override string DebugDisplay ( ) => $ "({ this . target . DebugDisplay ( ) } ).{ this . methodName } (…)";
140+ internal override string DebugDisplay ( ) => $ "({ this . Target . DebugDisplay ( ) } ).{ this . methodName } (…)";
141+
142+ /// <summary>
143+ /// Evaluates <paramref name="argument"/> against an empty resolver to pull
144+ /// out its compile-time string value; a column / variable / runtime
145+ /// reference surfaces as <see cref="NotSupportedException"/>.
146+ /// </summary>
147+ private static string ConstantString ( Expression argument , ParserContext context , string role )
148+ {
149+ try
150+ {
151+ var value = argument . Run ( new RuntimeContext ( _ => throw new InvalidOperationException ( ) , context . Batch ) ) ;
152+ if ( ! value . IsNull && SqlType . IsStringCategory ( value . Type ) )
153+ return value . AsString ;
154+ }
155+ catch ( InvalidOperationException )
156+ {
157+ // Falls through to the unsupported-shape throw below.
158+ }
159+ throw new NotSupportedException ( $ "A non-literal { role } argument to an XML method is not modeled.") ;
160+ }
161+
162+ /// <summary>
163+ /// Resolves a <c>value()</c> target-type literal (e.g. <c>nvarchar(30)</c>,
164+ /// <c>money</c>, <c>decimal(9, 4)</c>, <c>integer</c>) into a
165+ /// <see cref="SqlType"/> + max-length by re-tokenizing the literal and
166+ /// reusing <see cref="SqlType.GetByName"/>. <c>integer</c> is mapped to
167+ /// <c>int</c> (an XQuery type synonym <see cref="SqlType.GetByName"/>
168+ /// doesn't itself accept).
169+ /// </summary>
170+ private static ( SqlType Type , int ? MaxLength ) ResolveValueType ( string spec , BatchContext batch )
171+ {
172+ var collation = batch . CurrentDatabase . Collation ;
173+ var index = 0 ;
174+ Token ? NextToken ( )
175+ {
176+ Token ? token ;
177+ do
178+ {
179+ token = Tokenizer . NextToken ( spec , ref index , collation ) ;
180+ }
181+ while ( token is Whitespace ) ;
182+ return token ;
183+ }
184+
185+ if ( NextToken ( ) is not Name typeName )
186+ throw new NotSupportedException ( $ "Unrecognized XML value() target type '{ spec } '.") ;
187+ if ( typeName . Span . Equals ( "integer" , StringComparison . OrdinalIgnoreCase ) )
188+ return ( SqlType . Int32 , null ) ;
189+
190+ int ? declaredMaxLength = null ;
191+ int ? declaredScale = null ;
192+ if ( NextToken ( ) is Operator { Character : '(' } )
193+ {
194+ declaredMaxLength = NextToken ( ) switch
195+ {
196+ Numeric { Value : { IsNull : false } length } => length . AsInt32 ,
197+ UnquotedString { ContextualKeyword : ContextualKeyword . Max } => SqlType . MaxLengthSentinel ,
198+ _ => throw new NotSupportedException ( $ "Unrecognized XML value() target type '{ spec } '.") ,
199+ } ;
200+ if ( NextToken ( ) is Operator { Character : ',' } )
201+ {
202+ if ( NextToken ( ) is not Numeric { Value : { IsNull : false } scale } )
203+ throw new NotSupportedException ( $ "Unrecognized XML value() target type '{ spec } '.") ;
204+ declaredScale = scale . AsInt32 ;
205+ _ = NextToken ( ) ;
206+ }
207+ }
208+ return SqlType . GetByName ( typeName , declaredMaxLength , declaredScale , 1 , columnName : null ) ;
209+ }
89210}
0 commit comments