-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathFunctionalSqlSyntaxTests.cs
More file actions
352 lines (298 loc) · 13.1 KB
/
FunctionalSqlSyntaxTests.cs
File metadata and controls
352 lines (298 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
namespace SharpCoreDB.Functional.Tests;
using Microsoft.Extensions.DependencyInjection;
using SharpCoreDB.Interfaces;
using static SharpCoreDB.Functional.Prelude;
/// <summary>
/// Tests for the Functional SQL syntax extensions: <c>OPTIONALLY FROM</c>, <c>IS SOME</c>,
/// <c>IS NONE</c>, <c>UNWRAP</c>, and <c>MATCH SOME/NONE</c>.
/// Run these tests to verify the functional SQL layer works end-to-end.
/// </summary>
public sealed class FunctionalSqlSyntaxTests : IDisposable
{
private readonly Database _db;
private readonly FunctionalDb _fdb;
private readonly string _dbPath;
private readonly ServiceProvider _serviceProvider;
public FunctionalSqlSyntaxTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"functional_sql_test_{Guid.NewGuid():N}");
var services = new ServiceCollection();
services.AddSharpCoreDB();
_serviceProvider = services.BuildServiceProvider();
var factory = _serviceProvider.GetRequiredService<DatabaseFactory>();
_db = (Database)factory.Create(_dbPath, "testPassword");
_db.ExecuteSQL("CREATE TABLE Users (Id INTEGER, Name TEXT, Email TEXT, ManagerId INTEGER)");
_db.ExecuteBatchSQL([
"INSERT INTO Users VALUES (1, 'Alice', 'alice@example.com', NULL)",
"INSERT INTO Users VALUES (2, 'Bob', '', 1)",
"INSERT INTO Users VALUES (3, 'Charlie', 'charlie@example.com', 99)",
"INSERT INTO Users VALUES (4, 'Diana', '', NULL)",
"INSERT INTO Users VALUES (5, 'Eve', 'eve@example.com', 1)"
]);
_db.Flush();
_fdb = _db.Functional();
}
public void Dispose()
{
_db.Dispose();
_serviceProvider.Dispose();
try { Directory.Delete(_dbPath, true); } catch { /* best effort */ }
}
// ─────────────────────────────────────────────────────────────────────
// Translator unit tests
// ─────────────────────────────────────────────────────────────────────
[Fact]
public void IsFunctionalSql_WithStandardSql_ReturnsFalse()
{
Assert.False(FunctionalSqlTranslator.IsFunctionalSql(
"SELECT Id, Name FROM Users WHERE Id = 1"));
}
[Fact]
public void IsFunctionalSql_WithOptionallyFrom_ReturnsTrue()
{
Assert.True(FunctionalSqlTranslator.IsFunctionalSql(
"SELECT Id, Name OPTIONALLY FROM Users"));
}
[Fact]
public void IsFunctionalSql_WithIsSome_ReturnsTrue()
{
Assert.True(FunctionalSqlTranslator.IsFunctionalSql(
"SELECT Id FROM Users WHERE Email IS SOME"));
}
[Fact]
public void IsFunctionalSql_WithIsNone_ReturnsTrue()
{
Assert.True(FunctionalSqlTranslator.IsFunctionalSql(
"SELECT Id FROM Users WHERE Email IS NONE"));
}
[Fact]
public void Translate_OptionallyFrom_ProducesStandardFrom()
{
// Arrange
var translator = new FunctionalSqlTranslator();
// Act
var result = translator.Translate("SELECT Id, Name OPTIONALLY FROM Users");
// Assert
Assert.True(result.IsOptional);
Assert.Equal("SELECT Id, Name FROM Users", result.StandardSql);
}
[Fact]
public void Translate_IsSome_ProducesNotNullAndNotEmpty()
{
// Arrange
var translator = new FunctionalSqlTranslator();
// Act
var result = translator.Translate(
"SELECT Id, Name FROM Users WHERE Email IS SOME");
// Assert
Assert.DoesNotContain("IS SOME", result.StandardSql);
Assert.DoesNotContain("MATCH SOME", result.StandardSql);
Assert.Single(result.SomeColumns);
Assert.Equal("Email", result.SomeColumns[0]);
}
[Fact]
public void Translate_IsNone_ProducesIsNullOrEmpty()
{
// Arrange
var translator = new FunctionalSqlTranslator();
// Act
var result = translator.Translate(
"SELECT Id, Name FROM Users WHERE Email IS NONE");
// Assert
Assert.DoesNotContain("IS NONE", result.StandardSql);
Assert.DoesNotContain("MATCH NONE", result.StandardSql);
Assert.Single(result.NoneColumns);
Assert.Equal("Email", result.NoneColumns[0]);
}
[Fact]
public void Translate_Unwrap_ProducesColumnAlias()
{
// Arrange
var translator = new FunctionalSqlTranslator();
// Act
var result = translator.Translate(
"SELECT Id, UNWRAP Email AS SafeEmail DEFAULT 'none' OPTIONALLY FROM Users");
// Assert
Assert.True(result.IsOptional);
Assert.Contains("Email AS SafeEmail", result.StandardSql);
Assert.DoesNotContain("UNWRAP", result.StandardSql);
Assert.Single(result.UnwrapMappings);
Assert.Equal("Email", result.UnwrapMappings[0].Column);
Assert.Equal("SafeEmail", result.UnwrapMappings[0].Alias);
Assert.Equal("none", result.UnwrapMappings[0].DefaultValue);
}
[Fact]
public void Translate_CombinedSyntax_ParsesAllKeywords()
{
// Arrange
var translator = new FunctionalSqlTranslator();
var sql = "SELECT Id, Name, UNWRAP Email AS SafeEmail DEFAULT 'no-email' " +
"OPTIONALLY FROM Users WHERE Email IS SOME";
// Act
var result = translator.Translate(sql);
// Assert
Assert.True(result.IsOptional);
Assert.Single(result.SomeColumns);
Assert.Single(result.UnwrapMappings);
Assert.DoesNotContain("OPTIONALLY", result.StandardSql);
Assert.DoesNotContain("UNWRAP", result.StandardSql);
Assert.DoesNotContain("IS SOME", result.StandardSql);
}
[Fact]
public void Translate_MatchSome_TranslatesLikeIsSome()
{
// Arrange
var translator = new FunctionalSqlTranslator();
// Act
var result = translator.Translate(
"SELECT Id FROM Users WHERE MATCH SOME Email");
// Assert
Assert.DoesNotContain("MATCH SOME", result.StandardSql);
Assert.Single(result.SomeColumns);
}
// ─────────────────────────────────────────────────────────────────────
// End-to-end integration tests
// ─────────────────────────────────────────────────────────────────────
[Fact]
public async Task OptionallyFrom_WrapsEveryRowInOption()
{
// Act
var results = await _fdb.ExecuteFunctionalSqlAsync<UserDto>(
"SELECT Id, Name, Email OPTIONALLY FROM Users");
// Assert — all 5 users returned, each wrapped in Option
Assert.True(results.Count >= 5);
foreach (var opt in results)
{
Assert.True(opt.IsSome, "Every mapped row should be Some");
}
}
[Fact]
public async Task IsSome_FiltersOutNullAndEmptyEmails()
{
// Act — only users with a real email
var results = await _fdb.ExecuteFunctionalSqlAsync<Dictionary<string, object>>(
"SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS SOME");
// Assert — Alice, Charlie, Eve have emails; Bob and Diana do not
var names = new List<string>();
foreach (var opt in results)
{
opt.IfSome(row => names.Add(Convert.ToString(row["Name"], System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty));
}
Assert.Contains("Alice", names);
Assert.Contains("Charlie", names);
Assert.Contains("Eve", names);
Assert.DoesNotContain("Bob", names);
Assert.DoesNotContain("Diana", names);
}
[Fact]
public async Task IsNone_ReturnsOnlyRowsWithMissingEmail()
{
// Act — only users WITHOUT email
var results = await _fdb.ExecuteFunctionalSqlAsync<Dictionary<string, object>>(
"SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS NONE");
// Assert — Bob and Diana have no email
var names = new List<string>();
foreach (var opt in results)
{
opt.IfSome(row => names.Add(Convert.ToString(row["Name"], System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty));
}
Assert.Contains("Bob", names);
Assert.Contains("Diana", names);
Assert.DoesNotContain("Alice", names);
Assert.DoesNotContain("Eve", names);
}
[Fact]
public async Task IsSome_WithManagerId_FiltersNullForeignKeys()
{
// Act — only users who have a manager assigned
var results = await _fdb.ExecuteFunctionalSqlAsync<Dictionary<string, object>>(
"SELECT Id, Name, ManagerId OPTIONALLY FROM Users WHERE ManagerId IS SOME");
// Assert — Bob (1), Charlie (99), Eve (1) have ManagerId; Alice, Diana do not
var names = new List<string>();
foreach (var opt in results)
{
opt.IfSome(row => names.Add(Convert.ToString(row["Name"], System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty));
}
Assert.Contains("Bob", names);
Assert.Contains("Charlie", names);
Assert.Contains("Eve", names);
Assert.DoesNotContain("Alice", names);
Assert.DoesNotContain("Diana", names);
}
[Fact]
public async Task ExecuteFunctionalSqlUnwrapped_ReturnsFlatSequence()
{
// Act — convenience method that unwraps Option
var users = await _fdb.ExecuteFunctionalSqlUnwrappedAsync<Dictionary<string, object>>(
"SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS SOME");
// Assert — direct access, no Option wrapping
Assert.True(users.Count >= 3);
Assert.All(users, u =>
{
var email = Convert.ToString(u["Email"], System.Globalization.CultureInfo.InvariantCulture);
Assert.False(string.IsNullOrEmpty(email));
});
}
[Fact]
public async Task StandardSql_StillWorksViaFunctionalApi()
{
// Act — standard SQL without functional keywords still works
var results = await _fdb.ExecuteFunctionalSqlAsync<UserDto>(
"SELECT Id, Name FROM Users WHERE Id = 1");
// Assert — single result, not flagged as optional
Assert.Single(results);
var user = results.First();
Assert.True(user.IsSome);
Assert.Equal("Alice", user.Match(Some: u => u.Name, None: () => "fail"));
}
[Fact]
public async Task CombinedQuery_IsSomeAndIsNone_WorksOnDifferentColumns()
{
// Act — users who HAVE email but DO NOT have a manager
var results = await _fdb.ExecuteFunctionalSqlUnwrappedAsync<Dictionary<string, object>>(
"SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS SOME AND ManagerId IS NONE");
// Assert — only Alice (has email, no manager)
Assert.Single(results);
var first = results.First();
Assert.Equal("Alice", Convert.ToString(first["Name"], System.Globalization.CultureInfo.InvariantCulture));
}
[Fact]
public async Task EmptyResult_ReturnsEmptySequence()
{
// Act — impossible filter
var results = await _fdb.ExecuteFunctionalSqlAsync<UserDto>(
"SELECT Id, Name OPTIONALLY FROM Users WHERE Id = 999");
// Assert
Assert.True(results.Count == 0);
}
[Fact]
public async Task OptionChaining_WorksWithFunctionalSqlResults()
{
// Act — get results and chain Option operations
var results = await _fdb.ExecuteFunctionalSqlAsync<UserDto>(
"SELECT Id, Name, Email OPTIONALLY FROM Users WHERE Email IS SOME");
// Chain: extract emails, provide defaults
var emails = new List<string>();
foreach (var opt in results)
{
var email = opt
.Map(u => u.Email)
.Bind(e => string.IsNullOrEmpty(e) ? Option<string>.None : Option<string>.Some(e))
.IfNone("no-email");
emails.Add(email);
}
// All should be real emails (IS SOME filtered out empties)
Assert.All(emails, e => Assert.NotEqual("no-email", e));
Assert.All(emails, e => Assert.Contains("@", e));
}
// ─────────────────────────────────────────────────────────────────────
// DTOs
// ─────────────────────────────────────────────────────────────────────
private sealed class UserDto
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public int? ManagerId { get; set; }
}
}