Skip to content

Commit 7ffdce2

Browse files
committed
Design-time caching and concurrency fixes
1 parent 6851725 commit 7ffdce2

28 files changed

Lines changed: 229 additions & 170 deletions

File tree

docs/RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 1.5.22 - 11.06.2026
2+
* Initial groupValBy and better exits-subquery support
3+
* Caching and concurrency Fixes
4+
* Reference file updates
5+
16
### 1.5.21 - 28.05.2026
27
* Added more tolerance to type mapping
38

src/SQLProvider.Common/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.Common")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type provider for SQL database access, common library")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.Common"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type provider for SQL database access, common library"
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

src/SQLProvider.DesignTime/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.DesignTime")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type providers for any SQL database access.")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.DesignTime"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type providers for any SQL database access."
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

src/SQLProvider.DesignTime/SqlDesignTime.fs

Lines changed: 109 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ type DesignCacheKey =
2929
string * // SSDT Path
3030
string)) //typeName
3131

32-
module DesignTimeCacheSchema =
33-
let schemaMap = System.Collections.Concurrent.ConcurrentDictionary<DesignCacheKey*string, ProvidedTypeDefinition>()
34-
3532
type internal ParameterValue =
3633
| UserProvided of string * string * Type
3734
| Default of Expr
@@ -466,11 +463,6 @@ module DesignTimeUtils =
466463
|> Seq.map snd
467464
| sproc::rest -> generateTypeTree con prov (walkSproc con prov [] None createdTypes sproc) rest
468465

469-
let getOrAddSchema (args:DesignCacheKey) (name:string) =
470-
DesignTimeCacheSchema.schemaMap.GetOrAdd((args,name), fun (a,nme) ->
471-
let pt = ProvidedTypeDefinition(nme + "Schema", Some typeof<obj>, isErased=true)
472-
pt)
473-
474466
let createDesignTimeCommands (prov:ISqlProvider) contextSchemaPath recreate (invalidate: _ -> Unit)=
475467
let designTimeCommandsContainer = ProvidedTypeDefinition("DesignTimeCommands", Some typeof<obj>, isErased=true)
476468
let designTime = ProvidedProperty("Design Time Commands", designTimeCommandsContainer, getterCode = empty)
@@ -525,7 +517,6 @@ module DesignTimeUtils =
525517
schemacache.SprocsParams.Clear()
526518
schemacache.Packages.Clear()
527519
schemacache.Individuals.Clear()
528-
DesignTimeCacheSchema.schemaMap.Clear()
529520
invalidate()
530521
let pf = recreate()
531522
saveInProcess <- false
@@ -741,6 +732,13 @@ module DesignTimeUtils =
741732
sprocList
742733
generateTypeTree con prov Map.empty sprocs
743734

