Repository navigation
Expand file tree
/
Copy pathtest_text_index.cpp
More file actions
275 lines (246 loc) · 11.6 KB
/
Copy pathtest_text_index.cpp
File metadata and controls
275 lines (246 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// test_text_index — sqlite::TextIndex over an in-memory DB: schema, index, score.
#include <sqlite3.h>
#include "check.h"
#include <iostream>
#include <string>
#include <vector>
#include "sqlite/text_index.h"
#include "sqlite/tables.h"
namespace {
// PHASE3 Step 1: registry self-consistency.
void p3_registry_checks() {
using namespace ragger::sqlite;
CHECK(kTextTableCount == 5); // PHASE8: summaries gone (turns, turn_summaries, episodes, knowledge, documents)
for (const auto& t : kTextTables) {
CHECK(std::string(t.junction) == std::string(t.table) + "_terms");
CHECK(find_text_table(t.table) == &t);
}
CHECK(find_text_table("nope") == nullptr);
CHECK(find_text_table("") == nullptr);
std::cout << " registry OK\n";
}
// Raw-SQL mirrors of the index queries (TextIndex is `final` and its hooks
// are protected; the pin asserts DB state directly, which is stronger).
int p3_scalar(sqlite3* db, const char* sql) {
sqlite3_stmt* s = nullptr;
CHECK(sqlite3_prepare_v2(db, sql, -1, &s, nullptr) == SQLITE_OK);
int v = -1;
if (sqlite3_step(s) == SQLITE_ROW) v = sqlite3_column_int(s, 0);
sqlite3_finalize(s);
return v;
}
int p3_term_id(sqlite3* db, const std::string& term) {
std::string sql = "SELECT term_id FROM terms WHERE term = '" + term + "'";
return p3_scalar(db, sql.c_str());
}
int p3_doc_freq(sqlite3* db, const std::string& junction, int term_id) {
std::string sql = "SELECT COUNT(*) FROM " + junction +
" WHERE term_id = " + std::to_string(term_id);
return p3_scalar(db, sql.c_str());
}
std::vector<int> p3_posting_ids(sqlite3* db, const std::string& junction,
const std::string& pk, int term_id) {
std::string sql = "SELECT " + pk + " FROM " + junction +
" WHERE term_id = " + std::to_string(term_id) +
" ORDER BY " + pk;
std::vector<int> ids;
sqlite3_stmt* s = nullptr;
CHECK(sqlite3_prepare_v2(db, sql.c_str(), -1, &s, nullptr) == SQLITE_OK);
while (sqlite3_step(s) == SQLITE_ROW) ids.push_back(sqlite3_column_int(s, 0));
sqlite3_finalize(s);
return ids;
}
// One raw INSERT per known text table (each has different required columns).
// An unknown table name must fail loudly so later phases that add or remove
// a table are forced to update this helper (PHASE3 Step 1 intent).
void p3_insert_row(sqlite3* db, const std::string& table, int id,
const std::string& text) {
const std::string i = std::to_string(id);
std::string sql;
if (table == "turns")
sql = "INSERT INTO turns(turn_id, user_text, assistant_text) VALUES ("
+ i + ", '" + text + "', 'ack " + i + "')";
else if (table == "turn_summaries")
sql = "INSERT INTO turn_summaries(turn_summary_id, text) VALUES ("
+ i + ", '" + text + "')";
else if (table == "episodes")
sql = "INSERT INTO episodes(episode_id, text) VALUES ("
+ i + ", '" + text + "')";
else if (table == "knowledge")
sql = "INSERT INTO knowledge(knowledge_id, kind, text) VALUES ("
+ i + ", 'note', '" + text + "')";
else if (table == "documents") {
// The real store path always creates a document_sources row; the
// doc text formula's subquery needs one (no row => NULL fold).
std::string src = "INSERT INTO document_sources(document_source_id, title) VALUES ("
+ i + ", NULL);";
CHECK(sqlite3_exec(db, src.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK);
sql = "INSERT INTO documents(document_id, text, document_source_id) VALUES ("
+ i + ", '" + text + "', " + i + ")";
}
else
CHECK(!"p3_insert_row: unknown table");
CHECK(sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK);
}
// PHASE3 Step 0 pin: every text table round-trips through the index —
// insert 2 rows, index, corpus_size/doc_freq/postings, re-index changes
// postings, reindex_table counts both rows. Safety net for the registry
// refactor (Steps 1–3): behavior must be identical before and after.
void p3_pin_all_tables() {
sqlite3* db = nullptr;
CHECK(sqlite3_open(":memory:", &db) == SQLITE_OK);
CHECK(sqlite3_exec(db, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr) == SQLITE_OK);
const char* ddl =
"CREATE TABLE document_sources (document_source_id INTEGER PRIMARY KEY, title TEXT);"
"CREATE TABLE turns (turn_id INTEGER PRIMARY KEY, user_text TEXT, assistant_text TEXT,"
" unigram_count INTEGER NOT NULL DEFAULT 0, bigram_count INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE turn_summaries (turn_summary_id INTEGER PRIMARY KEY, text TEXT,"
" unigram_count INTEGER NOT NULL DEFAULT 0, bigram_count INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE episodes (episode_id INTEGER PRIMARY KEY, text TEXT,"
" unigram_count INTEGER NOT NULL DEFAULT 0, bigram_count INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE knowledge (knowledge_id INTEGER PRIMARY KEY, kind TEXT, text TEXT, trigger TEXT,"
" unigram_count INTEGER NOT NULL DEFAULT 0, bigram_count INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE documents (document_id INTEGER PRIMARY KEY, text TEXT,"
" document_source_id INTEGER,"
" unigram_count INTEGER NOT NULL DEFAULT 0, bigram_count INTEGER NOT NULL DEFAULT 0);";
CHECK(sqlite3_exec(db, ddl, nullptr, nullptr, nullptr) == SQLITE_OK);
ragger::sqlite::TextIndex idx(db);
idx.create_schema();
// The pin iterates the registry itself: any phase that adds or removes
// a registry row must extend p3_insert_row (unknown name = loud fail).
for (const auto& t : ragger::sqlite::kTextTables) {
const std::string table = t.table;
const std::string pk = t.pk;
const std::string junction = t.junction;
CHECK(junction == table + "_terms");
p3_insert_row(db, table, 1, "qwertyuiop asdfghjkl");
p3_insert_row(db, table, 2, "zxcvbnm asdfghjkl");
CHECK(idx.reindex_table(table) == 2);
CHECK(p3_scalar(db, ("SELECT COUNT(*) FROM " + table).c_str()) == 2);
const int t_one = p3_term_id(db, "qwertyuiop");
CHECK(t_one != -1 && t_one != 0);
CHECK(p3_doc_freq(db, junction, t_one) == 1);
auto posts = p3_posting_ids(db, junction, pk, t_one);
CHECK(posts.size() == 1 && posts[0] == 1);
// Re-index row 1 with different text: the old word's posting goes
// away, the new word's appears.
idx.index_record(table, 1, "vbnmzxcv asdfghjkl");
CHECK(p3_doc_freq(db, junction, t_one) == 0);
CHECK(p3_posting_ids(db, junction, pk, t_one).empty());
CHECK(p3_doc_freq(db, junction, p3_term_id(db, "vbnmzxcv")) == 1);
CHECK(idx.reindex_table(table) == 2);
std::cout << " pin " << table << " OK\n";
}
sqlite3_close(db);
}
} // namespace
int main() {
p3_registry_checks();
p3_pin_all_tables();
sqlite3* db = nullptr;
CHECK(sqlite3_open(":memory:", &db) == SQLITE_OK);
// Minimal knowledge table (id + text + count columns the index writes).
const char* ddl =
"CREATE TABLE knowledge ("
" knowledge_id INTEGER PRIMARY KEY AUTOINCREMENT,"
" kind TEXT,"
" text TEXT,"
" trigger TEXT," /* db 0.17: TextIndex reads trigger + text */
" unigram_count INTEGER NOT NULL DEFAULT 0,"
" bigram_count INTEGER NOT NULL DEFAULT 0);";
CHECK(sqlite3_exec(db, ddl, nullptr, nullptr, nullptr) == SQLITE_OK);
CHECK(sqlite3_exec(db,
"INSERT INTO knowledge(kind, text) VALUES "
"('note', 'The quick brown fox jumps'),"
"('note', 'A lazy brown dog sleeps'),"
"('note', 'Nothing about animals here');",
nullptr, nullptr, nullptr) == SQLITE_OK);
ragger::sqlite::TextIndex idx(db);
idx.create_schema();
// terms table must now exist.
{
sqlite3_stmt* s = nullptr;
CHECK(sqlite3_prepare_v2(db,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND name='terms'",
-1, &s, nullptr) == SQLITE_OK);
CHECK(sqlite3_step(s) == SQLITE_ROW);
CHECK(sqlite3_column_int(s, 0) == 1);
sqlite3_finalize(s);
}
// Full reindex: all 3 records processed.
int n = idx.reindex_table("knowledge");
CHECK(n == 3);
// knowledge_terms must be populated and idempotent on a second pass.
auto term_rows = [&]() {
sqlite3_stmt* s = nullptr;
sqlite3_prepare_v2(db, "SELECT count(*) FROM knowledge_terms", -1, &s, nullptr);
sqlite3_step(s);
int c = sqlite3_column_int(s, 0);
sqlite3_finalize(s);
return c;
};
int rows1 = term_rows();
CHECK(rows1 > 0);
int n2 = idx.reindex_table("knowledge");
CHECK(n2 == 3);
CHECK(term_rows() == rows1); // idempotent: no doubling
// Scoring: "brown" appears in rows 1 and 2 -> both should score > 0,
// and row 3 (no shared terms) should be absent.
auto scores = idx.score_query("knowledge", "brown fox");
bool saw1 = false, saw2 = false, saw3 = false;
for (const auto& r : scores) {
if (r.id == 1) saw1 = (r.score > 0.0f);
if (r.id == 2) saw2 = (r.score > 0.0f);
if (r.id == 3) saw3 = true;
}
CHECK(saw1 && "row 1 (quick brown fox) should score");
CHECK(saw2 && "row 2 (lazy brown dog) should score on 'brown'");
CHECK(!saw3 && "row 3 shares no query terms");
// "fox" is rarer than "brown" -> row 1 should outrank row 2.
float s1 = 0, s2 = 0;
for (const auto& r : scores) {
if (r.id == 1) s1 = r.score;
if (r.id == 2) s2 = r.score;
}
CHECK(s1 > s2 && "row with rarer 'fox' term should rank higher");
// ---- PHASE1 Step 3b: index writes must fail loudly -------------------
// FKs must be ON for the junction-table constraint to fire (explicit here
// even though our linked SQLite defaults them ON — the test proves the
// throw regardless of the default).
CHECK(sqlite3_exec(db, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr) == SQLITE_OK);
CHECK(sqlite3_exec(db,
"CREATE TABLE turn_summaries ("
" turn_summary_id INTEGER PRIMARY KEY AUTOINCREMENT,"
" text TEXT,"
" unigram_count INTEGER NOT NULL DEFAULT 0,"
" bigram_count INTEGER NOT NULL DEFAULT 0);",
nullptr, nullptr, nullptr) == SQLITE_OK);
// A record id that does not exist in the base table must throw (the FK
// failure used to be swallowed — the exact mechanism that hid the
// turn_summaries mis-keying bug). replace_record_terms/set_counts are
// protected on a final class, so the path is exercised through the
// public index_record(), which calls both.
bool threw = false;
try {
idx.index_record("turn_summaries", 999999, "brown fox");
} catch (const std::runtime_error&) {
threw = true;
}
CHECK(threw && "indexing a nonexistent record id must throw");
// set_counts on an existing row still works (counts land on the row).
CHECK(sqlite3_exec(db, "INSERT INTO turn_summaries(turn_summary_id, text) VALUES (7, 'seven brown foxes')",
nullptr, nullptr, nullptr) == SQLITE_OK);
idx.index_record("turn_summaries", 7, "seven brown foxes");
{
sqlite3_stmt* s = nullptr;
CHECK(sqlite3_prepare_v2(db,
"SELECT unigram_count FROM turn_summaries WHERE turn_summary_id = 7",
-1, &s, nullptr) == SQLITE_OK);
CHECK(sqlite3_step(s) == SQLITE_ROW);
CHECK(sqlite3_column_int(s, 0) > 0);
sqlite3_finalize(s);
}
sqlite3_close(db);
std::cout << "test_text_index: OK\n";
return 0;
}