Skip to content

Commit 312fb36

Browse files
committed
perf(store): index-friendly plans for architecture, schema counts and anchored name search
Three query rewrites over the existing indexes; every MCP response stays byte-identical to c9cacdf (tools + Cypher suites on django, redis and the linux kernel, same index each). - arch_packages: GROUP BY +n.name. With a bare column the planner walked idx_nodes_name over every node to get groups in name order, with a table lookup per node to test the label (6.3 s of the 7.3 s get_architecture on the 8.5M-node kernel graph, for 5 Package nodes). The unary plus seeks idx_nodes_label instead; the name tie-break is explicit because a sorted GROUP BY follows the ORDER BY direction and reversed tied counts. - get_schema_impl: label / edge-type counts step through the distinct values (MIN(col) > previous) and count each value's index range instead of a GROUP BY scan: kernel 0.26 -> 0.16 s and 0.54 -> 0.32 s. Values reach ORDER BY in the same ascending order, so ties keep their order. - cbm_store_search: an anchored name_pattern ("^schedule$", "^kmalloc") adds idx_nodes_name ranges for its literal prefix (every ASCII letter case of the first three letters under iregexp, which runs in the C locale), one UNION ALL arm per range merged by ORDER BY name, id without a sort; the count sums the ranges. LIKE hints and the regex stay in the WHERE, so the rows are those of the plain scan. Patterns that do not compile keep the plain query (and its error). Kernel, interleaved medians n=5: get_architecture 9.2-10.3 s -> 1.03 s, get_graph_schema 0.76 s -> 0.48 s, cycles 2.24 s -> 2.04 s, search_graph ^schedule$ 1.15 s -> 14 ms, ^kmalloc + label 0.64 s -> 14 ms. Peak daemon RSS for get_architecture +67 MB -> +22 MB. The ranged SQL is built with a bounds-checked append (no snprintf-derived sizes) into CBM_MEM_CLASS_STORE buffers; a build that does not fit rolls its binds back and the plain query runs. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 1818373 commit 312fb36

4 files changed

Lines changed: 498 additions & 11 deletions

File tree

‎src/store/store.c‎

Lines changed: 230 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// for ISO timestamp
1010

