Skip to content

feat(completions): complete in WITH CHECK and USING clauses #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 54 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
daf8efd
sure
juleswritescode May 27, 2025
dcf2439
so far!
juleswritescode May 27, 2025
9abf459
so far
juleswritescode May 28, 2025
8d4836e
yayyy
juleswritescode May 28, 2025
363c2ea
setup roles
juleswritescode May 28, 2025
0e1f0a8
use distinct method
juleswritescode May 28, 2025
8e4d17c
better…
juleswritescode May 28, 2025
d1d8453
sqlx prepare
juleswritescode May 29, 2025
9021bc0
ok
juleswritescode May 29, 2025
0753b50
better
juleswritescode May 29, 2025
1be61b4
ok
juleswritescode May 29, 2025
e55271f
ok
juleswritescode May 29, 2025
9cd04dd
ok
juleswritescode May 29, 2025
7e1c565
ok
juleswritescode May 29, 2025
b37adda
ok
juleswritescode May 29, 2025
cc38757
adjust test
juleswritescode May 29, 2025
a1e1a9c
ok
juleswritescode May 29, 2025
403cf82
resolve conflicts
juleswritescode Jun 2, 2025
9c3184e
ok
juleswritescode Jun 2, 2025
299e469
ok
juleswritescode Jun 3, 2025
2d37803
quicksave
juleswritescode Jun 3, 2025
027324f
reading the card…
juleswritescode Jun 3, 2025
0dd285f
wowa wiwa
juleswritescode Jun 3, 2025
f72297a
ok
juleswritescode Jun 5, 2025
578741e
lowercase…
juleswritescode Jun 5, 2025
ef5cb98
wowa wiwa
juleswritescode Jun 5, 2025
9214c91
add tests
juleswritescode Jun 5, 2025
f2b4b44
linty
juleswritescode Jun 5, 2025
b5e82ed
format
juleswritescode Jun 5, 2025
e736bd0
Merge branch 'main' of https://github.com/supabase-community/postgres…
juleswritescode Jun 5, 2025
cab8ead
Merge branch 'main' into feat/to-role
juleswritescode Jun 5, 2025
f109ece
Merge branch 'main' into feat/to-role
juleswritescode Jun 6, 2025
4beb1f2
merged
juleswritescode Jun 6, 2025
3117d07
simplify word parser
juleswritescode Jun 6, 2025
6640417
infer position
juleswritescode Jun 6, 2025
4c04ba9
hm
juleswritescode Jun 6, 2025
9a4e9fc
ok
juleswritescode Jun 6, 2025
eb54d62
cool
juleswritescode Jun 6, 2025
e9205c4
ok
juleswritescode Jun 6, 2025
f0ffd1d
ok
juleswritescode Jun 6, 2025
c1cbed8
fix tests
juleswritescode Jun 6, 2025
e8dbfdf
Update crates/pgt_completions/src/relevance/filtering.rs
juleswritescode Jun 6, 2025
11a4f57
fix sad bug
juleswritescode Jun 6, 2025
142c8e8
Merge branch 'feat/check-using' of https://github.com/supabase-commun…
juleswritescode Jun 6, 2025
ef6df7d
Update crates/pgt_completions/src/sanitization.rs
juleswritescode Jun 6, 2025
db31640
lintci
juleswritescode Jun 9, 2025
4da85cf
Merge branch 'feat/check-using' of https://github.com/supabase-commun…
juleswritescode Jun 9, 2025
6f12fcc
fmt
juleswritescode Jun 9, 2025
673b8d6
merge with main
juleswritescode Jun 10, 2025
2379f0f
Merge branch 'main' into feat/check-using
juleswritescode Jun 11, 2025
c1050c8
Merge branch 'main' of https://github.com/supabase-community/postgres…
juleswritescode Jun 12, 2025
d9497dd
ok
juleswritescode Jun 12, 2025
f03af23
ack
juleswritescode Jun 12, 2025
5606cf3
Merge branch 'feat/check-using' of https://github.com/supabase-commun…
juleswritescode Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/pgt_completions/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
context::CompletionContext,
item::CompletionItem,
providers::{
complete_columns, complete_functions, complete_policies, complete_schemas, complete_tables,
complete_columns, complete_functions, complete_policies, complete_roles, complete_schemas,
complete_tables,
},
sanitization::SanitizedCompletionParams,
};
Expand Down Expand Up @@ -36,6 +37,7 @@ pub fn complete(params: CompletionParams) -> Vec<CompletionItem> {
complete_columns(&ctx, &mut builder);
complete_schemas(&ctx, &mut builder);
complete_policies(&ctx, &mut builder);
complete_roles(&ctx, &mut builder);

builder.finish()
}
273 changes: 273 additions & 0 deletions crates/pgt_completions/src/context/base_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use pgt_text_size::{TextRange, TextSize};
use std::iter::Peekable;

