Code Examples

Data Model - EmployeeProps Click to view data model used in all examples
Tag with priority for categorization.
/// <summary>
/// Tag with priority for categorization.
/// </summary>
public class Tag
{
    public string Name { get; set; } = string.Empty;
    public int Priority { get; set; }
    public string? Description { get; set; }
}

/// <summary>
/// Project metrics for analytics.
/// </summary>
[RedbScheme("ProjectMetrics")]
public class ProjectMetricsProps
{
    public long ProjectId { get; set; }
    public long? TasksCompleted { get; set; }
    public long? TasksTotal { get; set; }
    public long? BugsFixed { get; set; }
    public double? Budget { get; set; }
    public long? TeamSize { get; set; }
    public Tag[]? Tags { get; set; }
    public string[]? Technologies { get; set; }
}

/// <summary>
/// Address with city, street and building details.
/// </summary>
public class Address
{
    public string City { get; set; } = string.Empty;
    public string Street { get; set; } = string.Empty;
    public BuildingInfo? Building { get; set; }
}

/// <summary>
/// Building information with floor and amenities.
/// </summary>
public class BuildingInfo
{
    public int Floor { get; set; }
    public string Name { get; set; } = string.Empty;
    public string[]? Amenities { get; set; }
    public int[]? AccessCodes { get; set; }
    public string[]? ParkingSpots { get; set; }
    public int[]? ElevatorFloors { get; set; }
}

/// <summary>
/// Contact detail key-value pair.
/// </summary>
public class ContactDetail
{
    public string Label { get; set; } = string.Empty;
    public string Value { get; set; } = string.Empty;
}

/// <summary>
/// Contact information (email, phone, etc).
/// </summary>
public class Contact
{
    public string Type { get; set; } = string.Empty;
    public string Value { get; set; } = string.Empty;
    public bool IsVerified { get; set; }
    public int[]? NotificationHours { get; set; }
    public ContactDetail[]? Metadata { get; set; }
}

/// <summary>
/// Department info with tags, metrics and nested budget data.
/// </summary>
public class Department
{
    public string Name { get; set; } = string.Empty;
    public int HeadCount { get; set; }
    public string[] Projects { get; set; } = [];
    public Tag[] Leaders { get; set; } = [];
    public Dictionary<string, int>? BudgetByYear { get; set; }
}

/// <summary>
/// Employee props - main model for examples.
/// Demonstrates all supported types:
/// - Simple types (int, string, DateTime, long, decimal)
/// - Arrays (string[], int[])
/// - Business classes (Address with nested BuildingInfo)
/// - Array of business classes (Contact[])
/// - RedbObject references (CurrentProject, PastProjects[])
/// - Dictionary types (various key and value types)
/// </summary>
[RedbScheme("Employee")]
public class EmployeeProps
{
    // Basic info
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int Age { get; set; }
    public DateTime HireDate { get; set; }
    public string Position { get; set; } = string.Empty;
    public decimal Salary { get; set; }
    public string Department { get; set; } = string.Empty;
    public string? EmployeeCode { get; set; }

    // Skills and certifications (arrays)
    public string[]? Skills { get; set; }
    public int[]? SkillLevels { get; set; }
    public string[]? Certifications { get; set; }
    public int[]? CertificationYears { get; set; }

    // Addresses (business classes)
    public Address? HomeAddress { get; set; }
    public Address? WorkAddress { get; set; }
    public Address? EmergencyAddress { get; set; }

    // Contacts (array of business classes)
    public Contact[]? Contacts { get; set; }

    // Project references (RedbObject)
    public RedbObject<ProjectMetricsProps>? CurrentProject { get; set; }
    public RedbObject<ProjectMetricsProps>[]? PastProjects { get; set; }

    // Phone directory: extension -> phone number
    public Dictionary<string, string>? PhoneDirectory { get; set; }

    // Office locations by city
    public Dictionary<string, Address>? OfficeLocations { get; set; }

    // Bonus history by year
    public Dictionary<int, decimal>? BonusByYear { get; set; }

    // Department details with complex nested data (Pro)
    public Dictionary<string, Department>? DepartmentHistory { get; set; }

