1- using System . Collections . Frozen ;
21using SqlServerSimulator . Storage ;
32
43namespace SqlServerSimulator . Parser . Expressions ;
@@ -7,8 +6,8 @@ namespace SqlServerSimulator.Parser.Expressions;
76/// SQL <c>DATABASEPROPERTYEX(database_name, property_name)</c>: returns
87/// the named property of a database. Real SQL Server projects this as
98/// <c>sql_variant</c> carrying a per-property inner base type; the
10- /// simulator doesn't model sql_variant, so it surfaces the bare true type
11- /// instead — numeric properties as <see cref="SqlType.Int32"/> /
9+ /// simulator doesn't model sql_variant here , so it surfaces the bare true
10+ /// type instead — numeric properties as <see cref="SqlType.Int32"/> /
1211/// <see cref="SqlType.TinyInt"/>, string properties as
1312/// <see cref="SqlType.NVarchar"/>. When the property-name argument is a
1413/// compile-time constant the true type flows to the projection schema;
@@ -26,35 +25,13 @@ namespace SqlServerSimulator.Parser.Expressions;
2625/// <c>SnapshotIsolationState</c>, <c>IsReadCommittedSnapshotOn</c>,
2726/// <c>ComparisonStyle</c>, <c>LCID</c>, <c>SQLSortOrder</c>, <c>Version</c>.
2827/// Properties not on the list return NULL (forward-compatible with future
29- /// tooling that may query newer property names).
28+ /// tooling that may query newer property names). <see cref="Produce"/> and
29+ /// <see cref="TypeOf"/> carry mirrored arm lists — every property appears in
30+ /// both, with <see cref="TypeOf"/> declaring the type the matching
31+ /// <see cref="Produce"/> arm's non-NULL value carries.
3032/// </remarks>
3133internal sealed class DatabasePropertyEx : Expression
3234{
33- private static readonly FrozenDictionary < string , ( SqlType Type , Func < Database , SqlValue > Produce ) > Properties = new Dictionary < string , ( SqlType Type , Func < Database , SqlValue > Produce ) >
34- {
35- [ "Status" ] = ( SqlType . NVarchar , _ => SqlValue . FromNVarchar ( "ONLINE" ) ) ,
36- [ "Version" ] = ( SqlType . Int32 , _ => SqlValue . FromInt32 ( 0 ) ) ,
37- [ "Recovery" ] = ( SqlType . NVarchar , _ => SqlValue . FromNVarchar ( "FULL" ) ) ,
38- // Updateability is always READ_WRITE (the simulator models no
39- // read-only databases at the DATABASEPROPERTYEX surface). SMO's
40- // database-properties preamble reads it as [IsUpdateable].
41- [ "Updateability" ] = ( SqlType . NVarchar , _ => SqlValue . FromNVarchar ( "READ_WRITE" ) ) ,
42- // DBCC CHECKDB isn't modeled, so the last-good-checkdb time is NULL —
43- // typed datetime (not the unknown-property NVarchar) so SMO's
44- // CAST(ISNULL(..., 0) AS datetime) resolves to 1900-01-01 rather than
45- // failing to convert the string '0' to datetime.
46- [ "LastGoodCheckDbTime" ] = ( SqlType . DateTime , _ => SqlValue . Null ( SqlType . DateTime ) ) ,
47- [ "Collation" ] = ( SqlType . NVarchar , db => SqlValue . FromNVarchar ( db . CollationName ) ) ,
48- [ "UserAccess" ] = ( SqlType . NVarchar , _ => SqlValue . FromNVarchar ( "MULTI_USER" ) ) ,
49- [ "IsAutoClose" ] = ( SqlType . Int32 , _ => SqlValue . FromInt32 ( 0 ) ) ,
50- [ "IsAutoShrink" ] = ( SqlType . Int32 , _ => SqlValue . FromInt32 ( 0 ) ) ,
51- [ "SnapshotIsolationState" ] = ( SqlType . Int32 , db => SqlValue . FromInt32 ( db . AllowSnapshotIsolation ? 1 : 0 ) ) ,
52- [ "IsReadCommittedSnapshotOn" ] = ( SqlType . Int32 , db => SqlValue . FromInt32 ( db . ReadCommittedSnapshot ? 1 : 0 ) ) ,
53- [ "ComparisonStyle" ] = ( SqlType . Int32 , _ => SqlValue . FromInt32 ( 196609 ) ) ,
54- [ "LCID" ] = ( SqlType . Int32 , _ => SqlValue . FromInt32 ( 1033 ) ) ,
55- [ "SQLSortOrder" ] = ( SqlType . TinyInt , db => SqlValue . FromByte ( SortIdFor ( db . CollationName ) ) ) ,
56- } . ToFrozenDictionary ( StringComparer . OrdinalIgnoreCase ) ;
57-
5835 private readonly Expression dbNameArg ;
5936 private readonly Expression propertyArg ;
6037
@@ -83,21 +60,80 @@ public override SqlValue Run(RuntimeContext runtime)
8360 if ( ! runtime . Batch . Connection . Simulation . Databases . TryGetValue ( dbName , out var db ) )
8461 return SqlValue . Null ( SqlType . NVarchar ) ;
8562
86- if ( ! Properties . TryGetValue ( property , out var def ) )
87- return SqlValue . Null ( SqlType . NVarchar ) ;
88-
89- var value = def . Produce ( db ) ;
63+ var value = Produce ( property , db ) ;
9064 // A non-constant property name couldn't resolve a true type at parse
9165 // time (GetSqlType fell back to NVarchar); coerce so runtime agrees.
9266 return this . propertyArg is Value ? value : value . CoerceTo ( SqlType . NVarchar ) ;
9367 }
9468
9569 public override SqlType GetSqlType ( BatchContext batch , Func < MultiPartName , SqlType > resolveColumnType )
9670 => this . propertyArg is Value { Constant : { IsNull : false } constant }
97- && Properties . TryGetValue ( constant . CoerceTo ( SqlType . NVarchar ) . AsString , out var def )
98- ? def . Type
71+ ? TypeOf ( constant . CoerceTo ( SqlType . NVarchar ) . AsString )
9972 : SqlType . NVarchar ;
10073
74+ private static SqlValue Produce ( string property , Database db )
75+ {
76+ // Longer than any recognized property name; also bounds the stackalloc
77+ // against an adversarially long argument.
78+ if ( property . Length > 32 )
79+ return SqlValue . Null ( SqlType . NVarchar ) ;
80+ Span < char > upper = stackalloc char [ property . Length ] ;
81+ _ = property . AsSpan ( ) . ToUpperInvariant ( upper ) ;
82+ return upper switch
83+ {
84+ "COLLATION" => SqlValue . FromNVarchar ( db . CollationName ) ,
85+ "COMPARISONSTYLE" => SqlValue . FromInt32 ( 196609 ) ,
86+ "ISAUTOCLOSE" => SqlValue . FromInt32 ( 0 ) ,
87+ "ISAUTOSHRINK" => SqlValue . FromInt32 ( 0 ) ,
88+ "ISREADCOMMITTEDSNAPSHOTON" => SqlValue . FromInt32 ( db . ReadCommittedSnapshot ? 1 : 0 ) ,
89+ // DBCC CHECKDB isn't modeled, so the last-good-checkdb time is
90+ // NULL — typed datetime (not the unknown-property NVarchar) so
91+ // SMO's CAST(ISNULL(..., 0) AS datetime) resolves to 1900-01-01
92+ // rather than failing to convert the string '0' to datetime.
93+ "LASTGOODCHECKDBTIME" => SqlValue . Null ( SqlType . DateTime ) ,
94+ "LCID" => SqlValue . FromInt32 ( 1033 ) ,
95+ "RECOVERY" => SqlValue . FromNVarchar ( "FULL" ) ,
96+ "SNAPSHOTISOLATIONSTATE" => SqlValue . FromInt32 ( db . AllowSnapshotIsolation ? 1 : 0 ) ,
97+ "SQLSORTORDER" => SqlValue . FromByte ( SortIdFor ( db . CollationName ) ) ,
98+ "STATUS" => SqlValue . FromNVarchar ( "ONLINE" ) ,
99+ // Updateability is always READ_WRITE (the simulator models no
100+ // read-only databases at the DATABASEPROPERTYEX surface). SMO's
101+ // database-properties preamble reads it as [IsUpdateable].
102+ "UPDATEABILITY" => SqlValue . FromNVarchar ( "READ_WRITE" ) ,
103+ "USERACCESS" => SqlValue . FromNVarchar ( "MULTI_USER" ) ,
104+ "VERSION" => SqlValue . FromInt32 ( 0 ) ,
105+ _ => SqlValue . Null ( SqlType . NVarchar ) ,
106+ } ;
107+ }
108+
109+ private static SqlType TypeOf ( string property )
110+ {
111+ // Same bound as Produce so a long name types as the unknown-property
112+ // NVarchar it evaluates to.
113+ if ( property . Length > 32 )
114+ return SqlType . NVarchar ;
115+ Span < char > upper = stackalloc char [ property . Length ] ;
116+ _ = property . AsSpan ( ) . ToUpperInvariant ( upper ) ;
117+ return upper switch
118+ {
119+ "COLLATION" => SqlType . NVarchar ,
120+ "COMPARISONSTYLE" => SqlType . Int32 ,
121+ "ISAUTOCLOSE" => SqlType . Int32 ,
122+ "ISAUTOSHRINK" => SqlType . Int32 ,
123+ "ISREADCOMMITTEDSNAPSHOTON" => SqlType . Int32 ,
124+ "LASTGOODCHECKDBTIME" => SqlType . DateTime ,
125+ "LCID" => SqlType . Int32 ,
126+ "RECOVERY" => SqlType . NVarchar ,
127+ "SNAPSHOTISOLATIONSTATE" => SqlType . Int32 ,
128+ "SQLSORTORDER" => SqlType . TinyInt ,
129+ "STATUS" => SqlType . NVarchar ,
130+ "UPDATEABILITY" => SqlType . NVarchar ,
131+ "USERACCESS" => SqlType . NVarchar ,
132+ "VERSION" => SqlType . Int32 ,
133+ _ => SqlType . NVarchar ,
134+ } ;
135+ }
136+
101137 // Derive the SQL sort-order id from the collation name; real SQL Server
102138 // reports 0 for collations with no SQL_* sort order.
103139 private static byte SortIdFor ( string collationName )
0 commit comments