Code Examples

Learn RedBase by example. Browse working code samples for LINQ queries, bulk operations, tree structures, and more.

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)

Seed volume: employees as a reporting tree

Seeds a large company: Employee objects arranged as a reporting tree.
Every employee but the first reports to a manager, and every manager has
direct reports, so 100000 employees form a tree six levels deep.
The same objects answer both kinds of query:
redb.Query<EmployeeProps>().Where(e => e.Salary > 90000) // everyone
redb.TreeQuery<EmployeeProps>(managerId).Where(e => e.Salary > 90000) // one manager's people
dotnet run --project redb.Examples -- E203
REDB_SEED_COUNT=20000 dotnet run --project redb.Examples -- E203
The whole-suite sweep never seeds. Seeding runs only when the example is NAMED on the
command line (default 100000 employees). REDB_SEED_COUNT overrides the size (0 disables even an
explicit run), REDB_SEED_BATCH the batch (default 1000).
Objects have the shape E000 builds: about 60 _values rows each, with arrays, nested classes
and dictionaries, so 100000 employees land around 6M rows in _values.
This example WRITES a lot. It appends and never clears: every run adds one more company with
its own head, so running it twice doubles the data. It ends by refreshing planner statistics
through redb.Maintenance.AnalyzeAsync(); after a bulk load the planner's estimates are
stale until that is done.
// Seeding volume is a decision, not a side effect. Being NAMED on the command line is
// that decision; being reached by the whole-suite sweep is not - the sweep always skips.
var isExplicitRun = ExplicitlyRequestedIds?.Contains("E203") == true;
var total = ReadInt("REDB_SEED_COUNT", isExplicitRun ? 100_000 : 0);
var batchSize = ReadInt("REDB_SEED_BATCH", 1_000);

if (total == 0)
{
    return Ok("E203", "Seed volume: employees as a reporting tree", ExampleTier.Pro, 0,
    [
        "Skipped: the whole-suite sweep never seeds.",
        "To seed: dotnet run --project redb.Examples -- E203   (default 100000)",
        "REDB_SEED_COUNT overrides the size, REDB_SEED_BATCH the batch (default 1000).",
        "100000 employees land around 6M rows in _values and take ~20 minutes.",
        "It appends and never clears, so running it twice doubles the data."
    ]);
}

if (total < 0 || batchSize <= 0)
    return Fail("E203", "Seed volume: employees as a reporting tree", ExampleTier.Pro, 0,
        "REDB_SEED_COUNT must not be negative and REDB_SEED_BATCH must be positive");

// Continue numbering after whatever is already there, so names and codes stay unique
// across runs.
var existing = await redb.Query<EmployeeProps>().CountAsync();

Console.WriteLine();
Console.WriteLine($"Seeding {total:N0} employees in batches of {batchSize:N0}, {DirectReports} direct reports per manager");
Console.WriteLine($"Already in scheme: {existing:N0}. New objects start at index {existing:N0}.");
Console.WriteLine($"Expect roughly {total * 60L:N0} rows in _values. This appends, it never clears.");
Console.WriteLine();

var sw = Stopwatch.StartNew();

// All ids up front: a manager's id has to be known before the reports are created.
var ids = await redb.Context.NextObjectIdBatchAsync(total);

var batchTimer = new Stopwatch();
var seeded = 0;
var batches = 0;

// Bulk-load mode: the plain secondary indexes of _values and _objects go before the load and are
// rebuilt after it, over the whole tables. Keys stay: primary keys, unique indexes and the foreign
// keys resting on them check every row as usual. Dropping and rebuilding are inside the measured time.
var droppedIndexes = await SetAsidePlainIndexesAsync(redb);
var rebuildTimer = new Stopwatch();
try
{
    while (seeded < total)
    {
        var take = Math.Min(batchSize, total - seeded);

        batchTimer.Restart();
        var employees = E000_BulkInsert.CreateEmployees(take, existing + seeded);
        for (var i = 0; i < employees.Count; i++)
        {
            // Position n reports to position (n - 1) / DirectReports; position 0 heads the
            // company. A manager always comes earlier than the reports, so it is already saved
            // or sits in the same batch.
            var position = seeded + i;
            employees[i].id = ids[position];
            employees[i].parent_id = position == 0 ? null : ids[(position - 1) / DirectReports];
        }

        await redb.AddNewObjectsAsync(employees.Cast<IRedbObject<EmployeeProps>>().ToList());
        batchTimer.Stop();

        seeded += employees.Count;
        batches++;

        var pct = seeded * 100.0 / total;
        var overallRate = seeded * 1000.0 / Math.Max(sw.ElapsedMilliseconds, 1);
        var remaining = overallRate > 0
            ? TimeSpan.FromSeconds((total - seeded) / overallRate)
            : TimeSpan.Zero;

        Console.WriteLine(
            $"[{seeded,8:N0}/{total:N0}] {pct,5:F1}%  " +
            $"batch {batchTimer.ElapsedMilliseconds,6:N0} ms  " +
            $"avg {overallRate,7:F0} obj/s  " +
            $"elapsed {sw.Elapsed:hh\\:mm\\:ss}  " +
            $"ETA {remaining:hh\\:mm\\:ss}");
    }
}
finally
{
    // Also after a failed batch: a database left without its indexes would slow every later query.
    rebuildTimer.Start();
    await RestoreIndexesAsync(redb, droppedIndexes);
    rebuildTimer.Stop();
}