    // Composite key: (year, quarter) -> performance score (Pro)
    public Dictionary<(int Year, string Quarter), string>? PerformanceReviews { get; set; }

    // Project metrics by code (Pro)
    public Dictionary<string, RedbObject<ProjectMetricsProps>>? ProjectMetrics { get; set; }
}

/// <summary>
/// Department props for tree examples (corporate hierarchy).
/// Used with TreeRedbObject for organizational structure.
/// Similar to CategoryTestProps in ConsoleTest.
/// </summary>
[RedbScheme("Department")]
public class DepartmentProps
{
    /// <summary>Department name (e.g. "IT Department Moscow").</summary>
    public string Name { get; set; } = string.Empty;

    /// <summary>Description (e.g. "Software development team").</summary>
    public string Description { get; set; } = string.Empty;

    /// <summary>Is department active?</summary>
    public bool IsActive { get; set; } = true;

    /// <summary>Budget in USD.</summary>
    public decimal Budget { get; set; }

    /// <summary>Department code (e.g. "IT-MSK-DEV").</summary>
    public string Code { get; set; } = string.Empty;
}

/// <summary>
/// City props for list examples (linked objects).
/// Used as target object for RedbListItem.Object reference.
/// </summary>
[RedbScheme("City")]
public class CityProps
{
    /// <summary>City name.</summary>
    public string Name { get; set; } = string.Empty;
    
    /// <summary>Population count.</summary>
    public int Population { get; set; }
    
    /// <summary>Region or district.</summary>
    public string Region { get; set; } = string.Empty;
    
    /// <summary>Is capital city?</summary>
    public bool IsCapital { get; set; }
    
    /// <summary>GPS coordinates [lat, lon].</summary>
    public double[] Coordinates { get; set; } = [];
}

/// <summary>
/// Person props demonstrating ListItem fields.
/// Shows single ListItem and List of ListItems usage.
/// </summary>
[RedbScheme("Person")]
public class PersonProps
{
    /// <summary>Person name.</summary>
    public string Name { get; set; } = string.Empty;
    
    /// <summary>Age in years.</summary>
    public int Age { get; set; }
    
    /// <summary>Email address.</summary>
    public string Email { get; set; } = string.Empty;
    
    /// <summary>Single ListItem field (e.g. status from dictionary).</summary>
    public RedbListItem? Status { get; set; }
    
    /// <summary>Array of ListItems (e.g. roles from dictionary).</summary>
    public List<RedbListItem>? Roles { get; set; }
}

/// <summary>
/// Product with a unique business key (V4 [RedbUnique]): the SKU is unique per scheme,
/// enforced by the database over the hash of the canonical value. Used by E003.
/// </summary>
public class ProductProps
{
    /// <summary>Unique business key. NULL never participates in uniqueness.</summary>
    [RedbUnique]
    public string? Sku { get; set; }

    /// <summary>Display name (not unique).</summary>
    public string Title { get; set; } = string.Empty;

    /// <summary>Price in USD.</summary>
    public decimal Price { get; set; }
}

PVT prefilter: SQL with and without the cutting step

Dumps SQL for the cases the PVT prefilter is meant to handle, so the same run can be
compared with and without the cutting step. See docs/PVT_PREFILTER_PLAN.md.
The prefilter is opt-in. Run twice and diff:
dotnet run --project redb.Examples -- E202 (off)
REDB_PVT_PREFILTER=1 dotnet run --project redb.Examples -- E202 (on)
Each case also executes, and the example compares the returned id sets between the two
runs by writing them to the dump. The prefilter is a superset by construction, so the id
sets MUST be identical. A difference is a defect, not a tuning question.
Expected shapes when enabled:
top-level OR -> disjunction spliced into the _values scan (Row form)
top-level AND -> Row form too, when the single branch covers the whole pivot
unanalysable -> no prefilter at all, SQL identical to the disabled run
var sw = Stopwatch.StartNew();

// What the service actually runs with: Program.cs may set it without the environment variable.
var enabled = redb.Configuration.EnablePvtPrefilter;
var log = new StringBuilder();
var summary = new List<string>();
var withPrefilter = 0;