pub(crate) struct TokenNavigator {
tokens: Peekable<std::vec::IntoIter<WordWithIndex>>,
pub previous_token: Option<WordWithIndex>,
pub current_token: Option<WordWithIndex>,
}

impl TokenNavigator {
pub(crate) fn next_matches(&mut self, options: &[&str]) -> bool {
self.tokens
.peek()
.is_some_and(|c| options.contains(&c.get_word_without_quotes().as_str()))
}

pub(crate) fn prev_matches(&self, options: &[&str]) -> bool {
self.previous_token
.as_ref()
.is_some_and(|t| options.contains(&t.get_word_without_quotes().as_str()))
}

pub(crate) fn advance(&mut self) -> Option<WordWithIndex> {
// we can't peek back n an iterator, so we'll have to keep track manually.
self.previous_token = self.current_token.take();
self.current_token = self.tokens.next();
self.current_token.clone()
}
}

impl From<Vec<WordWithIndex>> for TokenNavigator {
fn from(tokens: Vec<WordWithIndex>) -> Self {
TokenNavigator {
tokens: tokens.into_iter().peekable(),
previous_token: None,
current_token: None,
}
}
}

pub(crate) trait CompletionStatementParser: Sized {
type Context: Default;
const NAME: &'static str;

fn looks_like_matching_stmt(sql: &str) -> bool;
fn parse(self) -> Self::Context;
fn make_parser(tokens: Vec<WordWithIndex>, cursor_position: usize) -> Self;

fn get_context(sql: &str, cursor_position: usize) -> Self::Context {
assert!(
Self::looks_like_matching_stmt(sql),
"Using {} for a wrong statement! Developer Error!",
Self::NAME
);

match sql_to_words(sql) {
Ok(tokens) => {
let parser = Self::make_parser(tokens, cursor_position);
parser.parse()
}
Err(_) => Self::Context::default(),
}
}
}