sw.Stop();

var finalCount = await redb.Query<EmployeeProps>().CountAsync();
var rate = seeded * 1000.0 / Math.Max(sw.ElapsedMilliseconds, 1);

// A bulk load leaves the planner's statistics behind reality. The Maintenance facade runs
// the provider's own refresh (ANALYZE / sp_updatestats / PRAGMA analysis_limit + ANALYZE),
// so the seed hands back a database that is ready to be queried and measured.
Console.WriteLine();
Console.WriteLine("Refreshing planner statistics (redb.Maintenance.AnalyzeAsync)...");
var analyzeSw = Stopwatch.StartNew();
await redb.Maintenance.AnalyzeAsync();
analyzeSw.Stop();
Console.WriteLine();

var levels = 1;
for (long reach = 1, width = 1; reach < total; levels++)
{
    width *= DirectReports;
    reach += width;
}
DropPostgresIndexesAsync
private static async Task<List<string>> DropPostgresIndexesAsync(IRedbService redb)
{
    const string plainIndexes = """
        FROM pg_index x
        JOIN pg_class i ON i.oid = x.indexrelid
        JOIN pg_class t ON t.oid = x.indrelid
        JOIN pg_namespace n ON n.oid = t.relnamespace
        WHERE n.nspname = current_schema()
          AND t.relname IN ('_values', '_objects')
          AND NOT x.indisunique
          AND NOT EXISTS (SELECT 1 FROM pg_constraint c WHERE c.conindid = x.indexrelid)
        """;

    var definitions = await redb.Context.QueryScalarListAsync<string>(
        $"SELECT pg_get_indexdef(x.indexrelid) {plainIndexes} ORDER BY i.relname");
    var names = await redb.Context.QueryScalarListAsync<string>(
        $"SELECT format('%I', i.relname) {plainIndexes} ORDER BY i.relname");

    Console.WriteLine($"Bulk-load mode: dropping {names.Count} plain indexes of _values and _objects (keys stay).");
    foreach (var name in names)
        await redb.Context.ExecuteAsync($"DROP INDEX IF EXISTS {name}");

    return definitions;
}
DisableSqlServerIndexesAsync
private static async Task<List<string>> DisableSqlServerIndexesAsync(IRedbService redb)
{
    var targets = await redb.Context.QueryScalarListAsync<string>("""
        SELECT QUOTENAME(i.name) + ' ON ' + QUOTENAME(s.name) + '.' + QUOTENAME(t.name)
        FROM sys.indexes i
        JOIN sys.tables t ON t.object_id = i.object_id
        JOIN sys.schemas s ON s.schema_id = t.schema_id
        WHERE s.name = SCHEMA_NAME()
          AND t.name IN ('_values', '_objects')
          AND i.type = 2
          AND i.is_unique = 0
          AND i.is_primary_key = 0
          AND i.is_unique_constraint = 0
          AND i.is_disabled = 0
        ORDER BY i.name
        """);

    Console.WriteLine($"Bulk-load mode: disabling {targets.Count} plain indexes of _values and _objects (keys stay).");
    foreach (var target in targets)
        await redb.Context.ExecuteAsync($"ALTER INDEX {target} DISABLE");

    return targets.Select(target => $"ALTER INDEX {target} REBUILD").ToList();
}
DropSqliteIndexesAsync
private static async Task<List<string>> DropSqliteIndexesAsync(IRedbService redb)
{
    const string plainIndexes = """
        FROM sqlite_master
        WHERE type = 'index'
          AND tbl_name IN ('_values', '_objects')
          AND sql IS NOT NULL
          AND upper(sql) NOT LIKE 'CREATE UNIQUE%'
        """;

    var definitions = await redb.Context.QueryScalarListAsync<string>($"SELECT sql {plainIndexes} ORDER BY name");
    var names = await redb.Context.QueryScalarListAsync<string>(
        $"SELECT '\"' || replace(name, '\"', '\"\"') || '\"' {plainIndexes} ORDER BY name");

    Console.WriteLine($"Bulk-load mode: dropping {names.Count} plain indexes of _values and _objects (keys stay).");
    foreach (var name in names)
        await redb.Context.ExecuteAsync($"DROP INDEX IF EXISTS {name}");

    return definitions;
}
RestoreIndexesAsync
private static async Task RestoreIndexesAsync(IRedbService redb, List<string> restoreStatements)
{
    if (restoreStatements.Count == 0)
        return;

    Console.WriteLine();
    Console.WriteLine($"Bulk-load mode: rebuilding {restoreStatements.Count} indexes...");
    var sw = Stopwatch.StartNew();
    foreach (var statement in restoreStatements)
        await redb.Context.ExecuteAsync(statement);
    sw.Stop();
    Console.WriteLine($"Bulk-load mode: indexes rebuilt in {sw.ElapsedMilliseconds:N0} ms");
}
ReadInt
private static int ReadInt(string name, int fallback)
{
    var raw = Environment.GetEnvironmentVariable(name);
    return int.TryParse(raw, out var parsed) ? parsed : fallback;
}
SetupBulkInsertTreeBenchmarkIRedbService.AddNewObjectsAsyncIRedbContext.NextObjectIdBatchAsync
PG Pro 748385ms(100000)
postgres.ef 0ms
MS Pro 651875ms(100000)
sqlite.pro 1227290ms(100000)

