mod context;
mod gather;
mod items;
mod keywords;
mod tokenizer;
pub use context::{CompletionContext, detect_context, detect_context_with_schemas};
pub use gather::gather;
pub use items::{Completion, CompletionKind};
pub use keywords::{FUNCTIONS, KEYWORDS, PHRASES};
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use narwhal_core::{ColumnHeader, Schema, Table, TableKind};
use narwhal_domain::SchemaListing;
use super::*;
fn listing() -> Vec<SchemaListing> {
vec![(
Schema {
name: "public".into(),
},
vec![
Table {
schema: "public".into(),
name: "orders".into(),
kind: TableKind::Table,
},
Table {
schema: "public".into(),
name: "order_items".into(),
kind: TableKind::Table,
},
Table {
schema: "public".into(),
name: "users".into(),
kind: TableKind::Table,
},
],
)]
}
fn no_columns() -> HashMap<String, (String, Vec<ColumnHeader>)> {
HashMap::new()
}
#[test]
fn empty_prefix_yields_nothing() {
assert!(
gather(
"",
&listing(),
&CompletionContext::Generic,
&no_columns(),
20
)
.is_empty()
);
}
#[test]
fn prefix_hits_come_before_substring_hits() {
let out = gather(
"or",
&listing(),
&CompletionContext::Generic,
&no_columns(),
20,
);
let ord = out
.iter()
.position(|c| c.text == "orders")
.expect("orders present");
let ord_items = out
.iter()
.position(|c| c.text == "order_items")
.expect("order_items present");
let or = out
.iter()
.position(|c| c.text == "OR")
.expect("OR keyword present");
assert!(ord < out.len() && ord_items < out.len() && or < out.len());
}
#[test]
fn case_insensitive_match() {
let out = gather(
"SEL",
&listing(),
&CompletionContext::Generic,
&no_columns(),
20,
);
assert!(out.iter().any(|c| c.text == "SELECT"));
}
#[test]
fn deduplicates_by_kind_and_name() {
let mut listings = listing();
listings.push(listings[0].clone());
let out = gather(
"orders",
&listings,
&CompletionContext::Generic,
&no_columns(),
20,
);
let n = out.iter().filter(|c| c.text == "orders").count();
assert_eq!(n, 1);
}
#[test]
fn limit_is_respected() {
let out = gather(
"e",
&listing(),
&CompletionContext::Generic,
&no_columns(),
3,
);
assert!(out.len() <= 3);
}
#[test]
fn from_keyword_narrows_to_tables() {
let ctx = detect_context("SELECT * FROM u", 14);
assert_eq!(ctx, CompletionContext::TableExpected);
let out = gather("u", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "users" && c.kind == CompletionKind::Table)
);
assert!(
!out.iter()
.any(|c| c.text == "UNION" && c.kind == CompletionKind::Keyword)
);
assert!(
!out.iter()
.any(|c| c.text == "UPDATE" && c.kind == CompletionKind::Keyword)
);
}
#[test]
fn dotted_identifier_suggests_columns() {
let mut cols = HashMap::new();
cols.insert(
"users".to_owned(),
(
"public".to_owned(),
vec![
ColumnHeader {
name: "id".into(),
data_type: "int4".into(),
},
ColumnHeader {
name: "name".into(),
data_type: "varchar".into(),
},
ColumnHeader {
name: "email".into(),
data_type: "varchar".into(),
},
],
),
);
let ctx = detect_context("SELECT users.", 13);
assert_eq!(
ctx,
CompletionContext::ColumnExpected {
table: "users".into()
}
);
let out = gather("", &listing(), &ctx, &cols, 50);
assert!(out.is_empty());
let out = gather("n", &listing(), &ctx, &cols, 50);
assert!(
out.iter()
.any(|c| c.text == "name" && c.kind == CompletionKind::Column)
);
assert!(!out.iter().any(|c| c.kind == CompletionKind::Keyword));
}
#[test]
fn context_stops_at_previous_semicolon() {
let ctx = detect_context("SELECT * FROM users; SELECT u", 27);
assert_eq!(ctx, CompletionContext::Generic);
}
#[test]
fn join_keyword_narrows_to_tables() {
let ctx = detect_context("SELECT * FROM orders JOIN u", 27);
assert_eq!(ctx, CompletionContext::TableExpected);
let out = gather("u", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "users" && c.kind == CompletionKind::Table)
);
assert!(
!out.iter()
.any(|c| c.text == "UNION" && c.kind == CompletionKind::Keyword)
);
}
#[test]
fn update_keyword_narrows_to_tables() {
let ctx = detect_context("UPDATE u", 8);
assert_eq!(ctx, CompletionContext::TableExpected);
let out = gather("u", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "users" && c.kind == CompletionKind::Table)
);
assert!(
!out.iter()
.any(|c| c.text == "UNION" && c.kind == CompletionKind::Keyword)
);
}
fn user_cols() -> HashMap<String, (String, Vec<ColumnHeader>)> {
let mut m = HashMap::new();
m.insert(
"users".to_owned(),
(
"public".to_owned(),
vec![
ColumnHeader {
name: "id".into(),
data_type: "int4".into(),
},
ColumnHeader {
name: "email".into(),
data_type: "text".into(),
},
],
),
);
m
}
#[test]
fn alias_in_from_resolves_to_real_table_for_dot_completion() {
let buf = "SELECT * FROM users u WHERE u.";
let ctx = detect_context(buf, buf.len());
assert_eq!(
ctx,
CompletionContext::ColumnExpected {
table: "users".into()
}
);
let out = gather("e", &listing(), &ctx, &user_cols(), 50);
assert!(
out.iter()
.any(|c| c.text == "email" && c.kind == CompletionKind::Column)
);
}
#[test]
fn alias_with_explicit_as_keyword_is_resolved() {
let mut cols = user_cols();
cols.insert(
"orders".to_owned(),
(
"public".to_owned(),
vec![ColumnHeader {
name: "total".into(),
data_type: "numeric".into(),
}],
),
);
let buf = "SELECT * FROM users u JOIN orders AS o ON o.";
let ctx = detect_context(buf, buf.len());
assert_eq!(
ctx,
CompletionContext::ColumnExpected {
table: "orders".into()
}
);
let out = gather("t", &listing(), &ctx, &cols, 50);
assert!(out.iter().any(|c| c.text == "total"));
}
#[test]
fn schema_prefix_narrows_table_list() {
let buf = "SELECT * FROM public.";
let known = vec!["public".to_owned()];
let ctx = detect_context_with_schemas(buf, buf.len(), &known);
assert_eq!(
ctx,
CompletionContext::SchemaTableExpected {
schema: "public".into()
}
);
let out = gather("u", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "users" && c.kind == CompletionKind::Table)
);
}
#[test]
fn unknown_dotted_prefix_falls_back_to_column_lookup() {
let buf = "SELECT * FROM public.";
let ctx = detect_context(buf, buf.len());
assert_eq!(
ctx,
CompletionContext::ColumnExpected {
table: "public".into()
}
);
}
#[test]
fn generic_context_includes_functions() {
let ctx = detect_context("SELECT ", 7);
assert_eq!(ctx, CompletionContext::Generic);
let out = gather("cou", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "COUNT(" && c.kind == CompletionKind::Function)
);
}
#[test]
fn function_kind_distinct_from_keyword() {
let ctx = detect_context("SELECT ", 7);
let out = gather("now", &listing(), &ctx, &no_columns(), 50);
assert!(
out.iter()
.any(|c| c.text == "NOW()" && c.kind == CompletionKind::Function)
);
}
}