Repository navigation
feat(ai): give the schema prompt to the tables the question is about - #3750
Conversation
J2TeamNNL
left a comment
There was a problem hiding this comment.
Review notes on the schema relevance ranking. Each comment is a place where the behavior can go wrong.
| } | ||
| } | ||
|
|
||
| private func prioritizeSchema(for question: String) { |
There was a problem hiding this comment.
Ranking only sees columns for tables that are already loaded, and those are the first tables in default order. On the first question no columns are loaded, so a question like "which rows have an email" cannot find a table at position 300 by its column. On later turns the tables loaded earlier outrank better matches that are not loaded yet.
| } | ||
| } | ||
|
|
||
| private func prioritizeSchema(for question: String) { |
There was a problem hiding this comment.
A follow-up that matches no table ("and by month?") sets relevantTableNames to an empty list. The next prompt then falls back to alphabetical order and the tables the chat is about can drop out of the first maxTables. Keep the previous ranking when the new one is empty.
| } | ||
|
|
||
| private func prioritizeSchema(for question: String) { | ||
| let text = [question, currentQuery ?? ""].joined(separator: "\n") |
There was a problem hiding this comment.
currentQuery is joined into the ranking text. SQL words like order, group, user and table then match table names at full weight. SELECT * FROM x ORDER BY y boosts orders and order_items.
| await inFlight.value | ||
| return | ||
| // The load in flight fetched for the previous order; a new order may need other tables. | ||
| guard question != nil else { return } |
There was a problem hiding this comment.
Race in ensureSchemaLoaded. Two callers that wait on the same in-flight load both resume afterwards. The first sets inFlightSchemaLoad = nil and can clear the task the second caller just stored, so a third caller starts a duplicate load. Clear it only when it is still your own task.
| counts[word, default: 0] += 1 | ||
| } | ||
| } | ||
| for term in counts.keys { |
There was a problem hiding this comment.
Fuzzy matching builds trigram sets for the query word and for every vocabulary term on each comparison. With thousands of tables this allocates a lot, and it runs on the main actor. Compute term trigrams once per index.
| var score = 0.0 | ||
| for match in matches { | ||
| guard let frequency = terms[match.term] else { continue } | ||
| score += match.weight * index.bm25(frequency: frequency, idfTerm: match.term, length: index.lengths[offset]) |
There was a problem hiding this comment.
The full-name boost uses substring containment on the joined query words. A table data gets +5 from "metadata", and user from "username". The +5 outweighs the BM25 score. Match whole words in order instead.
| } | ||
| return word | ||
| } | ||
|
|
There was a problem hiding this comment.
The stemmer maps "statuses" to "statuse" but leaves "status" alone, and "caches" to "cach". It also skips non-ASCII words, so a question in CJK or Cyrillic shares no words with English identifiers and nothing ranks.
| lines.append( | ||
| "\n… and \(tables.count - maxTables) more tables (not shown)" | ||
| ) | ||
| let hiddenTables = tables.dropFirst(maxTables) |
There was a problem hiding this comment.
Up to 200 hidden table names are now added to every prompt, where before it was a count line. There is no token budget for them, so a database with 1000+ tables pays this on every message.
| guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } | ||
| relevantTableNames = AISchemaContext.relevantTableNames( | ||
| in: tables, | ||
| columnsByTable: columnsByTable, |
There was a problem hiding this comment.
relevantTableNames is shared mutable state set at the start of a turn and read later by orderedTables(). A slash command or an attachment prime that runs in between can render with another turn's ordering.
| guard let self else { return } | ||
| if settings.includeSchema { | ||
| await self.ensureSchemaLoaded() | ||
| let question = await MainActor.run { |
There was a problem hiding this comment.
The question is read from messages inside the detached task after an actor hop, not when the turn is created. If the history changes in between (regenerate, edit), the ranking uses the wrong question. Capture it before the task starts.
… clear the schema load only when it is ours A follow-up that matches no table no longer resets the ranking. The full-name boost needs the name's words in order in the question. Term trigrams are built once per index. The in-flight schema load is cleared only by the task that set it, and the question is read when the turn starts.
|
Follow-up to the review above. Fixes are in J2TeamNNL@8cf9ddf.
Checks: swiftlint --strict on ViewModels and Core/AI is clean. The ranker tests pass in a scratch package (8 of 8). The app target was not built here, so CI has to confirm it. |
…y, and keep hidden tables a count
… AI policy, and read table references past comments
…t table spelling first
… shares its name with
… an ALTER target's schema
…ulk entry only when it means one table
Summary
maxSchemaTablestables (20 by default), and took the first ones listed. Each turn now gives that budget to the tables it is about.SQLContextAnalyzer.tableReferences(in:grammar:). In chat this covers every statement in the 2,000 characters the prompt carries, while Include current query is on and the editor is SQL. Inline suggestions use the statement at the cursor.SchemaRelevanceRankerinTableProAutocomplete). The ranker reads table names and comments. It reads columns only once every table has them, so a table loaded on an earlier turn never outranks one whose columns were never read.fetchAllColumnsis one query (newDatabaseDriver.providesBulkColumnFetch), the first ranked turn reads every column in that one query.list_tablesanddescribe_table.ALTER TABLE schema.tablenow resolves totableinschema, for completion too, where it resolved to a table namedschema.What reaches the model is unchanged: the same settings and budget decide it. Only which tables fill the budget changes.
Docs: TableProApp/docs#122
Test plan
SchemaRelevanceRankerTestsandSQLContextAnalyzerTableReferenceTests(TableProCore,swift test): ranking, a statement ending in a comment or open string, a CTE that shares a table's name, a qualifiedALTERtarget.AISchemaContextRelevanceTests: question order, table comments, columns only when every table has them, editor tables first, SQL keywords match nothing, every statement counts, default schema first, case collisions keep both, the count line.SchemaContextForAITests: the tables the cursor's statement reads take the inline budget, including a table named after the first 2,000 characters.AIChatViewModelSlashTests: a custom command ranks by its body; a tool result turn does not replace the question.