Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agents/skills/verify-querykit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,14 @@ If the doctor fails and `qk rebuild` does not correct it, run `qk down` and then
## Drive

```bash
.agents/skills/verify-querykit/qk run <label> [--filter '<f>'] [--sort '<s>'] [--config <preset>] [--target memory|postgres|both] [--aggregate]
.agents/skills/verify-querykit/qk run <label> [--filter '<f>'] [--sort '<s>'] [--config <preset>] [--target memory|postgres|both] [--aggregate] [--culture <name>]
```

- `<label>` names the evidence files. Use letters, digits, `.`, `_`, and `-` only. Use a label that tells the feature, for example `filtering-in-operator`.
- `--filter` calls `ApplyQueryKitFilter`. `--sort` calls `ApplyQueryKitSort` after the filter.
- `--aggregate` calls `ApplyQueryKit(new QueryKitData { Filters, SortOrder, Configuration })` instead.
- `--config` selects a `QueryKitConfiguration` preset. The default is `none`. Run `qk configs` to list the presets. The presets are in `harness/Driver/Configs.cs`.
- `--culture` sets the thread culture of the driver, for example `de-DE`. The default is the culture of the machine.
- `--target` defaults to `both`. Use `both` for a proof. A difference between the two targets is a finding.

Put the filter in single quotes in the shell, because QueryKit strings use double quotes:
Expand Down
12 changes: 6 additions & 6 deletions .agents/skills/verify-querykit/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ In the commands below, `qk` means `.agents/skills/verify-querykit/qk`.

The driver model is `Recipe`. Every recipe has one `Author` and zero or more `Ingredients`. The seed data is fixed in `harness/Driver/Model.cs`.

| Title | Author.Name | Rating | Price | IsVegetarian | Visibility | CreatedAt (UTC) | DateOfOrigin | Directions | Tags | Ingredients (Name:Stock) |
|---|---|---|---|---|---|---|---|---|---|---|
| Pancakes | Julia Child | 5 | 4.50 | true | Public (1) | 2024-01-15 08:00 | 1900-01-01 | Whisk and fry | breakfast, sweet | flour:10, egg:0 |
| Beef Stew | Gordon Ramsay | 3 | 12.00 | false | Public (1) | 2024-03-01 18:30 | null | Simmer for hours | dinner | beef:2, salt:50 |
| Salt Bread | Julia Child | 4 | 3.25 | true | Private (2) | 2024-06-10 12:00 | 1950-05-20 | Knead and bake | bread, Winner | salt:5, flour:3 |
| Plain Water | Anonymous | 1 | 0.00 | true | Private (2) | 2023-12-31 23:59 | null | null | (none) | (none) |
| Title | Author.Name | Rating | Price | IsVegetarian | Visibility | CreatedAt (UTC) | DateOfOrigin | Sku | Serving | ServeTime | Directions | Tags | Ingredients (Name:Stock) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Pancakes | Julia Child | 5 | 4.50 | true | Public (1) | 2024-01-15 08:00 | 1900-01-01 | 001 | Warm, with syrup | 08:30:00.5 | Whisk and fry | breakfast, sweet | flour:10, egg:0 |
| Beef Stew | Gordon Ramsay | 3 | 12.00 | false | Public (1) | 2024-03-01 18:30 | null | 002 | Hot | 18:00:00 | Simmer for hours | dinner | beef:2, salt:50 |
| Salt Bread | Julia Child | 4 | 3.25 | true | Private (2) | 2024-06-10 12:00 | 1950-05-20 | 003 | Sliced | 12:15:30.25 | Knead and bake | bread, Winner | salt:5, flour:3 |
| Plain Water | Anonymous | 1 | 0.00 | true | Private (2) | 2023-12-31 23:59 | null | 004 | null | null | null | (none) | (none) |

## Driving conventions

Expand Down
10 changes: 9 additions & 1 deletion .agents/skills/verify-querykit/harness/Driver/Configs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ public static class Configs
s.DerivedProperty<Recipe>(x => x.Rating >= 4).HasQueryName("top_rated");
})),

["custom-operation"] = ("CustomOperation: total_stock_above = sum of Ingredients.Stock > value.",
["custom-operation"] = ("CustomOperation: total_stock_above = sum of Ingredients.Stock > value. sku_is = Sku == (string)value.",
() => new QueryKitConfiguration(s =>
{
s.CustomOperation<Recipe>((x, op, value) => x.Ingredients.Sum(i => i.Stock) > (int)value)
.HasQueryName("total_stock_above");
s.CustomOperation<Recipe>((x, op, value) => x.Sku == (string)value)
.HasQueryName("sku_is");
})),

["word-operators"] = ("Custom operators: eq neq gt gte lt lte ct sw ew, and/or, case-insensitive appendix i.",
Expand All @@ -52,6 +54,12 @@ public static class Configs
s.CaseInsensitiveAppendix = "i";
})),

["hidden-price"] = ("Price PreventFilter and PreventSort, with HasQueryName(\"cost\").",
() => new QueryKitConfiguration(s =>
{
s.Property<Recipe>(x => x.Price).HasQueryName("cost").PreventFilter().PreventSort();
})),