735+
// Schema container types are per-build on purpose: sharing them across rebuilds would
736+
// mutate types that another (VS) thread may concurrently enumerate, and would
737+
// accumulate duplicate members on every rebuild.
738+
let schemaTypes = System.Collections.Concurrent.ConcurrentDictionary<string, ProvidedTypeDefinition>()
739+
let getOrAddSchema (name:string) =
740+
schemaTypes.GetOrAdd(name, fun nme -> ProvidedTypeDefinition(nme + "Schema", Some typeof<obj>, isErased=true))
741+
744742
let addServiceTypeMembers (isReadonly:bool) =
745743
[
746744
if not isReadonly then
@@ -758,7 +756,7 @@ module DesignTimeUtils =
758756
for (KeyValue(key,(entityType,desc,_,schema))) in tableTypes do
759757
// collection type, individuals type
760758
let (ct,it) = baseCollectionTypes.Force().[key]
761-
let schemaType = getOrAddSchema args schema
759+
let schemaType = getOrAddSchema schema
762760

763761
let templateTable = ProvidedTypeDefinition(ct.Name+"Template", Some typeof<obj>, isErased=true)
764762
templateTable.AddMemberDelayed(fun () ->
@@ -1061,7 +1059,6 @@ module DesignTimeUtils =
10611059
| :? ObjectDisposedException -> ()
10621060

10631061
invalidate()
1064-
DesignTimeCacheSchema.schemaMap.Clear()
10651062

10661063
GC.Collect()
10671064

@@ -1082,12 +1079,11 @@ module DesignTimeUtils =
10821079
yield designTime :> MemberInfo
10831080

10841081
] @ [
1085-
for KeyValue((cachedargs,name),pt) in DesignTimeCacheSchema.schemaMap do
1086-
if args = cachedargs then
1087-
yield pt :> MemberInfo
1088-
yield ProvidedProperty(SchemaProjections.buildTableName(name),pt, getterCode = fun args ->
1089-
let a0 = args.[0]
1090-
<@@ ((%%a0 : obj) :?> ISqlDataContext) @@> ) :> MemberInfo
1082+
for KeyValue(name,pt) in schemaTypes do
1083+
yield pt :> MemberInfo
1084+
yield ProvidedProperty(SchemaProjections.buildTableName(name),pt, getterCode = fun args ->
1085+
let a0 = args.[0]
1086+
<@@ ((%%a0 : obj) :?> ISqlDataContext) @@> ) :> MemberInfo
10911087
]
10921088

10931089
serviceType.AddMembers(addServiceTypeMembers false)
@@ -1292,8 +1288,23 @@ type SqlRuntimeInfo (config : TypeProviderConfig) =
12921288
//| Choice2Of2(paths, errors) -> Assembly.GetExecutingAssembly()
12931289
member __.RuntimeAssembly = runtimeAssembly
12941290

1291+
/// One generation of provided types for one set of static parameters.
1292+
/// Reference equality so that ConcurrentDictionary.TryUpdate swaps compare generations,
1293+
/// not their (concurrently mutated) field values.
1294+
[<ReferenceEquality>]
1295+
type DesignCacheEntry =
1296+
{ /// The provided root type, when it was built, and how old the build may get before a
1297+
/// background refresh is started (adapted to build duration on slow systems).
1298+
Root : Lazy<ProvidedTypeDefinition * DateTime * TimeSpan>
1299+
/// Last access in UTC ticks. Updated lock-free on every instantiation request;
1300+
/// entries idle longer than the expiration are dropped to free memory.
1301+
mutable LastAccess : int64
1302+
/// 1 while a background refresh build is in flight: at most one refresh per entry,
1303+
/// no matter how many Visual Studio threads request the type concurrently.
1304+
mutable Refreshing : int }
1305+
12951306
module DesignTimeCache =
1296-
let cache = System.Collections.Concurrent.ConcurrentDictionary<DesignCacheKey,Lazy<ProvidedTypeDefinition> * DateTime>()
1307+
let cache = System.Collections.Concurrent.ConcurrentDictionary<DesignCacheKey,DesignCacheEntry>()
12971308

12981309
/// The idea of this is trying to avoid case where compile-time has loaded non-runtime assembly. (Happens in .NET 8.0, not in .NET Framework.)
12991310
/// So let's load compile-time (and design-time) manually the required runtime assembly.
@@ -1450,8 +1461,6 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
14501461
let _ = FixReferenceAssemblies.manualLoadNet8Runtime.Force()
14511462
#endif
14521463
let sqlRuntimeInfo = SqlRuntimeInfo(config)
1453-
let mySaveLock = new Object();
1454-
let mutable saveInProcess = false
14551464

14561465
let empty = fun (_:Expr list) -> <@@ () @@>
14571466

@@ -1514,52 +1523,90 @@ type public SqlTypeProvider(config: TypeProviderConfig) as this =
15141523
args.[12] :?> string, // SSDT Path
15151524
typeName)
15161525