log.AppendLine($"-- EnablePvtPrefilter = {(enabled ? "true (enabled)" : "false (disabled)")}");
log.AppendLine();

async Task CaseAsync(string label, string expectation, IRedbQueryable<EmployeeProps> query)
{
    log.AppendLine("-----------------------------------------------------------------");
    log.AppendLine($"-- {label}");
    log.AppendLine($"-- expected when enabled: {expectation}");
    log.AppendLine("-----------------------------------------------------------------");

    string sql;
    try
    {
        sql = await query.ToSqlStringAsync();
    }
    catch (Exception ex)
    {
        log.AppendLine($"-- SQL BUILD FAIL: {ex.GetType().Name}: {ex.Message}");
        log.AppendLine();
        summary.Add($"{label}: SQL build failed");
        Console.WriteLine($"[BUILD-FAIL] {label}: {ex.Message}");
        return;
    }

    log.AppendLine(sql);

    // Console.WriteLine();
    // Console.WriteLine($"--- {label} (expected when enabled: {expectation}) ---");
    // Console.WriteLine(sql);

    // "pf" is the alias the planner gives the prefilter scan. The Row form is the extra
    // parenthesised group spliced into the _values scan; note it may hold a SINGLE branch
    // (a merged range), so matching on " OR " would miss it.
    // Semi form was removed after measuring 3x worse than nothing; this stays as a tripwire.
    var hasSemi = sql.Contains("FROM _values pf");
    var hasRow = sql.Contains("AND ((v._id_structure");
    var form = hasSemi ? "Semi" : hasRow ? "Row" : "none";
    if (form != "none") withPrefilter++;

    string ids;
    try
    {
        var rows = await query.ToListAsync();
        ids = rows.Count == 0
            ? "<empty>"
            : string.Join(",", rows.Select(r => r.Id).OrderBy(x => x));
    }
    catch (Exception ex)
    {
        ids = $"EXEC FAIL {ex.GetType().Name}: {ex.Message.Split('\n')[0]}";
    }

    log.AppendLine($"-- prefilter form: {form}");
    log.AppendLine($"-- ids: {ids}");
    log.AppendLine();

    summary.Add($"{label}: {form}");
    Console.WriteLine($"[{form,4}] {label}");
}

// Top-level OR, both branches selective. The tsum case: one search string, several
// fields. Row form, and with pg_trgm the LIKE finally reaches the column.
await CaseAsync(
    "OR: two string fields, same needle",
    "Row",
    redb.Query<EmployeeProps>()
        .Where(e => e.Position.Contains("Design") || e.Department.Contains("Design"))
        .Take(100));

// Top-level AND on a single field: two predicates merge into one selective range.
// Splitting them into an OR would give a tautology, hence the merge.
await CaseAsync(
    "AND: range on one field",
    "Row, single branch with both bounds merged",
    redb.Query<EmployeeProps>()
        .Where(e => e.Salary >= 60000m && e.Salary < 90000m)
        .Take(100));

// Top-level AND across fields. Only the most selective conjunct is pushed; the weak
// one (Position != "") stays above the aggregate where it belongs.
await CaseAsync(
    "AND: three fields, one selective",
    "none (pivot not covered: Position and Age have no branch)",
    redb.Query<EmployeeProps>()
        .Where(e => e.Position != "" && e.Age >= 30 && e.Salary > 70000m)
        .Take(100));

// One weak branch in an OR voids the group: a disjunction is never narrower than its
// widest term, so the extra scan would buy nothing.
await CaseAsync(
    "OR: one weak branch",
    "none (weakest branch below threshold)",
    redb.Query<EmployeeProps>()
        .Where(e => e.Position != "" || e.Salary > 70000m)
        .Take(100));

// Null check is not expressible as a row predicate. Under AND it is simply dropped
// and the other conjunct still gets pushed.
await CaseAsync(
    "AND: null check plus selective field",
    "none (pivot not covered: EmployeeCode has no branch)",
    redb.Query<EmployeeProps>()
        .Where(e => e.EmployeeCode == null && e.Salary > 70000m)
        .Take(100));

