Skip to content

Commit 8256541

Browse files
committed
Trig / power scalars: SIN / COS / TAN / ASIN / ACOS / ATAN / ATN2 / COT / SQUARE / PI all return float regardless of input numeric category, with NULL propagation, Msg 174 on wrong arg count, and Msg 3623 on domain errors (ASIN/ACOS out of [-1, 1], COT(0), ATN2(0, 0)) — eight unary kinds bundle in TrigFunction.cs via TrigKind discriminator, Atn2.cs / Pi.cs standalone. DEGREES / RADIANS preserve input type except decimal(p, s) → decimal(38, max(s, 18)); integer arm truncates toward zero with Msg 8115 on overflow, decimal arm uses a 28-digit DecimalPi constant in (input * num) / denom order for trailing-digit fidelity. SQUARE overflow → Msg 8115 float.
1 parent 67b9ac2 commit 8256541

10 files changed

Lines changed: 647 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ Integer / money operands canonicalize before formulas apply (bit→(1,0)…bigin
197197
`SqlType.Promote` (joint-envelope, `scale = max(s1, s2); precision = min(38, max(p1-s1, p2-s2) + scale)`) stays the right rule for non-arithmetic uses.
198198

199199
### Math scalar functions
200-
`ABS`, `ROUND` (2- and 3-arg, half-away-from-zero plus truncate mode), `FLOOR`, `CEILING`, `POWER`, `SQRT`, `SIGN`, `LOG` (1- and 2-arg), `EXP`, `LOG10`. EF Core 10 emits all from natural `Math.X` LINQ; `Math.Truncate(x)``ROUND(x, 0, 1)`.
200+
`ABS`, `ROUND` (2- and 3-arg, half-away-from-zero plus truncate mode), `FLOOR`, `CEILING`, `POWER`, `SQRT`, `SIGN`, `LOG` (1- and 2-arg), `EXP`, `LOG10`, the trig family (`SIN` / `COS` / `TAN` / `ASIN` / `ACOS` / `ATAN` / `ATN2` / `COT`), `PI`, `DEGREES` / `RADIANS`, `SQUARE`. EF Core 10 emits all from natural `Math.X` LINQ; `Math.Truncate(x)``ROUND(x, 0, 1)`; `Math.Atan2``ATN2`.
201201

202202
**Type-widening rule** (shared across `ABS` / `FLOOR` / `CEILING` / `ROUND` / `SIGN` / `POWER`'s first-arg type — probe-confirmed via `SELECT INTO #t` + `tempdb.information_schema.columns`):
203203
- `tinyint` / `smallint``int`
@@ -209,6 +209,10 @@ Integer / money operands canonicalize before formulas apply (bit→(1,0)…bigin
209209

210210
Errors: `SQRT(neg)` / `LOG(<= 0)` / `LOG10(<= 0)` / `LOG(x, 1)` / `POWER(neg, frac)` → Msg 3623. `POWER(0, neg)` → Msg 8134. `EXP` overflow → Msg 8115 float. `ABS(int.MinValue)` / `ABS(bigint.MinValue)` → Msg 8115 with the result type's family in the data-type slot (smallint overflow is absorbed by int widening). `POWER` int-result overflow → Msg 232.
211211

212+
**Trig / power family** (`SIN` / `COS` / `TAN` / `ASIN` / `ACOS` / `ATAN` / `ATN2` / `COT` / `PI` / `SQUARE`): always return `float` regardless of input numeric category — probe-confirmed against SQL Server 2025 (2026-05-10) for int / bigint / decimal / money / float / real / bit / smallmoney inputs. NULL on any operand propagates as typed-NULL float. Domain errors map to Msg 3623: `ASIN` / `ACOS` argument outside `[-1, 1]`, `COT(0)`, `ATN2(0, 0)` (the last diverges from .NET's `Math.Atan2(0, 0) = 0`). `SQUARE` overflow → Msg 8115 float. Wrong arg count → Msg 174 (`"The {lower-name} function requires {N} argument(s)."`) — the eight unary kinds bundle into one `TrigFunction.cs` with a `TrigKind` discriminator since they share NULL / Msg-174 / Msg-3623 paths; `Atn2.cs` (binary) and `Pi.cs` (zero-arg, parens required — `pi(1)` raises Msg 174 not Msg 102) are standalone.
213+
214+
**`DEGREES` / `RADIANS`** are type-preserving with one tweak relative to the shared widening rule above: `decimal(p, s)` widens to `decimal(38, max(s, 18))` rather than preserving — probe-confirmed (`degrees(decimal(10, 2)) → decimal(38, 18)`, `degrees(decimal(38, 30)) → decimal(38, 30)`). Other categories follow `MathScalars.WidenForResult`. The integer arm computes `(double)input * (180/pi)` (or `pi/180`) and truncates toward zero — `DEGREES(360)` → `20626` from `20626.48...`; out-of-range integer results raise Msg 8115 with the result type's family name. The decimal arm uses a 28-digit `DecimalPi` constant in evaluation order `(input * 180m) / DecimalPi` to align trailing digits with SQL Server's emission. .NET decimal's 28-digit precision cap means scale > 28 results land at scale 28 with the type still declaring its full 38-scale envelope (pre-existing decimal-storage quirk). The shared `DegreesRadians.cs` helper holds the integer-arm bound check and decimal-arm rounding logic; the widening-rule divergence stays inline in `Degrees.cs` / `Radians.cs`.
215+
212216
**`Math.Sign(decimal)` doesn't work end-to-end against either real SQL Server or the simulator** (probe-confirmed 2026-05-09): the LINQ method's CLR signature returns `int` but EF emits `SELECT SIGN([col])` and SQL Server's `SIGN(decimal)` returns decimal, so the reader-side cast throws. Same failure mode in both; not a fidelity bug. Apps using `Math.Sign` over an int column work fine. (This is the milestone where the simulator started finding upstream bugs.)
213217

214218
### Date scalar functions: `DATEPART` / `DATEADD` / `DATEDIFF` / `DATEDIFF_BIG`

SqlServerSimulator.Tests.EFCore/EFCoreMath.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,56 @@ public void Log10_FloatProjection()
144144
AreClose(1.0915m, (decimal)values[0], 0.001m);
145145
}
146146

147+
[TestMethod]
148+
public void Trig_SinCosTan_FloatProjection()
149+
{
150+
// Math.Sin / Cos / Tan emit SIN / COS / TAN with float operands.
151+
using var context = SeededProductsContext();
152+
var values = context.Products
153+
.OrderBy(p => p.Id)
154+
.Select(p => new
155+
{
156+
Sin = Math.Sin((double)p.Price),
157+
Cos = Math.Cos((double)p.Price),
158+
Tan = Math.Tan((double)p.Price),
159+
})
160+
.ToArray();
161+
AreClose((decimal)Math.Sin(12.35), (decimal)values[0].Sin, 0.001m);
162+
AreClose((decimal)Math.Cos(12.35), (decimal)values[0].Cos, 0.001m);
163+
AreClose((decimal)Math.Tan(12.35), (decimal)values[0].Tan, 0.001m);
164+
}
165+
166+
[TestMethod]
167+
public void Trig_AsinAcosAtan_FloatProjection()
168+
{
169+
// Math.Asin / Acos / Atan emit ASIN / ACOS / ATAN. Inputs constrained
170+
// to [-1, 1] for ASIN / ACOS to avoid Msg 3623.
171+
using var context = SeededProductsContext();
172+
var values = context.Products
173+
.Where(p => p.Id == 2)
174+
.Select(p => new
175+
{
176+
Asin = Math.Asin((double)p.Price),
177+
Acos = Math.Acos((double)p.Price),
178+
Atan = Math.Atan((double)p.Price),
179+
})
180+
.ToArray();
181+
AreClose((decimal)Math.Asin(0.5), (decimal)values[0].Asin, 0.001m);
182+
AreClose((decimal)Math.Acos(0.5), (decimal)values[0].Acos, 0.001m);
183+
AreClose((decimal)Math.Atan(0.5), (decimal)values[0].Atan, 0.001m);
184+
}
185+
186+
[TestMethod]
187+
public void Atan2_FloatProjection_EmitsAtn2()
188+
{
189+
using var context = SeededProductsContext();
190+
var values = context.Products
191+
.Where(p => p.Id == 1)
192+
.Select(p => Math.Atan2((double)p.Price, 1.0))
193+
.ToArray();
194+
AreClose((decimal)Math.Atan2(12.35, 1.0), (decimal)values[0], 0.001m);
195+
}
196+
147197
private static void AreClose(decimal expected, decimal actual, decimal tolerance)
148198
=> Assert.IsLessThanOrEqualTo(tolerance, Math.Abs(expected - actual), $"Expected {expected} ± {tolerance}, got {actual}");
149199
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
2+
using static SqlServerSimulator.TestHelpers;
3+
4+
namespace SqlServerSimulator;
5+
6+
[TestClass]
7+
public sealed class MathScalarTrigTests
8+
{
9+
[TestMethod]
10+
public void Sin_OfZero_ReturnsZero() => AreEqual(0.0, ExecuteScalar("select sin(cast(0 as float))"));
11+
12+
[TestMethod]
13+
public void Cos_OfZero_ReturnsOne() => AreEqual(1.0, ExecuteScalar("select cos(cast(0 as float))"));
14+
15+
[TestMethod]
16+
public void Tan_OfZero_ReturnsZero() => AreEqual(0.0, ExecuteScalar("select tan(cast(0 as float))"));
17+
18+
[TestMethod]
19+
public void Asin_OfOne_ReturnsHalfPi()
20+
=> AreEqual(Math.PI / 2, ExecuteScalar("select asin(cast(1 as float))"));
21+
22+
[TestMethod]
23+
public void Asin_OfNegativeOne_ReturnsNegativeHalfPi()
24+
=> AreEqual(-Math.PI / 2, ExecuteScalar("select asin(cast(-1 as float))"));
25+
26+
[TestMethod]
27+
public void Acos_OfOne_ReturnsZero() => AreEqual(0.0, ExecuteScalar("select acos(cast(1 as float))"));
28+
29+
[TestMethod]
30+
public void Acos_OfNegativeOne_ReturnsPi()
31+
=> AreEqual(Math.PI, ExecuteScalar("select acos(cast(-1 as float))"));
32+
33+
[TestMethod]
34+
public void Atan_OfOne_ReturnsQuarterPi()
35+
=> AreEqual(Math.PI / 4, ExecuteScalar("select atan(cast(1 as float))"));
36+
37+
[TestMethod]
38+
public void Atn2_OfOneOne_ReturnsQuarterPi()
39+
=> AreEqual(Math.PI / 4, ExecuteScalar("select atn2(cast(1 as float), cast(1 as float))"));
40+
41+
[TestMethod]
42+
public void Atn2_OfZeroZero_RaisesMsg3623()
43+
=> AssertSqlError("select atn2(cast(0 as float), cast(0 as float))", 3623, "An invalid floating point operation occurred.");
44+
45+
[TestMethod]
46+
public void Cot_OfOne_ReturnsCotangentOne()
47+
{
48+
var v = (double)ExecuteScalar("select cot(cast(1 as float))")!;
49+
IsLessThan(1e-12, Math.Abs(v - (1.0 / Math.Tan(1.0))));
50+
}
51+
52+
[TestMethod]
53+
public void Cot_OfZero_RaisesMsg3623()
54+
=> AssertSqlError("select cot(cast(0 as float))", 3623, "An invalid floating point operation occurred.");
55+
56+
[TestMethod]
57+
public void Pi_ReturnsPiAsFloat() => AreEqual(Math.PI, ExecuteScalar("select pi()"));
58+
59+
[TestMethod]
60+
public void Pi_WithArgument_RaisesMsg174()
61+
=> AssertSqlError("select pi(1)", 174, "The pi function requires 0 argument(s).");
62+
63+
[TestMethod]
64+
public void Sin_TooFewArgs_RaisesMsg174()
65+
=> AssertSqlError("select sin()", 174, "The sin function requires 1 argument(s).");
66+
67+
[TestMethod]
68+
public void Sin_TooManyArgs_RaisesMsg174()
69+
=> AssertSqlError("select sin(1, 2)", 174, "The sin function requires 1 argument(s).");
70+
71+
[TestMethod]
72+
public void Atn2_OneArg_RaisesMsg174()
73+
=> AssertSqlError("select atn2(1)", 174, "The atn2 function requires 2 argument(s).");
74+
75+
[TestMethod]
76+
public void Atn2_ThreeArgs_RaisesMsg174()
77+
=> AssertSqlError("select atn2(1, 2, 3)", 174, "The atn2 function requires 2 argument(s).");
78+
79+
[TestMethod]
80+
public void Asin_OutOfDomain_RaisesMsg3623()
81+
{
82+
AssertSqlError("select asin(cast(2 as float))", 3623, "An invalid floating point operation occurred.");
83+
AssertSqlError("select asin(cast(-2 as float))", 3623, "An invalid floating point operation occurred.");
84+
}
85+
86+
[TestMethod]
87+
public void Acos_OutOfDomain_RaisesMsg3623()
88+
{
89+
AssertSqlError("select acos(cast(2 as float))", 3623, "An invalid floating point operation occurred.");
90+
AssertSqlError("select acos(cast(-2 as float))", 3623, "An invalid floating point operation occurred.");
91+
}
92+
93+
[TestMethod]
94+
public void Sin_NullInput_PropagatesNull()
95+
=> AreEqual(DBNull.Value, ExecuteScalar("select sin(cast(null as float))"));
96+
97+
[TestMethod]
98+
public void Atn2_NullFirst_PropagatesNull()
99+
=> AreEqual(DBNull.Value, ExecuteScalar("select atn2(cast(null as float), cast(1 as float))"));
100+
101+
[TestMethod]
102+
public void Atn2_NullSecond_PropagatesNull()
103+
=> AreEqual(DBNull.Value, ExecuteScalar("select atn2(cast(1 as float), cast(null as float))"));
104+
105+
[TestMethod]
106+
public void Sin_AcceptsAnyNumericInput()
107+
{
108+
AreEqual(Math.Sin(1), ExecuteScalar("select sin(cast(1 as int))"));
109+
AreEqual(Math.Sin(1), ExecuteScalar("select sin(cast(1 as bigint))"));
110+
AreEqual(Math.Sin(1), ExecuteScalar("select sin(cast(1 as decimal(10,2)))"));
111+
AreEqual(Math.Sin(1), ExecuteScalar("select sin(cast(1 as money))"));
112+
}
113+
114+
[TestMethod]
115+
public void Square_OfTwoFloat_ReturnsFour()
116+
=> AreEqual(4.0, ExecuteScalar("select square(cast(2 as float))"));
117+
118+
[TestMethod]
119+
public void Square_OfDecimal_WidensToFloat()
120+
=> AreEqual(12.25, ExecuteScalar("select square(cast(3.5 as decimal(10,2)))"));
121+
122+
[TestMethod]
123+
public void Square_OfInt_WidensToFloat()
124+
=> AreEqual(2500000000.0, ExecuteScalar("select square(cast(50000 as int))"));
125+
126+
[TestMethod]
127+
public void Square_Overflow_RaisesMsg8115()
128+
=> AssertSqlError("select square(cast(1e200 as float))", 8115, "Arithmetic overflow error converting expression to data type float.");
129+
130+
[TestMethod]
131+
public void Degrees_OfPi_ReturnsOneHundredEighty()
132+
=> AreEqual(180.0, ExecuteScalar("select degrees(pi())"));
133+
134+
[TestMethod]
135+
public void Radians_OfOneEighty_ReturnsPi()
136+
=> AreEqual(Math.PI, ExecuteScalar("select radians(cast(180 as float))"));
137+
138+
[TestMethod]
139+
public void Degrees_OfInt_PreservesIntTypeAndTruncates()
140+
{
141+
AreEqual(57, ExecuteScalar<int>("select degrees(cast(1 as int))"));
142+
AreEqual(20626, ExecuteScalar<int>("select degrees(cast(360 as int))"));
143+
AreEqual(-57, ExecuteScalar<int>("select degrees(cast(-1 as int))"));
144+
}
145+
146+
[TestMethod]
147+
public void Degrees_OfBigint_PreservesBigint()
148+
=> AreEqual(57L, ExecuteScalar<long>("select degrees(cast(1 as bigint))"));
149+
150+
[TestMethod]
151+
public void Degrees_IntOverflow_RaisesMsg8115Int()
152+
=> AssertSqlError("select degrees(cast(2147483646 as int))", 8115, "Arithmetic overflow error converting expression to data type int.");
153+
154+
[TestMethod]
155+
public void Degrees_OfDecimal_WidensToDecimal38_18()
156+
{
157+
// SQL Server's decimal arithmetic uses higher-precision intermediate
158+
// accumulators than .NET's 96-bit decimal, so the trailing digits
159+
// diverge past the 14th decimal place. Real value: 85.94366926962348429...;
160+
// simulator value: 85.94366926962348131... (matches at 13 digits).
161+
var v = (decimal)ExecuteScalar("select degrees(cast(1.5 as decimal(10,2)))")!;
162+
IsLessThan(1e-12m, Math.Abs(85.94366926962348m - v));
163+
}
164+
165+
[TestMethod]
166+
public void Radians_OfDecimal_WidensToDecimal38_18()
167+
{
168+
var v = (decimal)ExecuteScalar("select radians(cast(180 as decimal(10,2)))")!;
169+
// 180 * pi / 180 = pi rendered as decimal(38, 18). Tolerance to absorb
170+
// .NET-vs-SQL-Server decimal-arithmetic precision differences.
171+
IsLessThan(1e-12m, Math.Abs(3.141592653589793m - v));
172+
}
173+
174+
[TestMethod]
175+
public void Degrees_PreservesScaleAboveEighteen()
176+
{
177+
// Input scale 22 > 18 → result decimal(38, 22). The .NET decimal storage
178+
// accommodates scale 22 for small values; the encoder's Pow10(22) ≈ 1e22
179+
// fits inside decimal.MaxValue (7.92e28). Larger scales (e.g. > 28)
180+
// exceed .NET decimal's range entirely — that's the pre-existing
181+
// 28-digit-max quirk noted in CLAUDE.md, not specific to DEGREES.
182+
var v = (decimal)ExecuteScalar("select degrees(cast(0.001 as decimal(25,22)))")!;
183+
var scale = (decimal.GetBits(v)[3] >> 16) & 0xFF;
184+
IsLessThanOrEqualTo(22, scale);
185+
}
186+
187+
[TestMethod]
188+
public void Degrees_OfMoney_StaysMoney()
189+
{
190+
var v = (decimal)ExecuteScalar("select degrees(cast(1 as money))")!;
191+
// money has scale 4; 1 radian in degrees ≈ 57.2958.
192+
AreEqual(57.2958m, v);
193+
}
194+
195+
[TestMethod]
196+
public void Degrees_TooFewArgs_RaisesMsg174()
197+
=> AssertSqlError("select degrees()", 174, "The degrees function requires 1 argument(s).");
198+
199+
[TestMethod]
200+
public void Radians_TooManyArgs_RaisesMsg174()
201+
=> AssertSqlError("select radians(1, 2)", 174, "The radians function requires 1 argument(s).");
202+
203+
[TestMethod]
204+
public void Cos_OfPi_ReturnsNegativeOne()
205+
=> AreEqual(-1.0, ExecuteScalar("select cos(pi())"));
206+
207+
[TestMethod]
208+
public void Square_OfNullFloat_PropagatesNull()
209+
=> AreEqual(DBNull.Value, ExecuteScalar("select square(cast(null as float))"));
210+
}

SqlServerSimulator/Parser/Expression.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,22 +328,35 @@ private static Expression ResolveBuiltIn(string name, ParserContext context)
328328
Span<char> uppercaseName = stackalloc char[name.Length];
329329
return name.ToUpperInvariant(uppercaseName) switch
330330
{
331+
2 => uppercaseName switch
332+
{
333+
"PI" => new Pi(context),
334+
_ => null
335+
},
331336
3 => uppercaseName switch
332337
{
333338
"ABS" => new AbsoluteValue(context),
334339
"AVG" => AggregateExpression.Parse(context, AggregateKind.Avg),
340+
"COS" => new TrigFunction(context, TrigKind.Cos),
341+
"COT" => new TrigFunction(context, TrigKind.Cot),
335342
"EXP" => new Exp(context),
336343
"IIF" => new Iif(context),
337344
"LEN" => new Length(context),
338345
"LOG" => new Log(context),
339346
"MAX" => AggregateExpression.Parse(context, AggregateKind.Max),
340347
"MIN" => AggregateExpression.Parse(context, AggregateKind.Min),
348+
"SIN" => new TrigFunction(context, TrigKind.Sin),
341349
"SUM" => AggregateExpression.Parse(context, AggregateKind.Sum),
350+
"TAN" => new TrigFunction(context, TrigKind.Tan),
342351
"VAR" => AggregateExpression.Parse(context, AggregateKind.Var),
343352
_ => null
344353
},
345354
4 => uppercaseName switch
346355
{
356+
"ACOS" => new TrigFunction(context, TrigKind.Acos),
357+
"ASIN" => new TrigFunction(context, TrigKind.Asin),
358+
"ATAN" => new TrigFunction(context, TrigKind.Atan),
359+
"ATN2" => new Atn2(context),
347360
"CAST" => new Cast(context),
348361
"LEFT" => new Left(context),
349362
"SIGN" => new Sign(context),
@@ -373,6 +386,7 @@ private static Expression ResolveBuiltIn(string name, ParserContext context)
373386
"CONCAT" => new StringConcat(context, StringConcatKind.Concat),
374387
"ISNULL" => new IsNullExpression(context),
375388
"NULLIF" => new NullIf(context),
389+
"SQUARE" => new TrigFunction(context, TrigKind.Square),
376390
"STDEVP" => AggregateExpression.Parse(context, AggregateKind.StdevP),
377391
_ => null
378392
},
@@ -381,8 +395,10 @@ private static Expression ResolveBuiltIn(string name, ParserContext context)
381395
"CEILING" => new Ceiling(context),
382396
"CONVERT" => new ConvertExpression(context, tryMode: false),
383397
"DATEADD" => new DateAdd(context),
398+
"DEGREES" => new Degrees(context),
384399
"EOMONTH" => new EOMonth(context),
385400
"GETDATE" => new CurrentTimeFunction(context, CurrentTimeKind.GetDate),
401+
"RADIANS" => new Radians(context),
386402
"REPLACE" => new Replace(context),
387403
"REVERSE" => new Reverse(context),
388404
_ => null
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator.Parser.Expressions;
4+
5+
/// <summary>
6+
/// SQL <c>ATN2(y, x)</c>: returns the angle (in radians) whose tangent is
7+
/// <c>y / x</c>, taking the quadrant of <c>(x, y)</c> into account. Result
8+
/// is always <see cref="SqlType.Float"/> regardless of input categories —
9+
/// probe-confirmed against SQL Server 2025 (2026-05-10). NULL on either
10+
/// operand propagates as typed-NULL float.
11+
/// </summary>
12+
/// <remarks>
13+
/// SQL Server's <c>ATN2(0, 0)</c> raises Msg 3623 ("An invalid floating
14+
/// point operation occurred.") rather than returning 0 like .NET's
15+
/// <c>Math.Atan2(0, 0)</c> does — probe-confirmed.
16+
/// </remarks>
17+
internal sealed class Atn2 : Expression
18+
{
19+
private readonly Expression first;
20+
private readonly Expression second;
21+
22+
public Atn2(ParserContext context)
23+
{
24+
if (context.Token is Tokens.Operator { Character: ')' })
25+
throw SimulatedSqlException.FunctionRequiresNArguments("atn2", 2);
26+
this.first = Parse(context);
27+
if (context.Token is not Tokens.Operator { Character: ',' })
28+
throw SimulatedSqlException.FunctionRequiresNArguments("atn2", 2);
29+
context.MoveNextRequired();
30+
this.second = Parse(context);
31+
if (context.Token is not Tokens.Operator { Character: ')' })
32+
throw SimulatedSqlException.FunctionRequiresNArguments("atn2", 2);
33+
}
34+
35+
public override SqlValue Run(Func<MultiPartName, SqlValue> getColumnValue)
36+
{
37+
var y = this.first.Run(getColumnValue);
38+
if (y.IsNull) return SqlValue.Null(SqlType.Float);
39+
var x = this.second.Run(getColumnValue);
40+
if (x.IsNull) return SqlValue.Null(SqlType.Float);
41+
var yd = MathScalars.AsDouble(y);
42+
var xd = MathScalars.AsDouble(x);
43+
return yd == 0 && xd == 0
44+
? throw SimulatedSqlException.InvalidFloatingPointOperation()
45+
: SqlValue.FromDouble(Math.Atan2(yd, xd));
46+
}
47+
48+
public override SqlType GetSqlType(Func<MultiPartName, SqlType> resolveColumnType) => SqlType.Float;
49+
50+
internal override string DebugDisplay() => $"ATN2({this.first.DebugDisplay()}, {this.second.DebugDisplay()})";
51+
}

0 commit comments

Comments
 (0)