PVT prefilter in tree queries

Tree counterpart of E202: dumps the SQL a tree query generates with and without the PVT
prefilter, and checks that the row count is unchanged. See docs/PVT_PREFILTER_PLAN.md.
dotnet run --project redb.Examples -- E204 (off)
REDB_PVT_PREFILTER=1 dotnet run --project redb.Examples -- E204 (on)
A subtree already bounds the work, unlike a flat query which always scans the whole scheme,
so the prefilter only starts paying once the subtree itself is large. Measured on a 6-level
tree of 99200 nodes: 1189 -> 401 ms and 716677 -> 333755 buffers, identical result sets.
Needs a tree with real depth under the Employee scheme: E203 seeds one, employees arranged as a
reporting tree with ten direct reports per manager. Without depth this example still runs, it
just measures very little.
The differential check here is by COUNT rather than by id set: a tree query over a large
subtree returns tens of thousands of objects and materialising them all would dwarf the
query being measured. Full id-set equality was verified separately in psql.
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 root = await redb.TreeQuery<EmployeeProps>().WhereRoots().FirstOrDefaultAsync();
if (root == null)
{
    sw.Stop();
    return Fail("E204", "PVT prefilter in tree queries", ExampleTier.Pro, sw.ElapsedMilliseconds,
        "No Employee tree found. Seed one with E203.");
}

var log = new StringBuilder();
var summary = new List<string>();
var withPrefilter = 0;

log.AppendLine($"-- EnablePvtPrefilter = {(enabled ? "true (enabled)" : "false (disabled)")}");
log.AppendLine($"-- root id = {root.Id}");
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);

    var hasRow = sql.Contains("AND ((v._id_structure");
    // Dropping _objects oo is the second half of the change and only fires together
    // with the prefilter; surfacing it separately makes a half-applied change visible.
    var droppedObjects = sql.Contains("FROM tree t\n") || sql.Contains("    FROM tree t");
    var form = hasRow ? (droppedObjects ? "Row+" : "Row") : "none";
    if (hasRow) withPrefilter++;

    string count;
    var timer = Stopwatch.StartNew();
    try
    {
        count = (await query.CountAsync()).ToString();
    }
    catch (Exception ex)
    {
        count = $"EXEC FAIL {ex.GetType().Name}: {ex.Message.Split('\n')[0]}";
    }
    timer.Stop();

    log.AppendLine($"-- prefilter form: {form}");
    log.AppendLine($"-- count: {count} in {timer.ElapsedMilliseconds} ms");
    log.AppendLine();

    summary.Add($"{label}: {form}, count={count}");
    Console.WriteLine($"[{form,5}] {label} -> count={count} in {timer.ElapsedMilliseconds} ms");
}

