@@ -131,6 +131,158 @@ public Task<Seq<T>> QueryAsync<T>(
131131 return Task . FromResult ( toSeq ( list ) ) ;
132132 }
133133
134+ /// <summary>
135+ /// Executes a Functional SQL statement that uses SharpCoreDB's extended syntax.
136+ /// Supports <c>OPTIONALLY FROM</c>, <c>IS SOME</c>, <c>IS NONE</c>,
137+ /// <c>UNWRAP column AS alias</c>, and <c>MATCH SOME/NONE</c> keywords.
138+ /// </summary>
139+ /// <typeparam name="T">Row model type.</typeparam>
140+ /// <param name="functionalSql">The Functional SQL statement.</param>
141+ /// <param name="parameters">Optional query parameters.</param>
142+ /// <param name="cancellationToken">Cancellation token.</param>
143+ /// <returns>
144+ /// When <c>OPTIONALLY FROM</c> is used, returns a sequence of <see cref="Option{T}"/>
145+ /// (each row wrapped). Otherwise returns <c>Some</c> rows only.
146+ /// </returns>
147+ /// <example>
148+ /// <code>
149+ /// // Get users who have an email, each wrapped in Option
150+ /// var users = await fdb.ExecuteFunctionalSqlAsync<UserDto>(
151+ /// "SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS SOME");
152+ ///
153+ /// foreach (var userOpt in users)
154+ /// {
155+ /// var name = userOpt.Map(u => u.Name).IfNone("unknown");
156+ /// }
157+ /// </code>
158+ /// </example>
159+ public Task < Seq < Option < T > > > ExecuteFunctionalSqlAsync < T > (
160+ string functionalSql ,
161+ Dictionary < string , object ? > ? parameters = null ,
162+ CancellationToken cancellationToken = default )
163+ where T : class , new ( )
164+ {
165+ ArgumentException . ThrowIfNullOrWhiteSpace ( functionalSql ) ;
166+ cancellationToken . ThrowIfCancellationRequested ( ) ;
167+
168+ var translator = new FunctionalSqlTranslator ( ) ;
169+ var result = translator . Translate ( functionalSql ) ;
170+
171+ var rows = _inner . ExecuteQuery ( result . StandardSql , parameters ) ;
172+ if ( rows . Count == 0 )
173+ {
174+ return Task . FromResult ( Seq < Option < T > > . Empty ) ;
175+ }
176+
177+ var list = new List < Option < T > > ( rows . Count ) ;
178+ foreach ( var row in rows )
179+ {
180+ // Apply UNWRAP defaults: if column is null/empty, substitute default
181+ foreach ( var unwrap in result . UnwrapMappings )
182+ {
183+ if ( TryGetValueIgnoreCase ( row , unwrap . Column , out var val )
184+ && ( val is null || ( val is string s && string . IsNullOrEmpty ( s ) ) )
185+ && unwrap . DefaultValue is not null )
186+ {
187+ row [ unwrap . Column ] = unwrap . DefaultValue ;
188+ }
189+ }
190+
191+ // Apply post-filter for SOME columns (defense-in-depth beyond SQL translation)
192+ if ( result . SomeColumns . Count > 0 && ! PassesSomeFilter ( row , result . SomeColumns ) )
193+ {
194+ continue ;
195+ }
196+
197+ // Apply post-filter for NONE columns (defense-in-depth beyond SQL translation)
198+ if ( result . NoneColumns . Count > 0 && ! PassesNoneFilter ( row , result . NoneColumns ) )
199+ {
200+ continue ;
201+ }
202+
203+ var mapped = TryMapRow < T > ( row ) ;
204+ list . Add ( mapped ) ;
205+ }
206+
207+ return Task . FromResult ( toSeq ( list ) ) ;
208+ }
209+
210+ /// <summary>
211+ /// Executes a Functional SQL query returning only successfully mapped rows
212+ /// (unwrapped from <c>Option<T></c>). Convenience method when you want
213+ /// a flat sequence instead of <c>Seq<Option<T>></c>.
214+ /// </summary>
215+ /// <typeparam name="T">Row model type.</typeparam>
216+ /// <param name="functionalSql">The Functional SQL statement.</param>
217+ /// <param name="parameters">Optional query parameters.</param>
218+ /// <param name="cancellationToken">Cancellation token.</param>
219+ /// <returns>A flat <see cref="Seq{T}"/> of successfully mapped rows.</returns>
220+ public async Task < Seq < T > > ExecuteFunctionalSqlUnwrappedAsync < T > (
221+ string functionalSql ,
222+ Dictionary < string , object ? > ? parameters = null ,
223+ CancellationToken cancellationToken = default )
224+ where T : class , new ( )
225+ {
226+ var results = await ExecuteFunctionalSqlAsync < T > ( functionalSql , parameters , cancellationToken )
227+ . ConfigureAwait ( false ) ;
228+
229+ var list = new List < T > ( results . Count ) ;
230+ foreach ( var opt in results )
231+ {
232+ opt . IfSome ( list . Add ) ;
233+ }
234+
235+ return toSeq ( list ) ;
236+ }
237+
238+ private static bool PassesSomeFilter ( Dictionary < string , object > row , IReadOnlyList < string > someColumns )
239+ {
240+ foreach ( var col in someColumns )
241+ {
242+ if ( ! TryGetValueIgnoreCase ( row , col , out var val ) )
243+ {
244+ return false ;
245+ }
246+
247+ if ( IsSemanticNone ( val ) )
248+ {
249+ return false ;
250+ }
251+ }
252+
253+ return true ;
254+ }
255+
256+ private static bool PassesNoneFilter ( Dictionary < string , object > row , IReadOnlyList < string > noneColumns )
257+ {
258+ foreach ( var col in noneColumns )
259+ {
260+ if ( ! TryGetValueIgnoreCase ( row , col , out var val ) )
261+ {
262+ continue ;
263+ }
264+
265+ if ( ! IsSemanticNone ( val ) )
266+ {
267+ return false ;
268+ }
269+ }
270+
271+ return true ;
272+ }
273+
274+ private static bool IsSemanticNone ( object ? value )
275+ {
276+ return value switch
277+ {
278+ null => true ,
279+ DBNull => true ,
280+ string s when string . IsNullOrWhiteSpace ( s ) => true ,
281+ string s when string . Equals ( s , "NULL" , StringComparison . OrdinalIgnoreCase ) => true ,
282+ _ => false
283+ } ;
284+ }
285+
134286 /// <summary>
135287 /// Inserts an entity into a table.
136288 /// </summary>
@@ -378,7 +530,20 @@ private static bool TryGetValueIgnoreCase(Dictionary<string, object> row, string
378530 ArgumentNullException . ThrowIfNull ( value ) ;
379531 ArgumentNullException . ThrowIfNull ( targetType ) ;
380532
381- var underlying = Nullable . GetUnderlyingType ( targetType ) ?? targetType ;
533+ if ( value == DBNull . Value )
534+ {
535+ return Nullable . GetUnderlyingType ( targetType ) is not null || ! targetType . IsValueType
536+ ? null
537+ : throw new InvalidOperationException ( $ "Cannot map DBNull to non-nullable type '{ targetType . Name } '.") ;
538+ }
539+
540+ var nullableUnderlying = Nullable . GetUnderlyingType ( targetType ) ;
541+ var underlying = nullableUnderlying ?? targetType ;
542+
543+ if ( value is string textValue && string . IsNullOrEmpty ( textValue ) && nullableUnderlying is not null )
544+ {
545+ return null ;
546+ }
382547
383548 if ( underlying . IsInstanceOfType ( value ) )
384549 {
0 commit comments