pub(crate) fn schema_and_table_name(token: &WordWithIndex) -> (String, Option<String>) {
let word = token.get_word_without_quotes();
let mut parts = word.split('.');

(
parts.next().unwrap().into(),
parts.next().map(|tb| tb.into()),
)
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct WordWithIndex {
word: String,
start: usize,
end: usize,
}

impl WordWithIndex {
pub(crate) fn is_under_cursor(&self, cursor_pos: usize) -> bool {
self.start <= cursor_pos && self.end > cursor_pos
}

pub(crate) fn get_range(&self) -> TextRange {
let start: u32 = self.start.try_into().expect("Text too long");
let end: u32 = self.end.try_into().expect("Text too long");
TextRange::new(TextSize::from(start), TextSize::from(end))
}

pub(crate) fn get_word_without_quotes(&self) -> String {
self.word.replace('"', "")
}

pub(crate) fn get_word(&self) -> String {
self.word.clone()
}
}

pub(crate) struct SubStatementParser {
start_of_word: Option<usize>,
current_word: String,
in_quotation_marks: bool,
is_fn_call: bool,
words: Vec<WordWithIndex>,
}

impl SubStatementParser {
pub(crate) fn parse(sql: &str) -> Result<Vec<WordWithIndex>, String> {
let mut parser = SubStatementParser {
start_of_word: None,
current_word: String::new(),
in_quotation_marks: false,
is_fn_call: false,
words: vec![],
};

parser.collect_words(sql);

if parser.in_quotation_marks {
Err("String was not closed properly.".into())
} else {
Ok(parser.words)
}
}

pub fn collect_words(&mut self, sql: &str) {
for (pos, c) in sql.char_indices() {
match c {
'"' => {
if !self.has_started_word() {
self.in_quotation_marks = true;
self.add_char(c);
self.start_word(pos);
} else {
self.in_quotation_marks = false;
self.add_char(c);
}
}

'(' => {
if !self.has_started_word() {
self.push_char_as_word(c, pos);
} else {
self.add_char(c);
self.is_fn_call = true;
}
}

')' => {
if self.is_fn_call {
self.add_char(c);
self.is_fn_call = false;
} else {
if self.has_started_word() {
self.push_word(pos);
}
self.push_char_as_word(c, pos);
}
}

_ => {
if c.is_ascii_whitespace() || c == ';' {
if self.in_quotation_marks {
self.add_char(c);
} else if !self.is_empty() && self.has_started_word() {
self.push_word(pos);
}
} else if self.has_started_word() {
self.add_char(c);
} else {
self.start_word(pos);
self.add_char(c)
}
}
}
}

if self.has_started_word() && !self.is_empty() {
self.push_word(sql.len())
}
}

fn is_empty(&self) -> bool {
self.current_word.is_empty()
}

fn add_char(&mut self, c: char) {
self.current_word.push(c)
}

fn start_word(&mut self, pos: usize) {
self.start_of_word = Some(pos);
}

fn has_started_word(&self) -> bool {
self.start_of_word.is_some()
}

fn push_char_as_word(&mut self, c: char, pos: usize) {
self.words.push(WordWithIndex {
word: String::from(c),
start: pos,
end: pos + 1,
});
}

fn push_word(&mut self, current_position: usize) {
self.words.push(WordWithIndex {
word: self.current_word.clone(),
start: self.start_of_word.unwrap(),
end: current_position,
});
self.current_word = String::new();
self.start_of_word = None;
}
}

/// Note: A policy name within quotation marks will be considered a single word.
pub(crate) fn sql_to_words(sql: &str) -> Result<Vec<WordWithIndex>, String> {
SubStatementParser::parse(sql)
}

#[cfg(test)]
mod tests {
use crate::context::base_parser::{SubStatementParser, WordWithIndex, sql_to_words};

#[test]
fn determines_positions_correctly() {
let query = "\ncreate policy \"my cool pol\"\n\ton auth.users\n\tas permissive\n\tfor select\n\t\tto public\n\t\tusing (auth.uid());".to_string();

let words = SubStatementParser::parse(query.as_str()).unwrap();

assert_eq!(words[0], to_word("create", 1, 7));
assert_eq!(words[1], to_word("policy", 8, 14));
assert_eq!(words[2], to_word("\"my cool pol\"", 15, 28));
assert_eq!(words[3], to_word("on", 30, 32));
assert_eq!(words[4], to_word("auth.users", 33, 43));
assert_eq!(words[5], to_word("as", 45, 47));
assert_eq!(words[6], to_word("permissive", 48, 58));
assert_eq!(words[7], to_word("for", 60, 63));
assert_eq!(words[8], to_word("select", 64, 70));
assert_eq!(words[9], to_word("to", 73, 75));
assert_eq!(words[10], to_word("public", 78, 84));
assert_eq!(words[11], to_word("using", 87, 92));
assert_eq!(words[12], to_word("(", 93, 94));
assert_eq!(words[13], to_word("auth.uid()", 94, 104));
assert_eq!(words[14], to_word(")", 104, 105));
}

#[test]
fn handles_schemas_in_quotation_marks() {
let query = r#"grant select on "public"."users""#.to_string();

let words = sql_to_words(query.as_str()).unwrap();

assert_eq!(words[0], to_word("grant", 0, 5));
assert_eq!(words[1], to_word("select", 6, 12));
assert_eq!(words[2], to_word("on", 13, 15));
assert_eq!(words[3], to_word(r#""public"."users""#, 16, 32));
}

fn to_word(word: &str, start: usize, end: usize) -> WordWithIndex {
WordWithIndex {
word: word.into(),
start,
end,
}
}
}
Loading
Loading