2@testable import ParseableViewer
4final class SQLAutoCompleteTests: XCTestCase {
6 private let streams = ["access_logs", "error_logs", "metrics"]
8 SchemaField(name: "p_timestamp", dataType: "Utf8"),
9 SchemaField(name: "level", dataType: "Utf8"),
10 SchemaField(name: "message", dataType: "Utf8"),
11 SchemaField(name: "host", dataType: "Utf8"),
12 SchemaField(name: "status_code", dataType: "Int64"),
15 // MARK: - Context detection
17 func testContextAfterSelect() {
18 let ctx = SQLCompletionProvider.determineContext(text: "SELECT ", position: 7)
19 XCTAssertEqual(ctx, .columnRef)
22 func testContextAfterFrom() {
23 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM ", position: 14)
24 XCTAssertEqual(ctx, .tableRef)
27 func testContextAfterWhere() {
28 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM t WHERE ", position: 22)
29 XCTAssertEqual(ctx, .columnRef)
32 func testContextAfterAnd() {
33 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM t WHERE x = 1 AND ", position: 32)
34 XCTAssertEqual(ctx, .columnRef)
37 func testContextAfterJoin() {
38 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM a JOIN ", position: 21)
39 XCTAssertEqual(ctx, .tableRef)
42 func testContextAfterOrder() {
43 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM t ORDER ", position: 22)
44 XCTAssertEqual(ctx, .afterOrder)
47 func testContextAfterGroup() {
48 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM t GROUP ", position: 22)
49 XCTAssertEqual(ctx, .afterGroup)
52 func testContextAfterCommaInSelect() {
53 let ctx = SQLCompletionProvider.determineContext(text: "SELECT a, ", position: 10)
54 XCTAssertEqual(ctx, .columnRef)
57 func testContextAfterCommaInFrom() {
58 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM a, ", position: 17)
59 XCTAssertEqual(ctx, .tableRef)
62 func testContextGeneral() {
63 let ctx = SQLCompletionProvider.determineContext(text: "", position: 0)
64 XCTAssertEqual(ctx, .general)
67 // MARK: - Completions for keywords
69 func testKeywordCompletionSelect() {
70 let result = SQLCompletionProvider.completions(
71 for: "SEL", cursorPosition: 3, streamNames: streams, schemaFields: fields
73 XCTAssertTrue(result.items.contains(where: { $0.text == "SELECT" }))
74 XCTAssertEqual(result.prefix, "SEL")
75 XCTAssertEqual(result.range, NSRange(location: 0, length: 3))
78 func testKeywordCompletionCaseInsensitive() {
79 let result = SQLCompletionProvider.completions(
80 for: "sel", cursorPosition: 3, streamNames: streams, schemaFields: fields
82 XCTAssertTrue(result.items.contains(where: { $0.text == "SELECT" }))
85 func testNoCompletionOnExactMatch() {
86 let result = SQLCompletionProvider.completions(
87 for: "SELECT", cursorPosition: 6, streamNames: [], schemaFields: []
89 // When the only match is exact, no items are returned
90 XCTAssertTrue(result.items.isEmpty)
93 // MARK: - Table name completions
95 func testTableCompletionAfterFrom() {
96 let result = SQLCompletionProvider.completions(
97 for: "SELECT * FROM ac", cursorPosition: 16, streamNames: streams, schemaFields: fields
99 XCTAssertTrue(result.items.contains(where: { $0.text == "\"access_logs\"" && $0.kind == .table }))
100 XCTAssertFalse(result.items.contains(where: { $0.text == "\"metrics\"" }))
103 func testTableCompletionIncludesQuotes() {
104 let result = SQLCompletionProvider.completions(
105 for: "SELECT * FROM err", cursorPosition: 17, streamNames: streams, schemaFields: fields
107 let tableItem = result.items.first(where: { $0.kind == .table })
108 XCTAssertNotNil(tableItem)
109 XCTAssertEqual(tableItem?.text, "\"error_logs\"")
112 // MARK: - Column completions
114 func testColumnCompletionAfterWhere() {
115 let result = SQLCompletionProvider.completions(
116 for: "SELECT * FROM t WHERE lev", cursorPosition: 25,
117 streamNames: streams, schemaFields: fields
119 XCTAssertTrue(result.items.contains(where: { $0.text == "level" && $0.kind == .column }))
122 func testColumnCompletionIncludesDetail() {
123 let result = SQLCompletionProvider.completions(
124 for: "SELECT * FROM t WHERE sta", cursorPosition: 25,
125 streamNames: streams, schemaFields: fields
127 let colItem = result.items.first(where: { $0.kind == .column })
128 XCTAssertNotNil(colItem)
129 XCTAssertEqual(colItem?.detail, "Int64")
132 func testColumnCompletionAfterSelect() {
133 let result = SQLCompletionProvider.completions(
134 for: "SELECT mes", cursorPosition: 10,
135 streamNames: streams, schemaFields: fields
137 XCTAssertTrue(result.items.contains(where: { $0.text == "message" && $0.kind == .column }))
140 // MARK: - Function completions
142 func testFunctionCompletion() {
143 let result = SQLCompletionProvider.completions(
144 for: "SELECT COU", cursorPosition: 10,
145 streamNames: streams, schemaFields: fields
147 XCTAssertTrue(result.items.contains(where: { $0.text == "COUNT" && $0.kind == .function }))
150 // MARK: - Empty prefix returns nothing
152 func testEmptyPrefixNoCompletions() {
153 let result = SQLCompletionProvider.completions(
154 for: "SELECT ", cursorPosition: 7,
155 streamNames: streams, schemaFields: fields
157 XCTAssertTrue(result.items.isEmpty)
160 // MARK: - Prefix range
162 func testPrefixRangeInMiddle() {
163 let text = "SELECT * FROM acc WHERE"
164 let result = SQLCompletionProvider.completions(
165 for: text, cursorPosition: 17,
166 streamNames: streams, schemaFields: fields
168 // "acc" starts at position 14
169 XCTAssertEqual(result.range, NSRange(location: 14, length: 3))
170 XCTAssertEqual(result.prefix, "acc")
173 // MARK: - ORDER BY / GROUP BY
175 func testOrderByCompletion() {
176 let result = SQLCompletionProvider.completions(
177 for: "SELECT * FROM t ORDER B", cursorPosition: 23,
178 streamNames: streams, schemaFields: fields
180 XCTAssertTrue(result.items.contains(where: { $0.text == "BY" }))
183 func testGroupByCompletion() {
184 let result = SQLCompletionProvider.completions(
185 for: "SELECT * FROM t GROUP B", cursorPosition: 23,
186 streamNames: streams, schemaFields: fields
188 XCTAssertTrue(result.items.contains(where: { $0.text == "BY" }))
191 // MARK: - isWordCharacter
193 func testWordCharacters() {
194 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x41)) // A
195 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x5A)) // Z
196 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x61)) // a
197 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x7A)) // z
198 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x30)) // 0
199 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x39)) // 9
200 XCTAssertTrue(SQLCompletionProvider.isWordCharacter(0x5F)) // _
201 XCTAssertFalse(SQLCompletionProvider.isWordCharacter(0x20)) // space
202 XCTAssertFalse(SQLCompletionProvider.isWordCharacter(0x2C)) // comma
203 XCTAssertFalse(SQLCompletionProvider.isWordCharacter(0x28)) // (
206 // MARK: - Edge cases
208 func testCursorAtZero() {
209 let result = SQLCompletionProvider.completions(
210 for: "SELECT", cursorPosition: 0,
211 streamNames: streams, schemaFields: fields
213 XCTAssertTrue(result.items.isEmpty)
216 func testCursorBeyondEnd() {
217 let result = SQLCompletionProvider.completions(
218 for: "SEL", cursorPosition: 100,
219 streamNames: streams, schemaFields: fields
221 XCTAssertTrue(result.items.isEmpty)
224 func testEmptyText() {
225 let result = SQLCompletionProvider.completions(
226 for: "", cursorPosition: 0,
227 streamNames: streams, schemaFields: fields
229 XCTAssertTrue(result.items.isEmpty)
232 // MARK: - Mixed context completions
234 func testGeneralContextIncludesAllKinds() {
235 // Typing "m" at the start should match keywords (no keywords start with m),
236 // functions, tables ("metrics"), and columns ("message")
237 let result = SQLCompletionProvider.completions(
238 for: "m", cursorPosition: 1,
239 streamNames: streams, schemaFields: fields
241 XCTAssertTrue(result.items.contains(where: { $0.kind == .function })) // MAX, MIN
242 XCTAssertTrue(result.items.contains(where: { $0.kind == .table })) // metrics
243 XCTAssertTrue(result.items.contains(where: { $0.kind == .column })) // message
246 // MARK: - Completion item properties
248 func testCompletionItemKindLabels() {
249 let kw = SQLCompletionItem(text: "SELECT", kind: .keyword)
250 XCTAssertEqual(kw.kindLabel, "K")
252 let fn = SQLCompletionItem(text: "COUNT", kind: .function)
253 XCTAssertEqual(fn.kindLabel, "F")
255 let tbl = SQLCompletionItem(text: "logs", kind: .table)
256 XCTAssertEqual(tbl.kindLabel, "T")
258 let col = SQLCompletionItem(text: "level", kind: .column)
259 XCTAssertEqual(col.kindLabel, "C")
262 // MARK: - Context with DISTINCT, NOT, BETWEEN, etc.
264 func testContextAfterDistinct() {
265 let ctx = SQLCompletionProvider.determineContext(text: "SELECT DISTINCT ", position: 16)
266 XCTAssertEqual(ctx, .columnRef)
269 func testContextAfterNot() {
270 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM t WHERE NOT ", position: 26)
271 XCTAssertEqual(ctx, .columnRef)
274 func testContextAfterLeftJoin() {
275 let ctx = SQLCompletionProvider.determineContext(text: "SELECT * FROM a LEFT JOIN ", position: 26)
276 XCTAssertEqual(ctx, .tableRef)
279 // MARK: - Identifier quoting / non-SQL regions
281 func testColumnCompletionQuotesNamesThatNeedIt() {
282 let fields = [SchemaField(name: "statusCode", dataType: "Int64"), SchemaField(name: "status", dataType: "Utf8")]
283 let result = SQLCompletionProvider.completions(
284 for: "SELECT * FROM t WHERE stat", cursorPosition: 26,
285 streamNames: [], schemaFields: fields
287 let columns = result.items.filter { $0.kind == .column }.map(\.text)
288 XCTAssertEqual(columns, ["status", "\"statusCode\""])
291 func testNoCompletionsInsideLineComment() {
292 let text = "-- grouped by date"
293 let result = SQLCompletionProvider.completions(
294 for: text, cursorPosition: (text as NSString).length,
295 streamNames: streams, schemaFields: fields
297 XCTAssertTrue(result.items.isEmpty)
300 func testNoCompletionsInsideStringLiteral() {
301 let text = "SELECT * FROM t WHERE msg LIKE '%sel"
302 let result = SQLCompletionProvider.completions(
303 for: text, cursorPosition: (text as NSString).length,
304 streamNames: streams, schemaFields: fields
306 XCTAssertTrue(result.items.isEmpty)
309 func testFullyTypedMixedCaseColumnStillOffersQuotedForm() {
310 let fields = [SchemaField(name: "statusCode", dataType: "Int64")]
311 let bare = "SELECT * FROM t WHERE statusCode"
312 let result = SQLCompletionProvider.completions(
313 for: bare, cursorPosition: (bare as NSString).length,
314 streamNames: [], schemaFields: fields
316 XCTAssertEqual(result.items.map(\.text), ["\"statusCode\""])
318 // Already quoted: accepting would change nothing, so no popup.
319 let quoted = "SELECT * FROM t WHERE \"statusCode\""
320 let quotedResult = SQLCompletionProvider.completions(
321 for: quoted, cursorPosition: (quoted as NSString).length - 1,
322 streamNames: [], schemaFields: fields
324 XCTAssertTrue(quotedResult.items.isEmpty)
327 func testReservedWordColumnIsQuoted() {
328 let fields = [SchemaField(name: "order", dataType: "Utf8")]
329 let result = SQLCompletionProvider.completions(
330 for: "SELECT ord", cursorPosition: 10, streamNames: [], schemaFields: fields
332 XCTAssertEqual(result.items.filter { $0.kind == .column }.map(\.text), ["\"order\""])
336// Make SQLCompletionContext Equatable for assertions
337extension SQLCompletionContext: Equatable {}
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list