feat: extend query
This commit is contained in:
1 parent
d6c31cb6d0
commit
b16f2ddf47
1 file changed
+164
+164
@@ -2537,3 +2537,167 @@ test "updateWhere keeps a secondary index in sync when the indexed field changes
|
|||||||
);
|
);
|
||||||
try std.testing.expectEqual(@as(usize, 1), updated);
|
try std.testing.expectEqual(@as(usize, 1), updated);
|
||||||
|
|
||||||
|
// The stale index entry is gone, a fresh one under the new value works,
|
||||||
|
// and explain() confirms the index (not a full scan) still serves it.
|
||||||
|
const stale = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } });
|
||||||
|
defer freeRows(allocator, stale);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), stale.len);
|
||||||
|
|
||||||
|
const fresh = try table.select(.{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } });
|
||||||
|
defer freeRows(allocator, fresh);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), fresh.len);
|
||||||
|
|
||||||
|
const constTable: *const Table = &table;
|
||||||
|
var plan = try constTable.explain(allocator, .{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } });
|
||||||
|
defer plan.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(PlanKind.index_eq, plan.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "updateWhere keeps a composite index in sync across leading and trailing column moves" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var tmp = std.testing.tmpDir(.{});
|
||||||
|
defer tmp.cleanup();
|
||||||
|
var db = try openTemp(&tmp, allocator);
|
||||||
|
defer db.close();
|
||||||
|
|
||||||
|
var table = try Table.open(&db, allocator, "users");
|
||||||
|
defer table.deinit();
|
||||||
|
try table.createCompositeIndex("city_age", &.{ "city", "age" });
|
||||||
|
|
||||||
|
try table.put(.{ .int = 1 }, &.{
|
||||||
|
.{ .name = "city", .value = .{ .text = "warsaw" } },
|
||||||
|
.{ .name = "age", .value = .{ .int = 30 } },
|
||||||
|
.{ .name = "name", .value = .{ .text = "alice" } },
|
||||||
|
});
|
||||||
|
try table.put(.{ .int = 2 }, &.{
|
||||||
|
.{ .name = "city", .value = .{ .text = "warsaw" } },
|
||||||
|
.{ .name = "age", .value = .{ .int = 31 } },
|
||||||
|
.{ .name = "name", .value = .{ .text = "bob" } },
|
||||||
|
});
|
||||||
|
try table.put(.{ .int = 3 }, &.{
|
||||||
|
.{ .name = "city", .value = .{ .text = "krakow" } },
|
||||||
|
.{ .name = "age", .value = .{ .int = 30 } },
|
||||||
|
.{ .name = "name", .value = .{ .text = "carol" } },
|
||||||
|
});
|
||||||
|
try table.put(.{ .int = 4 }, &.{
|
||||||
|
.{ .name = "city", .value = .{ .text = "krakow" } },
|
||||||
|
.{ .name = "age", .value = .{ .int = 40 } },
|
||||||
|
.{ .name = "name", .value = .{ .text = "dave" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// updateWhere matched through the composite index (both columns pinned
|
||||||
|
// by equality) moves the TRAILING column (age). The old composite entry
|
||||||
|
// for row 1 must vanish and a fresh one under the new age must serve it.
|
||||||
|
{
|
||||||
|
const preds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 30 } } },
|
||||||
|
};
|
||||||
|
const updated = try table.updateWhere(
|
||||||
|
.{ .and_ = &preds },
|
||||||
|
&.{.{ .name = "age", .value = .{ .int = 45 } }},
|
||||||
|
);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), updated);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const stalePreds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 30 } } },
|
||||||
|
};
|
||||||
|
const stale = try table.select(.{ .and_ = &stalePreds });
|
||||||
|
defer freeRows(allocator, stale);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), stale.len);
|
||||||
|
|
||||||
|
const freshPreds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 45 } } },
|
||||||
|
};
|
||||||
|
const fresh = try table.select(.{ .and_ = &freshPreds });
|
||||||
|
defer freeRows(allocator, fresh);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), fresh.len);
|
||||||
|
try std.testing.expectEqual(@as(i64, 1), fresh[0].get(pkField).?.int);
|
||||||
|
|
||||||
|
const constTable: *const Table = &table;
|
||||||
|
var plan = try constTable.explain(allocator, .{ .and_ = &freshPreds });
|
||||||
|
defer plan.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(PlanKind.composite_eq, plan.kind);
|
||||||
|
try std.testing.expectEqualStrings("city_age", plan.indexName.?);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), plan.matchedFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same check but moving the LEADING column (city) instead of the
|
||||||
|
// trailing one - a different code path in the composite key encoding
|
||||||
|
// (the prefix itself shifts, not just the suffix).
|
||||||
|
{
|
||||||
|
const preds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 31 } } },
|
||||||
|
};
|
||||||
|
const updated = try table.updateWhere(
|
||||||
|
.{ .and_ = &preds },
|
||||||
|
&.{.{ .name = "city", .value = .{ .text = "gdansk" } }},
|
||||||
|
);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), updated);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const stalePreds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "warsaw" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 31 } } },
|
||||||
|
};
|
||||||
|
const stale = try table.select(.{ .and_ = &stalePreds });
|
||||||
|
defer freeRows(allocator, stale);
|
||||||
|
try std.testing.expectEqual(@as(usize, 0), stale.len);
|
||||||
|
|
||||||
|
const freshPreds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "gdansk" } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = 31 } } },
|
||||||
|
};
|
||||||
|
const fresh = try table.select(.{ .and_ = &freshPreds });
|
||||||
|
defer freeRows(allocator, fresh);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), fresh.len);
|
||||||
|
try std.testing.expectEqual(@as(i64, 2), fresh[0].get(pkField).?.int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWhere reached through composite_range (leading column pinned by
|
||||||
|
// equality, trailing column range-bound) must update every row the
|
||||||
|
// range covers, and leave the composite index able to keep serving
|
||||||
|
// both afterwards even though only the unindexed "name" field changed.
|
||||||
|
{
|
||||||
|
const rangePreds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = "krakow" } } },
|
||||||
|
.{ .gte = .{ .field = "age", .value = .{ .int = 30 } } },
|
||||||
|
};
|
||||||
|
const constTable: *const Table = &table;
|
||||||
|
var rangePlan = try constTable.explain(allocator, .{ .and_ = &rangePreds });
|
||||||
|
defer rangePlan.deinit(allocator);
|
||||||
|
try std.testing.expectEqual(PlanKind.composite_range, rangePlan.kind);
|
||||||
|
try std.testing.expectEqualStrings("city_age", rangePlan.indexName.?);
|
||||||
|
|
||||||
|
const updated = try table.updateWhere(
|
||||||
|
.{ .and_ = &rangePreds },
|
||||||
|
&.{.{ .name = "name", .value = .{ .text = "updated" } }},
|
||||||
|
);
|
||||||
|
try std.testing.expectEqual(@as(usize, 2), updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final sanity sweep: every row is still reachable through the
|
||||||
|
// composite index under exactly its current (city, age), nothing was
|
||||||
|
// lost, duplicated, or left under a stale key.
|
||||||
|
const expectations = [_]struct { pk: i64, city: []const u8, age: i64, name: []const u8 }{
|
||||||
|
.{ .pk = 1, .city = "warsaw", .age = 45, .name = "alice" },
|
||||||
|
.{ .pk = 2, .city = "gdansk", .age = 31, .name = "bob" },
|
||||||
|
.{ .pk = 3, .city = "krakow", .age = 30, .name = "updated" },
|
||||||
|
.{ .pk = 4, .city = "krakow", .age = 40, .name = "updated" },
|
||||||
|
};
|
||||||
|
for (expectations) |exp| {
|
||||||
|
const preds = [_]Predicate{
|
||||||
|
.{ .eq = .{ .field = "city", .value = .{ .text = exp.city } } },
|
||||||
|
.{ .eq = .{ .field = "age", .value = .{ .int = exp.age } } },
|
||||||
|
};
|
||||||
|
const rows = try table.select(.{ .and_ = &preds });
|
||||||
|
defer freeRows(allocator, rows);
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), rows.len);
|
||||||
|
try std.testing.expectEqual(exp.pk, rows[0].get(pkField).?.int);
|
||||||
|
try std.testing.expectEqualStrings(exp.name, rows[0].get("name").?.text);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user