["allow-unknown"] = ("AllowUnknownProperties = true.",
() => new QueryKitConfiguration(s => s.AllowUnknownProperties = true)),

Expand Down
20 changes: 15 additions & 5 deletions .agents/skills/verify-querykit/harness/Driver/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class Recipe
public Visibility Visibility { get; set; }
public DateTime CreatedAt { get; set; }
public DateOnly? DateOfOrigin { get; set; }
public string Sku { get; set; } = "";
public string? Serving { get; set; }
public TimeOnly? ServeTime { get; set; }
public List<string> Tags { get; set; } = [];
public Guid AuthorId { get; set; }
public Author Author { get; set; } = null!;
Expand Down Expand Up @@ -61,25 +64,29 @@ public static List<Recipe> Build()
Recipe(1, "Pancakes", julia, rating: 5, price: 4.50m, vegetarian: true, Visibility.Public,
created: new DateTime(2024, 1, 15, 8, 0, 0, DateTimeKind.Utc), origin: new DateOnly(1900, 1, 1),
directions: "Whisk and fry", tags: ["breakfast", "sweet"],
ingredients: [("flour", 10), ("egg", 0)]),
ingredients: [("flour", 10), ("egg", 0)],
sku: "001", serving: "Warm, with syrup", serveTime: new TimeOnly(8, 30, 0, 500)),
Recipe(2, "Beef Stew", gordon, rating: 3, price: 12.00m, vegetarian: false, Visibility.Public,
created: new DateTime(2024, 3, 1, 18, 30, 0, DateTimeKind.Utc), origin: null,
directions: "Simmer for hours", tags: ["dinner"],
ingredients: [("beef", 2), ("salt", 50)]),
ingredients: [("beef", 2), ("salt", 50)],
sku: "002", serving: "Hot", serveTime: new TimeOnly(18, 0, 0)),
Recipe(3, "Salt Bread", julia, rating: 4, price: 3.25m, vegetarian: true, Visibility.Private,
created: new DateTime(2024, 6, 10, 12, 0, 0, DateTimeKind.Utc), origin: new DateOnly(1950, 5, 20),
directions: "Knead and bake", tags: ["bread", "Winner"],
ingredients: [("salt", 5), ("flour", 3)]),
ingredients: [("salt", 5), ("flour", 3)],
sku: "003", serving: "Sliced", serveTime: new TimeOnly(12, 15, 30, 250)),
Recipe(4, "Plain Water", anon, rating: 1, price: 0m, vegetarian: true, Visibility.Private,
created: new DateTime(2023, 12, 31, 23, 59, 0, DateTimeKind.Utc), origin: null,
directions: null, tags: [],
ingredients: []),
ingredients: [],
sku: "004", serving: null, serveTime: null),
];
}

private static Recipe Recipe(int n, string title, Author author, int rating, decimal price, bool vegetarian,
Visibility visibility, DateTime created, DateOnly? origin, string? directions, List<string> tags,
(string Name, int Stock)[] ingredients)
(string Name, int Stock)[] ingredients, string sku, string? serving, TimeOnly? serveTime)
{
var id = G(n);
return new Recipe
Expand All @@ -93,6 +100,9 @@ private static Recipe Recipe(int n, string title, Author author, int rating, dec
Visibility = visibility,
CreatedAt = created,
DateOfOrigin = origin,
Sku = sku,
Serving = serving,
ServeTime = serveTime,
Tags = tags,
AuthorId = author.Id,
Author = author,
Expand Down
17 changes: 15 additions & 2 deletions .agents/skills/verify-querykit/harness/Driver/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
var target = "both";
var configName = "none";
var aggregate = false;
string? culture = null;

for (var i = 0; i < args.Length; i++)
{
Expand All @@ -24,16 +25,24 @@
case "--target": target = args[++i]; break;
case "--config": configName = args[++i]; break;
case "--aggregate": aggregate = true; break;
case "--culture": culture = args[++i]; break;
case "--list-configs":
foreach (var (name, preset) in Configs.All) Console.WriteLine($"{name,-18} {preset.Description}");
return 0;
default:
Console.Error.WriteLine($"Unknown argument '{args[i]}'.");
Console.Error.WriteLine("Usage: qk-driver [--filter <f>] [--sort <s>] [--target memory|postgres|both] [--config <preset>] [--aggregate] | --list-configs");
Console.Error.WriteLine("Usage: qk-driver [--filter <f>] [--sort <s>] [--target memory|postgres|both] [--config <preset>] [--aggregate] [--culture <name>] | --list-configs");
return 1;
}
}

if (culture is not null)
{
// Simulates a consumer app whose thread culture is not en-US, for example de-DE.
System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo(culture);
System.Globalization.CultureInfo.CurrentUICulture = System.Globalization.CultureInfo.CurrentCulture;
}

if (!Configs.All.TryGetValue(configName, out var configPreset))
{
Console.Error.WriteLine($"Unknown config preset '{configName}'. Run with --list-configs.");
Expand Down Expand Up @@ -72,7 +81,8 @@

var doc = new JsonObject
{
["input"] = new JsonObject { ["filter"] = filter, ["sort"] = sort, ["config"] = configName, ["api"] = api },
["input"] = new JsonObject { ["filter"] = filter, ["sort"] = sort, ["config"] = configName, ["api"] = api,
["culture"] = System.Globalization.CultureInfo.CurrentCulture.Name, ["timeZone"] = TimeZoneInfo.Local.Id },
["expression"] = DescribeExpression(),
["results"] = results,
};
Expand Down Expand Up @@ -162,6 +172,9 @@ void AddRows(JsonObject result, List<Recipe> recipes)
["visibility"] = r.Visibility.ToString(),
["createdAt"] = r.CreatedAt.ToString("O"),
["dateOfOrigin"] = r.DateOfOrigin?.ToString("O"),
["sku"] = r.Sku,
["serving"] = r.Serving,
["serveTime"] = r.ServeTime?.ToString("O"),
["directions"] = r.Directions,
["tags"] = new JsonArray(r.Tags.Select(t => (JsonNode?)t).ToArray()),
["ingredients"] = new JsonArray(r.Ingredients.OrderBy(x => x.Name)
Expand Down
4 changes: 3 additions & 1 deletion QueryKit.IntegrationTests/TestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public async Task InitializeAsync()
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Consts.Testing.IntegrationTestingEnvName
EnvironmentName = Consts.Testing.IntegrationTestingEnvName,
// the file watcher for config reload can hang on start on macOS
Args = ["--hostBuilder:reloadConfigOnChange=false"]
});

_dbContainer = new PostgreSqlBuilder("postgres:15.1").Build();
Expand Down
59 changes: 58 additions & 1 deletion QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,31 @@ public async Task can_filter_by_string_for_collection_with_count()
recipes.Count.Should().Be(1);
recipes[0].Id.Should().Be(fakeRecipeOne.Id);
}