// The text-search shape: one needle across two string fields. Row form plus the dropped
// _objects join, and pg_trgm finally reaches the column.
await CaseAsync(
    "OR: two string fields, same needle",
    "Row+ (prefilter and _objects join dropped)",
    redb.TreeQuery<EmployeeProps>(root.Id)
        .Where(e => e.Position.Contains("Design") || e.Department.Contains("Design")));

// Two predicates on one field merge into a single selective branch.
await CaseAsync(
    "AND: range on one field",
    "Row+ (both bounds merged)",
    redb.TreeQuery<EmployeeProps>(root.Id)
        .Where(e => e.Salary >= 60000m && e.Salary < 90000m));

// Dates use _datetimeoffset and a different parameter round-trip.
await CaseAsync(
    "AND: date range",
    "Row+",
    redb.TreeQuery<EmployeeProps>(root.Id)
        .Where(e => e.HireDate >= new DateTime(2030, 1, 1) && e.HireDate < new DateTime(2031, 1, 1)));

// Pivot not covered: Position and Age have no branch of their own, so the guard suppresses
// the prefilter rather than nulling their columns out.
await CaseAsync(
    "AND: three fields, one selective",
    "none (pivot not covered)",
    redb.TreeQuery<EmployeeProps>(root.Id)
        .Where(e => e.Position != "" && e.Age >= 30 && e.Salary > 70000m));

// Null check switches the CTE to the LEFT JOIN form, where dropping rows would blur
// "absent" and "present but not matching".
await CaseAsync(
    "AND: null check plus selective field",
    "none (hasNullCheck)",
    redb.TreeQuery<EmployeeProps>(root.Id)
        .Where(e => e.EmployeeCode == null && e.Salary > 70000m));

string? outPath = null;
try
{
    var dir = Path.Combine(AppContext.BaseDirectory, "Output");
    Directory.CreateDirectory(dir);
    outPath = Path.Combine(dir, enabled ? "tree_prefilter_on.txt" : "tree_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 a count over the
// whole subtree of the root, so the count of queries 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 counted over the subtree of root {root.Id}",
    $"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 tree_prefilter_off.txt against tree_prefilter_on.txt: counts must match exactly"
});
SqlDebugPrefilterProPvtTreeIRedbService.TreeQueryIRedbQueryable.ToSqlStringAsync
PG Pro 5344ms(5)
postgres.pro.repeat 3070ms(5)
postgres.pro.cache 3282ms(5)
MS Pro 6039ms(5)
mssql.pro.repeat 8009ms(5)
mssql.pro.cache 4559ms(5)
sqlite.pro 2210ms(5)
sqlite.pro.repeat 2205ms(5)
sqlite.pro.cache 2190ms(5)

WhereRedb - System Fields and Composition