1517-
let addCache args =
1518-
lazy
1519-
let struct(connectionString, conStringName,dbVendor,resolutionPath,individualsAmount,useOptionTypes,owner,caseSensitivity, tableNames, contextSchemaPath, odbcquote, sqliteLibrary, ssdtPath, rootTypeName) = args
1520-
1521-
let rootType = ProvidedTypeDefinition(sqlRuntimeInfo.RuntimeAssembly,FSHARP_DATA_SQL,rootTypeName,Some typeof<obj>, isErased=true)
1522-
let serviceType = ProvidedTypeDefinition( "dataContext", Some typeof<obj>, isErased=true)
1523-
let readServiceType = ProvidedTypeDefinition( "readDataContext", Some typeof<obj>, isErased=true)
1524-
1525-
createTypes rootType serviceType readServiceType config sqlRuntimeInfo invalidate registerDispose args
1526-
createConstructors config (rootType, serviceType, readServiceType, args)
1527-
1528-
// This is not a perfect cache-invalidation solution, it can remove a valid item from
1529-
// cache after the time-out, causing one extra hit, but this is only a design-time cache
1530-
// and it will work well enough to deal with Visual Studio's multi-threading problems
1531-
let expiration = TimeSpan.FromMinutes 3.0
1532-
let rec invalidationFunction key =
1526+
// Entries idle longer than this are dropped to free memory. An actively used entry is
1527+
// never dropped on a timer; instead it is refreshed in the background once its build is
1528+
// older than its staleness interval, so database schema changes are still picked up
1529+
// without IntelliSense ever stalling on a synchronous rebuild.
1530+
let idleExpiration = TimeSpan.FromMinutes 3.0
1531+
1532+
let buildRoot (args:DesignCacheKey) =
1533+
let struct(_,_,_,_,_,_,_,_,_,_,_,_,_, rootTypeName) = args
1534+
let buildStarted = DateTime.UtcNow
1535+
1536+
let rootType = ProvidedTypeDefinition(sqlRuntimeInfo.RuntimeAssembly,FSHARP_DATA_SQL,rootTypeName,Some typeof<obj>, isErased=true)
1537+
let serviceType = ProvidedTypeDefinition( "dataContext", Some typeof<obj>, isErased=true)
1538+
let readServiceType = ProvidedTypeDefinition( "readDataContext", Some typeof<obj>, isErased=true)
1539+
1540+
createTypes rootType serviceType readServiceType config sqlRuntimeInfo invalidate registerDispose args
1541+
createConstructors config (rootType, serviceType, readServiceType, args)
1542+
1543+
// Refresh no sooner than the idle expiration, and on a slow system no sooner than
1544+
// 5x the time a build takes, so refreshes cannot put the system under pressure.
1545+
let buildDuration = DateTime.UtcNow - buildStarted
1546+
let staleAfter = max idleExpiration (TimeSpan.FromTicks(buildDuration.Ticks * 5L))
1547+
rootType, DateTime.UtcNow, staleAfter
1548+
1549+
let dropDesignTimeDcProvider (key:DesignCacheKey) =
1550+
// Release the design-time data context provider (used by the Individuals feature)
1551+
// together with its type tree generation, so it cannot pin stale schema in memory.
1552+
let struct(_,_,_,_,_,_,_,_,_,_,_,_,_, rootTypeName) = key
1553+
DcCache.providerCache.TryRemove rootTypeName |> ignore
1554+
1555+
let rec idleWatcher (key:DesignCacheKey) =
1556+
async {
1557+
do! Async.Sleep (int idleExpiration.TotalMilliseconds)
1558+
match DesignTimeCache.cache.TryGetValue key with
1559+
| true, entry ->
1560+
let lastAccess = DateTime(System.Threading.Interlocked.Read(&entry.LastAccess), DateTimeKind.Utc)
1561+
if DateTime.UtcNow - lastAccess >= idleExpiration then
1562+
DesignTimeCache.cache.TryRemove key |> ignore
1563+
dropDesignTimeDcProvider key
1564+
else
1565+
do! idleWatcher key
1566+
| _ -> ()
1567+
}
1568+
1569+
let addCache (key:DesignCacheKey) =
1570+
{ Root =
1571+
lazy
1572+
let generation = buildRoot key
1573+
idleWatcher key |> Async.Start
1574+
generation
1575+
LastAccess = DateTime.UtcNow.Ticks
1576+
Refreshing = 0 }
1577+
1578+
try
1579+
let entry = DesignTimeCache.cache.GetOrAdd(arguments, addCache)
1580+
System.Threading.Interlocked.Exchange(&entry.LastAccess, DateTime.UtcNow.Ticks) |> ignore
1581+
1582+
// Stale-while-revalidate: always serve the current tree; if it has gone stale,
1583+
// rebuild it once in the background and swap the entry atomically. Callers never
1584+
// wait on a refresh, and concurrent VS threads can never start a second one.
1585+
if entry.Root.IsValueCreated then
1586+
let _, builtAt, staleAfter = entry.Root.Value
1587+
if DateTime.UtcNow - builtAt >= staleAfter
1588+
&& System.Threading.Interlocked.CompareExchange(&entry.Refreshing, 1, 0) = 0 then
15331589
async {
1534-
do! Async.Sleep (int expiration.TotalMilliseconds)
1590+
try
1591+
try
1592+
let freshGeneration = buildRoot arguments
1593+
let freshEntry =
1594+
{ Root = Lazy<_>.CreateFromValue freshGeneration
1595+
LastAccess = DateTime.UtcNow.Ticks
1596+
Refreshing = 0 }
1597+
if DesignTimeCache.cache.TryUpdate(arguments, freshEntry, entry) then
1598+
dropDesignTimeDcProvider arguments
1599+
with
1600+
| _ -> () // keep serving the previous generation; retried on a later access
1601+
finally
1602+
System.Threading.Interlocked.Exchange(&entry.Refreshing, 0) |> ignore
1603+
} |> Async.Start
15351604