// Arrays are not single-row values. Under OR that voids the whole prefilter.
await CaseAsync(
    "OR: array branch",
    "none (array is not a row predicate)",
    redb.Query<EmployeeProps>()
        .Where(e => e.Skills!.Contains("C#") || e.Salary > 70000m)
        .Take(100));

// Sorting pulls Age into the pivot without putting it in the filter. The Row form
// would null that column out, so the guard suppresses the prefilter entirely.
//
// ThenByRedb(Id) is not decoration: Age has thousands of ties, and Take(100) over a tie
// returns whichever hundred the plan happens to produce. Without a unique tiebreak this
// case reported a difference between two runs of the SAME configuration, which reads as
// a prefilter defect and is not one. Id is a base field, so it adds nothing to the pivot
// and the guard still fires on Age.
await CaseAsync(
    "OR plus OrderBy on an uncovered field",
    "none (pivot coverage guard: Age comes from OrderBy)",
    redb.Query<EmployeeProps>()
        .Where(e => e.Position.Contains("Design") || e.Department.Contains("Design"))
        .OrderBy(e => e.Age)
        .ThenByRedb(o => o.Id)
        .Take(100));

// ---- dates -------------------------------------------------------
// Same algebra as numbers, different column (_datetimeoffset) and a different
// parameter round-trip. Worth its own cases: a DateTime that survives C# to Npgsql
// to the pivot but not into the prefilter branch would silently narrow the result.

// Closed range on one field. Two predicates, one structure, one merged branch.
await CaseAsync(
    "AND: date range on one field",
    "Row, single branch with both bounds merged",
    redb.Query<EmployeeProps>()
        .Where(e => e.HireDate >= new DateTime(2030, 1, 1) && e.HireDate < new DateTime(2031, 1, 1))
        .Take(100));

// Open-ended comparison, single leaf at the root.
await CaseAsync(
    "date: single greater-than",
    "Row, one branch",
    redb.Query<EmployeeProps>()
        .Where(e => e.HireDate > new DateTime(2048, 1, 1))
        .Take(100));

// Mixed types under OR: a date branch and a string branch in one disjunction.
await CaseAsync(
    "OR: date branch plus string branch",
    "Row, two branches on different columns",
    redb.Query<EmployeeProps>()
        .Where(e => e.HireDate > new DateTime(2050, 1, 1) || e.Position.Contains("Design"))
        .Take(100));

// Inequality on a date is not selective enough to earn a scan.
await CaseAsync(
    "date: not-equal",
    "none (NotEqual passes almost everything)",
    redb.Query<EmployeeProps>()
        .Where(e => e.HireDate != new DateTime(2030, 1, 1))
        .Take(100));

string? outPath = null;
try
{
    var dir = Path.Combine(AppContext.BaseDirectory, "Output");
    Directory.CreateDirectory(dir);
    outPath = Path.Combine(dir, enabled ? "pvt_prefilter_on.txt" : "pvt_prefilter_off.txt");
    await File.WriteAllTextAsync(outPath, log.ToString());
}
catch (Exception ex)
{
    Console.WriteLine($"[WARN] failed to write dump file: {ex.Message}");
}

sw.Stop();

// The run time above is the sum over all the queries of the example, each printed and then
// executed, so the count comes first: without it the figure reads as one slow query.
var output = new List<string>
{
    $"{summary.Count} queries in this example: SQL printed, then each executed",
    $"Prefilter: {(enabled ? "ENABLED" : "disabled")}, applied in {withPrefilter} of {summary.Count} cases"
};
output.AddRange(summary);
output.AddRange(new[]
{
    outPath != null ? $"Output: {outPath}" : "Output file: <not written>",
    "Diff pvt_prefilter_off.txt against pvt_prefilter_on.txt: id sets must match exactly"
});
SqlDebugPrefilterProPvtIRedbQueryable.ToSqlStringAsync
PG Pro 1126ms(11)
postgres.pro.repeat 756ms(11)
postgres.pro.cache 872ms(11)
MS Pro 2898ms(11)
mssql.pro.repeat 1696ms(11)
mssql.pro.cache 1666ms(11)
sqlite.pro 1672ms(11)
sqlite.pro.repeat 1422ms(11)
sqlite.pro.cache 1424ms(11)