WhereRedb: filtering on SYSTEM fields of _objects (id, parent_id, key, value_string,
date_modify and friends) as opposed to Where, which filters on your Props. The two
compose freely in one query - the system predicate and the Props predicate both run
server-side. Patterns shown here mirror production usage: a lookup by the service
value_string key, an id-set prefilter with Contains, and the composite
WhereRedb().Where() shape (discussion #12, item on WhereRedb docs).
var sw = Stopwatch.StartNew();
await redb.SyncSchemeAsync<EmployeeProps>();

// Seed: three employees carrying SYSTEM fields alongside Props. The service fields
// (key, value_string, value_long...) live on the _objects row itself - no scheme
// change needed - and are ideal for lookup keys next to your business data.
var seeded = new List<long>();
for (var i = 0; i < 3; i++)
{
    var employee = new RedbObject<EmployeeProps>
    {
        name = $"e205-employee-{i}",
        key = 7000 + i,                        // numeric service key
        value_string = i == 0 ? "e205:head-of-desk" : null, // string lookup key
        Props = new EmployeeProps
        {
            FirstName = $"E205-{i}",
            LastName = "Sample",
            Department = i == 2 ? "Sales" : "IT",
            Salary = 50000m + i * 10000m,
        },
    };
    seeded.Add(await redb.SaveAsync(employee));
}

// 1. Lookup by the service string key - the settings/singleton pattern:
//    one indexed system column instead of a dedicated Props field.
var head = await redb.Query<EmployeeProps>()
    .WhereRedb(o => o.ValueString == "e205:head-of-desk")
    .FirstOrDefaultAsync();

// 2. Id-set prefilter with Contains - "load these exact objects, filtered further".
var byIds = await redb.Query<EmployeeProps>()
    .WhereRedb(o => seeded.Contains(o.Id))
    .ToListAsync();

// 3. The composite shape: a SYSTEM predicate and a Props predicate in one query.
//    Both are translated to SQL - the numeric key range narrows on _objects, the
//    department condition narrows on Props values.
var itByKey = await redb.Query<EmployeeProps>()
    .WhereRedb(o => o.Key >= 7000 && o.Key <= 7002)
    .Where(e => e.Department == "IT")
    .ToListAsync();

sw.Stop();

// Cleanup: the example leaves no residue behind.
await redb.DeleteAsync(seeded);
WhereRedbQuerySystemFieldsCompositeIRedbQueryable.WhereRedbIRedbQueryable.WhereIRedbObject.KeyIRedbObject.ValueString
PG Pro 356ms(6)
postgres.ef 3594ms(6)
postgres.pro.repeat 329ms(6)
postgres.pro.cache 326ms(6)
postgres.ef.repeat 3277ms(6)
MS Pro 482ms(6)
mssql.pro.repeat 397ms(6)
mssql.pro.cache 365ms(6)
sqlite.pro 42ms(6)
sqlite.pro.repeat 31ms(6)
sqlite.pro.cache 35ms(6)

WhereRedb - Narrow by Parent First

WhereRedb narrows first. A condition on the object's own columns (here the parent) is checked
on _objects before any Props field is read, so the query reads the fields of the few objects
that pass and nothing else. The same Props filter and page over the whole scheme reads every
employee. This is the shape of "the points of one route sorted by their number" or "the lines
of one order": a narrow parent, a Props filter, a page sorted by a Props field.
var sw = Stopwatch.StartNew();
await redb.SyncSchemeAsync<EmployeeProps>();

// A team lead and twenty employees under it: the parent is a system column (_id_parent),
// no Props field is needed to tell the team apart.
var lead = new RedbObject<EmployeeProps>
{
    name = "e206-team-lead",
    Props = new EmployeeProps { FirstName = "E206", LastName = "Lead", Department = "Sales", Salary = 150000m, Age = 45 },
};
var leadId = await redb.SaveAsync(lead);

var team = Enumerable.Range(0, 20).Select(i => (IRedbObject)new RedbObject<EmployeeProps>
{
    name = $"e206-member-{i}",
    parent_id = leadId,
    Props = new EmployeeProps
    {
        FirstName = $"E206-{i}",
        LastName = "Member",
        Department = "Sales",
        Age = 25 + i,
        Salary = 60000m + i * 2500m,
    },
}).ToList();
var memberIds = await redb.SaveAsync(team);

// The same Props filter and page, once over the whole scheme and once under the parent.
var whole = Stopwatch.StartNew();
var anywhere = await redb.Query<EmployeeProps>()
    .Where(e => e.Salary > 70000m && e.Age >= 30)
    .OrderByDescending(e => e.Salary)
    .Take(10)
    .ToListAsync();
var wholeMs = whole.ElapsedMilliseconds;

var underLead = redb.Query<EmployeeProps>()
    .WhereRedb(o => o.ParentId == leadId)
    .Where(e => e.Salary > 70000m && e.Age >= 30)
    .OrderByDescending(e => e.Salary)
    .Take(10);

var narrow = Stopwatch.StartNew();
var teamPage = await underLead.ToListAsync();
var narrowMs = narrow.ElapsedMilliseconds;

// The SQL shows where the parent goes: into the scan of _objects that drives the query,
// ahead of every Props field.
var sql = await underLead.ToSqlStringAsync();
var drivingScan = sql.Split('\n').FirstOrDefault(l => l.Contains("_id_parent"))?.Trim() ?? "<parent condition not found>";

sw.Stop();

// Cleanup: the example leaves no residue behind.
await redb.DeleteAsync(memberIds.Append(leadId).ToList());
WhereRedbParentIdPerformanceCompositeIRedbQueryable.WhereRedbIRedbQueryable.WhereIRedbQueryable.ToSqlStringAsync
PG Pro 250ms(10)
postgres.ef 1963ms(10)
postgres.pro.repeat 254ms(10)
postgres.pro.cache 250ms(10)
postgres.ef.repeat 2051ms(10)
MS Pro 330ms(10)
mssql.pro.repeat 270ms(10)
mssql.pro.cache 180ms(10)
sqlite.pro 213ms(10)
sqlite.pro.repeat 39ms(10)
sqlite.pro.cache 166ms(10)

DateTime REAL Julian Verify

Regression probe for SQLite REAL Julian-day datetime storage.
Seeds 3 employees with distinct, far-apart HireDates (incl. a fractional time
of day), then asserts that server-side range filters select EXACTLY the right
rows and that a saved datetime round-trips. A broken comparison (the old TEXT
lexical bug) would make ranges always-true/false and fail these assertions.
var sw = Stopwatch.StartNew();
var lines = new List<string>();

// Idempotent: purge any leftovers from a previous run so the "exactly 1 per range"
// asserts hold whether this runs once or as part of the full suite.
var stale = await redb.Query<EmployeeProps>().Where(e => e.Department == Dept).Select(e => e.Id).ToListAsync();
if (stale.Count > 0)
    await redb.DeleteAsync(stale);

// Distinct UTC instants, years apart; the 2024 one carries a fractional
// time of day to exercise the non-integer part of the Julian day.
var dPast   = new DateTime(2018, 03, 15, 00, 00, 00, DateTimeKind.Utc);
var dMiddle = new DateTime(2024, 06, 15, 13, 45, 30, DateTimeKind.Utc);
var dFuture = new DateTime(2030, 09, 20, 00, 00, 00, DateTimeKind.Utc);

var seeded = new[] { (dPast, "PAST"), (dMiddle, "MID"), (dFuture, "FUT") }
    .Select(s => (IRedbObject)new RedbObject<EmployeeProps>
    {
        name = $"JulianVerify-{s.Item2}",
        Props = new EmployeeProps
        {
            FirstName = s.Item2, LastName = "Verify", Age = 30,
            Position = "QA", Department = Dept, Salary = 1m,
            HireDate = s.Item1, EmployeeCode = $"JV-{s.Item2}"
        }
    })
    .ToList();
await redb.SaveAsync(seeded);

// Range filters — each must return exactly its single seeded match.
var midRange = await redb.Query<EmployeeProps>()
    .Where(e => e.Department == Dept && e.HireDate >= new DateTime(2023, 1, 1) && e.HireDate < new DateTime(2026, 1, 1))
    .ToListAsync();

var futureOnly = await redb.Query<EmployeeProps>()
    .Where(e => e.Department == Dept && e.HireDate >= new DateTime(2029, 1, 1))
    .ToListAsync();

var pastOnly = await redb.Query<EmployeeProps>()
    .Where(e => e.Department == Dept && e.HireDate < new DateTime(2020, 1, 1))
    .ToListAsync();

var failures = new List<string>();
void Expect(string label, List<RedbObject<EmployeeProps>> got, string wantCode)
{
    if (got.Count == 1 && got[0].Props.EmployeeCode == $"JV-{wantCode}")
        lines.Add($"OK  {label}: 1 row = {wantCode}");
    else
        failures.Add($"{label}: expected [1×{wantCode}], got [{string.Join(",", got.Select(g => g.Props.EmployeeCode))}]");
}

Expect("[2023,2026)", midRange, "MID");
Expect(">=2029", futureOnly, "FUT");
Expect("<2020", pastOnly, "PAST");

// Round-trip: the saved fractional UTC datetime must read back intact.
if (midRange.Count == 1)
{
    var rt = midRange[0].Props.HireDate.ToUniversalTime();
    var drift = Math.Abs((rt - dMiddle).TotalMilliseconds);
    if (drift <= 2)
        lines.Add($"OK  round-trip: {rt:yyyy-MM-ddTHH:mm:ss.fff}Z (drift {drift:0.###}ms)");
    else
        failures.Add($"round-trip drift {drift:0.###}ms: got {rt:O}, want {dMiddle:O}");
}

sw.Stop();

if (failures.Count > 0)
    return Fail("E997", "DateTime REAL Julian Verify", ExampleTier.Free, sw.ElapsedMilliseconds,
        string.Join(" | ", failures));
WhereDateTimeRegression
PG Pro 148ms(3)
postgres.ef 2457ms(3)
postgres.pro.repeat 131ms(3)
postgres.pro.cache 133ms(3)
postgres.ef.repeat 2923ms(3)
MS Pro 376ms(3)
mssql.pro.repeat 283ms(3)
mssql.pro.cache 229ms(3)
sqlite.pro 330ms(3)
sqlite.pro.repeat 422ms(3)
sqlite.pro.cache 282ms(3)