@@ -51,6 +51,35 @@ public void SetActiveAccess(IKvAccess? kv)
5151 /// </summary>
5252 public bool IdentityInsertEnabled { get ; set ; }
5353
54+ // Cache of parsed DEFAULT expressions keyed by source SQL — the same
55+ // string fires once per row inserted, parser is cheap but constant
56+ // overhead adds up under bulk loads.
57+ private readonly Dictionary < string , Ast . Expression > _defaultExprCache = new ( ) ;
58+
59+ /// <summary>
60+ /// Evaluate a column's <c>DefaultExpressionSql</c> (v2.4 non-literal
61+ /// DEFAULT). Cached on first use; the expression has no row context
62+ /// (DEFAULT can't reference columns) so we evaluate against an empty
63+ /// dummy table.
64+ /// </summary>
65+ private SqlValue EvalDefaultExpression ( string defExprSql )
66+ {
67+ if ( ! _defaultExprCache . TryGetValue ( defExprSql , out var expr ) )
68+ {
69+ expr = Parsing . TSqlParser . ParseExpressionString ( defExprSql ) ;
70+ _defaultExprCache [ defExprSql ] = expr ;
71+ }
72+ var dummyTable = new TableSchema (
73+ Name : "(default-expr)" ,
74+ Columns : Array . Empty < ColumnSchema > ( ) ,
75+ PrimaryKeyColumnIndex : null ,
76+ IdentityColumnIndex : null ) ;
77+ var ctx = new ExpressionEvaluator . RowContext (
78+ dummyTable , ReadOnlySpan < SqlValue > . Empty ,
79+ new Dictionary < string , SqlValue > ( ) ) ;
80+ return ExpressionEvaluator . Evaluate ( expr , ctx ) ;
81+ }
82+
5483 // ── Public entry points ─────────────────────────────────────────────────
5584
5685 public async Task < int > ExecuteAsync (
@@ -225,7 +254,12 @@ private async Task<InsertResult> ExecuteInsertCore(
225254 for ( int i = 0 ; i < table . Columns . Count ; i ++ )
226255 {
227256 var col = table . Columns [ i ] ;
228- record [ i ] = col . DefaultValue ?? SqlValue . Null_ ;
257+ // v2.4: non-literal DEFAULT (e.g. GETUTCDATE()) is stored
258+ // as a SQL string; parse + evaluate at INSERT time.
259+ if ( col . DefaultExpressionSql is { } defExprSql )
260+ record [ i ] = EvalDefaultExpression ( defExprSql ) ;
261+ else
262+ record [ i ] = col . DefaultValue ?? SqlValue . Null_ ;
229263 }
230264
231265 // Track whether the caller supplied an explicit IDENTITY value
@@ -1387,17 +1421,30 @@ private async Task<DeleteResult> ExecuteDeleteCore(
13871421 private static SqlValue EvalLiteralOrBound (
13881422 Expression expr , IReadOnlyDictionary < string , SqlValue > bound )
13891423 {
1390- return expr switch
1424+ // Fast paths for the overwhelmingly common cases — literal and
1425+ // parameter dispatch without building a RowContext.
1426+ switch ( expr )
13911427 {
1392- LiteralExpression l => l . Value ,
1393- ParameterExpression p => bound . TryGetValue ( p . Name , out var v )
1394- ? v
1395- : throw new InvalidOperationException (
1396- $ "Parameter '{ p . Name } ' was not bound.") ,
1397- UnaryExpression { Op : UnaryOperator . Negate } u => NegateLiteral ( u . Operand , bound ) ,
1398- _ => throw new InvalidOperationException (
1399- $ "Expression { expr . GetType ( ) . Name } is not a constant — INSERT supports only literals and parameters.") ,
1400- } ;
1428+ case LiteralExpression l :
1429+ return l . Value ;
1430+ case ParameterExpression p :
1431+ return bound . TryGetValue ( p . Name , out var v )
1432+ ? v
1433+ : throw new InvalidOperationException ( $ "Parameter '{ p . Name } ' was not bound.") ;
1434+ case UnaryExpression { Op : UnaryOperator . Negate } u :
1435+ return NegateLiteral ( u . Operand , bound ) ;
1436+ }
1437+ // General expression in INSERT VALUES position — CASE WHEN, function
1438+ // calls, arithmetic. Evaluate against a dummy empty-row context so
1439+ // column refs throw cleanly (no source row at INSERT time).
1440+ var dummyTable = new TableSchema (
1441+ Name : "(insert-values)" ,
1442+ Columns : Array . Empty < ColumnSchema > ( ) ,
1443+ PrimaryKeyColumnIndex : null ,
1444+ IdentityColumnIndex : null ) ;
1445+ var ctx = new ExpressionEvaluator . RowContext (
1446+ dummyTable , ReadOnlySpan < SqlValue > . Empty , bound ) ;
1447+ return ExpressionEvaluator . Evaluate ( expr , ctx ) ;
14011448 }
14021449
14031450 private static SqlValue NegateLiteral (
0 commit comments