1536-
match DesignTimeCache.cache.TryGetValue key with
1537-
| true, (_, timestamp) ->
1538-
if DateTime.UtcNow - timestamp >= expiration then
1539-
DesignTimeCache.cache.TryRemove key |> ignore
1540-
else
1541-
do! invalidationFunction key
1542-
| _ -> ()
1543-
1544-
}
1545-
invalidationFunction args |> Async.Start
1546-
rootType
1547-
, DateTime.UtcNow
1548-
try (DesignTimeCache.cache.GetOrAdd(arguments, addCache) |> fst).Value
1605+
let root, _, _ = entry.Root.Value
1606+
root
15491607
with
15501608
| e ->
1551-
let _ = DesignTimeCache.cache.TryRemove(arguments)
1552-
let _ =
1553-
lock mySaveLock (fun() ->
1554-
let keysToClear =
1555-
DesignTimeCacheSchema.schemaMap.Keys
1556-
|> Seq.toList
1557-
|> List.filter(fun (a,k) -> a = arguments)
1558-
keysToClear |> Seq.iter(fun ak ->
1559-
let _ = DesignTimeCacheSchema.schemaMap.TryRemove ak
1560-
()
1561-
)
1562-
)
1609+
DesignTimeCache.cache.TryRemove(arguments) |> ignore
15631610
reraise()
15641611
)
15651612

src/SQLProvider.DuckDb.DesignTime/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.DuckDb.DesignTime")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type providers for DuckDb database access.")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.DuckDb.DesignTime"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type providers for DuckDb database access."
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

src/SQLProvider.DuckDb.Runtime/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.DuckDb.Runtime")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type providers for DuckDb database access.")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.DuckDb.Runtime"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type providers for DuckDb database access."
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

src/SQLProvider.Firebird.DesignTime/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.Firebird.DesignTime")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type providers for Firebird database access.")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.Firebird.DesignTime"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type providers for Firebird database access."
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

src/SQLProvider.Firebird.Runtime/AssemblyInfo.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ open System.Reflection
55
[<assembly: AssemblyTitleAttribute("SQLProvider.Firebird.Runtime")>]
66
[<assembly: AssemblyProductAttribute("SQLProvider")>]
77
[<assembly: AssemblyDescriptionAttribute("Type providers for Firebird database access.")>]
8-
[<assembly: AssemblyVersionAttribute("1.5.21")>]
9-
[<assembly: AssemblyFileVersionAttribute("1.5.21")>]
8+
[<assembly: AssemblyVersionAttribute("1.5.22")>]
9+
[<assembly: AssemblyFileVersionAttribute("1.5.22")>]
1010
do ()
1111

1212
module internal AssemblyVersionInformation =
1313
let [<Literal>] AssemblyTitle = "SQLProvider.Firebird.Runtime"
1414
let [<Literal>] AssemblyProduct = "SQLProvider"
1515
let [<Literal>] AssemblyDescription = "Type providers for Firebird database access."
16-
let [<Literal>] AssemblyVersion = "1.5.21"
17-
let [<Literal>] AssemblyFileVersion = "1.5.21"
16+
let [<Literal>] AssemblyVersion = "1.5.22"
17+
let [<Literal>] AssemblyFileVersion = "1.5.22"

0 commit comments

Comments
 (0)