1111
#include <limits.h>
12+
#include <stdarg.h>
1213
#include <stdint.h>
1314
#include "foundation/constants.h"
1415
#include "foundation/arena.h"
@@ -54,9 +55,13 @@ enum {
5455
ST_GLOB_MIN_LEN = 3,
5556
ST_GLOB_SKIP = 2,
5657
ST_MAX_LANG = 10,
57-
ST_SEARCH_MAX_BINDS = 32, /* increased: LIKE pre-filter adds binds per pattern */
58-
ST_LIKE_POOL_MAX = 12, /* max malloc'd LIKE strings alive during one search */
59-
ST_LIKE_HINT_MAX = 2, /* max LIKE hints extracted per regex pattern */
58+
ST_SEARCH_MAX_BINDS = 48, /* LIKE pre-filters + anchored-name range bounds add binds */
59+
ST_LIKE_POOL_MAX = 12, /* max malloc'd LIKE strings alive during one search */
60+
ST_LIKE_HINT_MAX = 2, /* max LIKE hints extracted per regex pattern */
61+
ST_NAME_RANGE_PREFIX = 64, /* longest anchored name prefix seeked on */
62+
ST_NAME_RANGE_LETTERS = 3, /* case-insensitive: letters expanded into case variants */
63+
ST_NAME_RANGE_MAX = 8, /* 2^ST_NAME_RANGE_LETTERS name ranges (UNION ALL arms) */
64+
ST_NAME_RANGE_ARM_SQL = 96, /* per-arm SQL beyond the select list and WHERE */
6065
ST_MAX_PKGS = 64,
6166
ST_INIT_CAP_4 = 4,
6267
ST_HEADER_PREFIX = 3,
@@ -6732,6 +6737,36 @@ int cbm_extract_like_hints(const char *pattern, char **out, int max_out) {
67326737
return count;
67336738
}
67346739

6740+
/* Patterns run as POSIX ERE without REG_NEWLINE, so '^' anchors at the start of
6741+
* the name only, and a pattern without '|' is one concatenation: every match
6742+
* begins with the literal run after the '^'. A following '*', '?', '{' may
6743+
* repeat the run's last character zero times and '+' may be combined with them
6744+
* (glibc reads "a+?" as "(a+)?"), so that character is dropped; so is the one
6745+
* before a backslash, which no rule here needs to interpret. */
6746+
int cbm_regex_anchored_prefix(const char *pattern, char *out, int out_sz) {
6747+
if (!pattern || !out || out_sz <= 1 || pattern[0] != '^' || strchr(pattern, '|')) {
6748+
if (out && out_sz > 0) {
6749+
out[0] = '\0';
6750+
}
6751+
return 0;
6752+
}
6753+
int len = 0;
6754+
const char *p = pattern + SKIP_ONE;
6755+
while (*p && len < out_sz - SKIP_ONE) {
6756+
unsigned char c = (unsigned char)*p;
6757+
if (c < ' ' || c > '~' || strchr(".[]()*+?{}|^$\\", c)) {
6758+
break;
6759+
}
6760+
out[len++] = (char)c;
6761+
p++;
6762+
}
6763+
if (len > 0 && *p && strchr("*+?{\\", *p)) {
6764+
len--;
6765+
}
6766+
out[len] = '\0';
6767+
return len;
6768+
}
6769+
67356770
/* ── ensureCaseInsensitive / stripCaseFlag ────────────────────── */
67366771

67376772
const char *cbm_ensure_case_insensitive(const char *pattern) {
@@ -7028,6 +7063,151 @@ static int search_build_where(const cbm_search_params_t *params, char *where, in
70287063
return nparams;
70297064
}
70307065

7066+
/* Anchored name patterns ("^schedule$", "^kmalloc"): every matching name begins
7067+
* with the pattern's literal prefix (cbm_regex_anchored_prefix), so it lies in a
7068+
* half-open idx_nodes_name range [prefix, prefix with its last byte + 1). Under
7069+
* iregexp the prefix matches in any letter case: the process never calls
7070+
* setlocale, so the regex runs in the C locale and folds ASCII letters only, and
7071+
* each of the first ST_NAME_RANGE_LETTERS letters doubles the ranges. The ranges
7072+
* are ADDED to the full WHERE (LIKE hints and regex stay), so the rows are those
7073+
* of the plain query — which instead scans every name of the project. */
7074+
typedef struct {
7075+
char lo[ST_NAME_RANGE_MAX][ST_NAME_RANGE_PREFIX];
7076+
char hi[ST_NAME_RANGE_MAX][ST_NAME_RANGE_PREFIX];
7077+
int count;
7078+
} search_name_ranges_t;
7079+
7080+
static bool name_range_letter(char c) {
7081+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
7082+
}
7083+
7084+
static char name_range_case(char c, bool lower) {
7085+
if (lower && c >= 'A' && c <= 'Z') {
7086+
return (char)(c - 'A' + 'a');
7087+
}
7088+
if (!lower && c >= 'a' && c <= 'z') {
7089+
return (char)(c - 'a' + 'A');
7090+
}
7091+
return c;
7092+
}
7093+
7094+
static int search_name_ranges(const cbm_search_params_t *p, search_name_ranges_t *r) {
7095+
r->count = 0;
7096+
/* Without the project the index cannot seek the name; degree filters wrap
7097+
* the query in a subselect this rewrite leaves alone. */
7098+
if (!p->project || !p->name_pattern || p->min_degree >= 0 || p->max_degree >= 0) {
7099+
return 0;
7100+
}
7101+
char prefix[ST_NAME_RANGE_PREFIX];
7102+
int len = cbm_regex_anchored_prefix(p->name_pattern, prefix, (int)sizeof(prefix));
7103+
if (len == 0) {
7104+
return 0;
7105+
}
7106+
/* A pattern the regex function rejects fails the plain query once a row
7107+
* reaches the regex; fewer rows reach it here, so it keeps the plain query. */
7108+
int flags = CBM_REG_EXTENDED | CBM_REG_NOSUB | (p->case_sensitive ? 0 : CBM_REG_ICASE);
7109+
cbm_regex_t re;
7110+
if (cbm_regcomp(&re, p->name_pattern, flags) != 0) {
7111+
return 0;
7112+
}
7113+
cbm_regfree(&re);
7114+
int letters = 0;
7115+
if (!p->case_sensitive) {
7116+
int k = 0;
7117+
for (; k < len; k++) {
7118+
if (name_range_letter(prefix[k])) {
7119+
if (letters == ST_NAME_RANGE_LETTERS) {
7120+
break;
7121+
}
7122+
letters++;
7123+
}
7124+
}
7125+
len = k;
7126+
}
7127+
int variants = SKIP_ONE << letters;
7128+
for (int v = 0; v < variants; v++) {
7129+
int bit = letters - SKIP_ONE;
7130+
for (int i = 0; i < len; i++) {
7131+
char c = prefix[i];
7132+
if (!p->case_sensitive && name_range_letter(c)) {
7133+
/* Variant 0 is all upper case: ranges in ascending byte order. */
7134+
c = name_range_case(c, ((v >> bit) & SKIP_ONE) != 0);
7135+
bit--;
7136+
}
7137+
r->lo[v][i] = c;
7138+
r->hi[v][i] = c;
7139+
}
7140+
r->lo[v][len] = '\0';
7141+
r->hi[v][len] = '\0';
7142+
r->hi[v][len - SKIP_ONE]++; /* printable ASCII: never wraps */
7143+
}
7144+
r->count = variants;
7145+
return variants;
7146+
}
7147+
7148+
/* Main and count SQL over the name ranges: one UNION ALL arm per range, each
7149+
* the plain WHERE plus its range, so every arm seeks idx_nodes_name. The arms
7150+
* are disjoint and each yields (name, id) order, so ORDER BY 4, 1 (n.name,
7151+
* n.id) merges them without a sort and the per-row degree subqueries run only
7152+
* for rows the page emits; the count sums the per-range counts. On false
7153+
* (allocation, bind slots) nothing is bound and the plain query runs. */
7154+
/* Append to buf[0..cap) at *len; false (nothing written past cap) when it
7155+
* does not fit. */
7156+
static bool search_sql_append(char *buf, size_t cap, size_t *len, const char *fmt, ...) {
7157+
if (*len >= cap) {
7158+
return false;
7159+
}
7160+
va_list args;
7161+
va_start(args, fmt);
7162+
int wrote = vsnprintf(buf + *len, cap - *len, fmt, args);
7163+
va_end(args);
7164+
if (wrote < 0 || (size_t)wrote >= cap - *len) {
7165+
return false;
7166+
}
7167+
*len += (size_t)wrote;
7168+
return true;
7169+
}
7170+
7171+
static bool search_build_ranged_sql(const char *select_cols, const char *where,
7172+
const search_name_ranges_t *r, int limit, int offset,
7173+
search_bind_t *binds, int *bind_idx, char **out_sql,
7174+
char **out_count_sql) {
7175+
if (*bind_idx + (r->count * ST_COL_2) > ST_SEARCH_MAX_BINDS) {
7176+
return false;
7177+
}
7178+
size_t cap = (strlen(select_cols) + strlen(where) + ST_NAME_RANGE_ARM_SQL) * (size_t)r->count +
7179+
ST_NAME_RANGE_ARM_SQL;
7180+
char *sql = cbm_alloc(CBM_MEM_CLASS_STORE, cap);
7181+
char *cnt = cbm_alloc(CBM_MEM_CLASS_STORE, cap);
7182+
int first_bind = *bind_idx;
7183+
size_t sl = 0;
7184+
size_t cl = 0;
7185+
bool ok = sql && cnt && search_sql_append(cnt, cap, &cl, "SELECT ");
7186+
for (int i = 0; ok && i < r->count; i++) {
7187+
int lo = *bind_idx + SKIP_ONE;
7188+
where_bind_text(binds, bind_idx, r->lo[i]);
7189+
where_bind_text(binds, bind_idx, r->hi[i]);
7190+
ok = search_sql_append(sql, cap, &sl,
7191+
"%s%s FROM nodes n WHERE (%s) AND n.name >= ?%d AND n.name < ?%d",
7192+
i > 0 ? " UNION ALL " : "", select_cols, where, lo, lo + SKIP_ONE) &&
7193+
search_sql_append(cnt, cap, &cl,
7194+
"%s(SELECT COUNT(*) FROM nodes n WHERE (%s) AND n.name >= ?%d AND "
7195+
"n.name < ?%d)",
7196+
i > 0 ? " + " : "", where, lo, lo + SKIP_ONE);
7197+
}
7198+
ok = ok && search_sql_append(sql, cap, &sl, " ORDER BY 4, 1 LIMIT %d OFFSET %d", limit, offset);
7199+
if (!ok) {
7200+
/* Nothing is bound: the caller runs the plain query. */
7201+
*bind_idx = first_bind;
7202+
cbm_free(CBM_MEM_CLASS_STORE, sql);
7203+
cbm_free(CBM_MEM_CLASS_STORE, cnt);
7204+
return false;
7205+
}
7206+
*out_sql = sql;
7207+
*out_count_sql = cnt;
7208+
return true;
7209+
}
7210+
70317211
int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_search_output_t *out) {
70327212
memset(out, 0, sizeof(*out));
70337213
if (!s || !s->db) {
@@ -7088,9 +7268,21 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
70887268
id_col, limit, offset);
70897269
strncat(sql, order_limit, sizeof(sql) - strlen(sql) - 1);
70907270

7271+
/* Anchored name pattern: the same rows through idx_nodes_name seeks. */
7272+
search_name_ranges_t name_ranges;
7273+
char *ranged_sql = NULL;
7274+
char *ranged_count_sql = NULL;
7275+
if (nparams > 0 && search_name_ranges(params, &name_ranges) > 0) {
7276+
(void)search_build_ranged_sql(select_cols, where, &name_ranges, limit, offset, binds,
7277+
&bind_idx, &ranged_sql, &ranged_count_sql);
7278+
}
7279+
const char *main_sql = ranged_sql ? ranged_sql : sql;
7280+
const char *total_sql = ranged_count_sql ? ranged_count_sql : count_sql;
7281+
70917282
/* Execute count query */
70927283
sqlite3_stmt *cnt_stmt = NULL;
7093-
int rc = sqlite3_prepare_v2(s->db, count_sql, CBM_NOT_FOUND, &cnt_stmt, NULL);
7284+
int rc = sqlite3_prepare_v2(s->db, total_sql, CBM_NOT_FOUND, &cnt_stmt, NULL);
7285+
cbm_free(CBM_MEM_CLASS_STORE, ranged_count_sql); /* prepare_v2 copies the text */
70947286
if (rc == SQLITE_OK) {
70957287
for (int i = 0; i < bind_idx; i++) {
70967288
bind_text(cnt_stmt, i + SKIP_ONE, binds[i].text);
@@ -7103,7 +7295,8 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
71037295

71047296
/* Execute main query */
71057297
sqlite3_stmt *main_stmt = NULL;
7106-
rc = sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &main_stmt, NULL);
7298+
rc = sqlite3_prepare_v2(s->db, main_sql, CBM_NOT_FOUND, &main_stmt, NULL);
7299+
cbm_free(CBM_MEM_CLASS_STORE, ranged_sql);
71077300
if (rc != SQLITE_OK) {
71087301
store_set_error_sqlite(s, "search prepare");
71097302
like_pool_free(&like_pool);
@@ -7956,6 +8149,25 @@ int cbm_store_count_edges_scoped(cbm_store_t *s, const char *project, const char
79568149
return n;
79578150
}
79588151

8152+
/* Per-label and per-type counts. GROUP BY over the (project, label) index
8153+
* decodes every entry to compare its group key; stepping through the distinct
8154+
* values with MIN(col) > previous seeks and counting each value's index range
8155+
* is ~1.6x faster (kernel, 8.5M nodes: 0.26 -> 0.16 s; 16.9M edges: 0.54 ->
8156+
* 0.32 s). The values reach ORDER BY in the ascending order GROUP BY emitted
8157+
* them in, so tied counts keep their order. label and type are NOT NULL. */
8158+
static const char ST_LABEL_COUNTS_SQL[] =
8159+
"WITH RECURSIVE v(x) AS (SELECT MIN(label) FROM nodes WHERE project = ?1 UNION ALL "
8160+
"SELECT (SELECT MIN(label) FROM nodes WHERE project = ?1 AND label > v.x) FROM v "
8161+
"WHERE v.x IS NOT NULL) "
8162+
"SELECT x, (SELECT COUNT(*) FROM nodes WHERE project = ?1 AND label = v.x) AS c "
8163+
"FROM v WHERE x IS NOT NULL ORDER BY c DESC;";
8164+
static const char ST_TYPE_COUNTS_SQL[] =
8165+
"WITH RECURSIVE v(x) AS (SELECT MIN(type) FROM edges WHERE project = ?1 UNION ALL "
8166+
"SELECT (SELECT MIN(type) FROM edges WHERE project = ?1 AND type > v.x) FROM v "
8167+
"WHERE v.x IS NOT NULL) "
8168+
"SELECT x, (SELECT COUNT(*) FROM edges WHERE project = ?1 AND type = v.x) AS c "
8169+
"FROM v WHERE x IS NOT NULL ORDER BY c DESC;";
8170+
79598171
/* with_props=false skips the per-label/per-type JSON property-key discovery:
79608172
* those json_each() scans walk EVERY row of each label/type (minutes-scale on
79618173
* multi-million-node graphs) and get_architecture only needs the counts. */
@@ -7968,8 +8180,7 @@ static int get_schema_impl(cbm_store_t *s, const char *project, cbm_schema_info_
79688180

79698181
/* Node labels */
79708182
{
7971-
const char *sql = "SELECT label, COUNT(*) FROM nodes WHERE project = ?1 GROUP BY label "
7972-
"ORDER BY COUNT(*) DESC;";
8183+
const char *sql = ST_LABEL_COUNTS_SQL;
79738184
sqlite3_stmt *stmt = NULL;
79748185
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) {
79758186
if (stmt) {
@@ -8041,8 +8252,7 @@ static int get_schema_impl(cbm_store_t *s, const char *project, cbm_schema_info_
80418252

80428253
/* Edge types */
80438254
{
8044-
const char *sql = "SELECT type, COUNT(*) FROM edges WHERE project = ?1 GROUP BY type ORDER "
8045-
"BY COUNT(*) DESC;";
8255+
const char *sql = ST_TYPE_COUNTS_SQL;
80468256
sqlite3_stmt *stmt = NULL;
80478257
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK || !stmt) {
80488258
if (stmt) {
@@ -9013,15 +9223,24 @@ static int arch_packages(cbm_store_t *s, const char *project, const char *path,
90139223
char like[CBM_SZ_512];
90149224
bool scoped = arch_path_prepare(path, norm, sizeof(norm), like, sizeof(like));
90159225
char sqlbuf[ST_SQL_BUF];
9226+
/* GROUP BY +n.name, not n.name: with a bare column the planner walks
9227+
* idx_nodes_name over EVERY node of the project to get groups in name order,
9228+
* with a table lookup per node to test the label (6.3 s on the 8.5M-node
9229+
* kernel graph, for 5 Package nodes). The unary plus keeps the value and its
9230+
* BINARY collation but not the index, so the rows come from the
9231+
* idx_nodes_label seek. The ORDER BY sorter keeps tied counts in group
9232+
* emission order, which was name order from the index; a sorted GROUP BY
9233+
* follows the ORDER BY direction instead, so the name tie-break is explicit. */
90169234
const char *base = "SELECT n.name, COUNT(*) as cnt FROM nodes n "
90179235
"WHERE n.project=?1 AND n.label='Package'";
90189236
if (scoped) {
90199237
snprintf(sqlbuf, sizeof(sqlbuf),
90209238
"%s AND (n.file_path = ?2 OR n.file_path LIKE ?3) "
9021-
"GROUP BY n.name ORDER BY cnt DESC LIMIT 15",
9239+
"GROUP BY +n.name ORDER BY cnt DESC, +n.name LIMIT 15",
90229240
base);
90239241
} else {
9024-
snprintf(sqlbuf, sizeof(sqlbuf), "%s GROUP BY n.name ORDER BY cnt DESC LIMIT 15", base);
9242+
snprintf(sqlbuf, sizeof(sqlbuf), "%s GROUP BY +n.name ORDER BY cnt DESC, +n.name LIMIT 15",
9243+
base);
90259244
}
90269245
sqlite3_stmt *stmt = NULL;
90279246
if (sqlite3_prepare_v2(s->db, sqlbuf, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {

‎src/store/store.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,13 @@ char *cbm_glob_to_like(const char *pattern);
12431243
* Each out[i] is malloc'd — caller must free each string. */
12441244
int cbm_extract_like_hints(const char *pattern, char **out, int max_out);
12451245

1246+
/* Literal prefix every match of an anchored POSIX ERE must begin with: the
1247+
* plain printable-ASCII run after a leading '^', minus its last character when
1248+
* a quantifier or escape follows it. Returns its length (written NUL-terminated
1249+
* to out); 0 when the pattern is not anchored, contains '|', or starts with an
1250+
* operator. Case is kept as written. */
1251+
int cbm_regex_anchored_prefix(const char *pattern, char *out, int out_sz);
1252+
12461253
/* Prepend (?i) to a regex pattern if not already present.
12471254
* Returns a static buffer — do NOT free. */
12481255
const char *cbm_ensure_case_insensitive(const char *pattern);

‎tests/test_store_arch.c‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,45 @@ TEST(arch_path_scoping) {
298298
PASS();
299299
}
300300

301+
/* packages: count DESC, tied counts in name (BINARY) order. The aggregation
302+
* runs off the label index now; a sorted GROUP BY follows the ORDER BY
303+
* direction, which reversed the ties until the name tie-break was explicit. */
304+
TEST(arch_packages_tied_counts_in_name_order) {
305+
cbm_store_t *s = cbm_store_open_memory();
306+
ASSERT_NOT_NULL(s);
307+
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
308+
static const char *const pkg_names[] = {"gamma", "alpha", "Zeta", "beta",
309+
"Alpha", "gamma", "beta"};
310+
enum { PKG_NODES = 7, PKG_GROUPS = 5 };
311+
for (int i = 0; i < PKG_NODES; i++) {
312+
char qn[64];
313+
snprintf(qn, sizeof(qn), "test.pkg%d.%s", i, pkg_names[i]);
314+
cbm_node_t n = {.project = "test",
315+
.label = "Package",
316+
.name = pkg_names[i],
317+
.qualified_name = qn,
318+
.file_path = "pkg/manifest.toml"};
319+
ASSERT_TRUE(cbm_store_upsert_node(s, &n) > 0);
320+
}
321+
static const char *const want_names[] = {"beta", "gamma", "Alpha", "Zeta", "alpha"};
322+
static const int want_counts[] = {2, 2, 1, 1, 1};
323+
const char *aspects[] = {"packages"};
324+
const char *paths[] = {NULL, "pkg"}; /* unscoped and path-scoped SQL */
325+
for (int p = 0; p < 2; p++) {
326+
cbm_architecture_info_t info;
327+
ASSERT_EQ(cbm_store_get_architecture(s, "test", paths[p], aspects, 1, &info),
328+
CBM_STORE_OK);
329+
ASSERT_EQ(info.package_count, PKG_GROUPS);
330+
for (int i = 0; i < PKG_GROUPS; i++) {
331+
ASSERT_STR_EQ(info.packages[i].name, want_names[i]);
332+
ASSERT_EQ(info.packages[i].node_count, want_counts[i]);
333+
}
334+
cbm_store_architecture_free(&info);
335+
}
336+
cbm_store_close(s);
337+
PASS();
338+
}
339+
301340
TEST(arch_empty_project) {
302341
cbm_store_t *s = cbm_store_open_memory();
303342
ASSERT_NOT_NULL(s);
@@ -1859,6 +1898,7 @@ SUITE(store_arch) {
18591898
RUN_TEST(arch_hotspots_exclude_tests);
18601899
RUN_TEST(arch_specific_aspects);
18611900
RUN_TEST(arch_path_scoping);
1901+
RUN_TEST(arch_packages_tied_counts_in_name_order);
18621902
RUN_TEST(arch_empty_project);
18631903
RUN_TEST(arch_languages);
18641904
RUN_TEST(arch_routes);

0 commit comments

Comments
 (0)