[Fact]
public async Task can_filter_by_collection_count_less_than_or_equal()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var prefix = $"count {Guid.NewGuid()} ";
var recipeWithNone = new FakeRecipeBuilder().WithTitle($"{prefix}none").Build();
var recipeWithOne = new FakeRecipeBuilder().WithTitle($"{prefix}one").Build();
recipeWithOne.AddIngredient(new FakeIngredientBuilder().Build());
var recipeWithTwo = new FakeRecipeBuilder().WithTitle($"{prefix}two").Build();
recipeWithTwo.AddIngredient(new FakeIngredientBuilder().Build());
recipeWithTwo.AddIngredient(new FakeIngredientBuilder().Build());
await testingServiceScope.InsertAsync(recipeWithNone, recipeWithOne, recipeWithTwo);

// Act
var queryableRecipes = testingServiceScope.DbContext().Recipes;
var recipes = await queryableRecipes
.Where(x => x.Title.StartsWith(prefix))
.ApplyQueryKitFilter("Ingredients #<= 1")
.ToListAsync();

// Assert
recipes.Select(x => x.Id).Should().BeEquivalentTo(new[] { recipeWithNone.Id, recipeWithOne.Id });
}

[Fact]
public async Task can_filter_by_string_for_collection_contains()
Expand Down Expand Up @@ -1973,7 +1998,8 @@ public async Task can_filter_with_property_to_property_child_properties()

// Act
var queryableRecipes = testingServiceScope.DbContext().Recipes
.Include(x => x.Author);
.Include(x => x.Author)
.Where(x => x.Id == fakeRecipe.Id || x.Id == differentRecipe.Id);
var appliedQueryable = queryableRecipes.ApplyQueryKitFilter(input);
var recipes = await appliedQueryable.ToListAsync();

Expand Down Expand Up @@ -4258,4 +4284,35 @@ public async Task bare_property_name_on_the_right_is_still_a_property_to_propert
people.Should().ContainSingle(p => p.Id == fakePersonOne.Id);
}

[Fact]
public async Task can_apply_query_kit_data()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var prefix = $"data {Guid.NewGuid()} ";
var low = new FakeRecipeBuilder().WithTitle($"{prefix}low").WithRating(1).Build();
var high = new FakeRecipeBuilder().WithTitle($"{prefix}high").WithRating(5).Build();
var middle = new FakeRecipeBuilder().WithTitle($"{prefix}middle").WithRating(3).Build();
await testingServiceScope.InsertAsync(low, high, middle);

var queryKitData = new QueryKitData
{
Filters = "score >= 3",
SortOrder = "score desc",
Configuration = new QueryKitConfiguration(config =>
{
config.Property<Recipe>(x => x.Rating).HasQueryName("score");
})
};

// Act
var queryableRecipes = testingServiceScope.DbContext().Recipes;
var recipes = await queryableRecipes
.Where(x => x.Title.StartsWith(prefix))
.ApplyQueryKit(queryKitData)
.ToListAsync();

// Assert
recipes.Select(x => x.Id).Should().Equal(high.Id, middle.Id);
}
}
Loading
Loading