Skip to content

Commit 544b6b0

Browse files
authored
feat: add support for named params (#458)
1 parent a34f6a3 commit 544b6b0

9 files changed

+231
-4
lines changed

crates/pgt_lexer/src/lexer.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ impl<'a> Lexer<'a> {
132132
pgt_tokenizer::TokenKind::Eof => SyntaxKind::EOF,
133133
pgt_tokenizer::TokenKind::Backtick => SyntaxKind::BACKTICK,
134134
pgt_tokenizer::TokenKind::PositionalParam => SyntaxKind::POSITIONAL_PARAM,
135+
pgt_tokenizer::TokenKind::NamedParam { kind } => {
136+
match kind {
137+
pgt_tokenizer::NamedParamKind::ColonIdentifier { terminated: false } => {
138+
err = "Missing trailing \" to terminate the named parameter";
139+
}
140+
pgt_tokenizer::NamedParamKind::ColonString { terminated: false } => {
141+
err = "Missing trailing ' to terminate the named parameter";
142+
}
143+
_ => {}
144+
};
145+
SyntaxKind::POSITIONAL_PARAM
146+
}
135147
pgt_tokenizer::TokenKind::QuotedIdent { terminated } => {
136148
if !terminated {
137149
err = "Missing trailing \" to terminate the quoted identifier"

crates/pgt_lexer/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,36 @@ mod tests {
5050
assert!(!errors[0].message.to_string().is_empty());
5151
}
5252

53+
#[test]
54+
fn test_lexing_string_params_with_errors() {
55+
let input = "SELECT :'unterminated string";
56+
let lexed = lex(input);
57+
58+
// Should have tokens
59+
assert!(!lexed.is_empty());
60+
61+
// Should have an error for unterminated string
62+
let errors = lexed.errors();
63+
assert!(!errors.is_empty());
64+
// Check the error message exists
65+
assert!(!errors[0].message.to_string().is_empty());
66+
}
67+
68+
#[test]
69+
fn test_lexing_identifier_params_with_errors() {
70+
let input = "SELECT :\"unterminated string";
71+
let lexed = lex(input);
72+
73+
// Should have tokens
74+
assert!(!lexed.is_empty());
75+
76+
// Should have an error for unterminated string
77+
let errors = lexed.errors();
78+
assert!(!errors.is_empty());
79+
// Check the error message exists
80+
assert!(!errors[0].message.to_string().is_empty());
81+
}
82+
5383
#[test]
5484
fn test_token_ranges() {
5585
let input = "SELECT id";

crates/pgt_lexer_codegen/src/syntax_kind.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const PUNCT: &[(&str, &str)] = &[
4343
("`", "BACKTICK"),
4444
];
4545

46-
const EXTRA: &[&str] = &["POSITIONAL_PARAM", "ERROR", "COMMENT", "EOF"];
46+
const EXTRA: &[&str] = &["POSITIONAL_PARAM", "NAMED_PARAM", "ERROR", "COMMENT", "EOF"];
4747

4848
const LITERALS: &[&str] = &[
4949
"BIT_STRING",

crates/pgt_tokenizer/src/lib.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mod cursor;
22
mod token;
33
use cursor::{Cursor, EOF_CHAR};
4-
pub use token::{Base, LiteralKind, Token, TokenKind};
4+
pub use token::{Base, LiteralKind, NamedParamKind, Token, TokenKind};
55

66
// via: https://github.com/postgres/postgres/blob/db0c96cc18aec417101e37e59fcc53d4bf647915/src/backend/parser/scan.l#L346
77
// ident_start [A-Za-z\200-\377_]
@@ -132,6 +132,46 @@ impl Cursor<'_> {
132132
}
133133
_ => TokenKind::Dot,
134134
},
135+
'@' => {
136+
if is_ident_start(self.first()) {
137+
// Named parameter with @ prefix.
138+
self.eat_while(is_ident_cont);
139+
TokenKind::NamedParam {
140+
kind: NamedParamKind::AtPrefix,
141+
}
142+
} else {
143+
TokenKind::At
144+
}
145+
}
146+
':' => {
147+
// Named parameters in psql with different substitution styles.
148+
//
149+
// https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-INTERPOLATION
150+
match self.first() {
151+
'\'' => {
152+
// Named parameter with colon prefix and single quotes.
153+
self.bump();
154+
let terminated = self.single_quoted_string();
155+
let kind = NamedParamKind::ColonString { terminated };
156+
TokenKind::NamedParam { kind }
157+
}
158+
'"' => {
159+
// Named parameter with colon prefix and double quotes.
160+
self.bump();
161+
let terminated = self.double_quoted_string();
162+
let kind = NamedParamKind::ColonIdentifier { terminated };
163+
TokenKind::NamedParam { kind }
164+
}
165+
c if is_ident_start(c) => {
166+
// Named parameter with colon prefix.
167+
self.eat_while(is_ident_cont);
168+
TokenKind::NamedParam {
169+
kind: NamedParamKind::ColonRaw,
170+
}
171+
}
172+
_ => TokenKind::Colon,
173+
}
174+
}
135175
// One-symbol tokens.
136176
';' => TokenKind::Semi,
137177
'\\' => TokenKind::Backslash,
@@ -140,11 +180,9 @@ impl Cursor<'_> {
140180
')' => TokenKind::CloseParen,
141181
'[' => TokenKind::OpenBracket,
142182
']' => TokenKind::CloseBracket,
143-
'@' => TokenKind::At,
144183
'#' => TokenKind::Pound,
145184
'~' => TokenKind::Tilde,
146185
'?' => TokenKind::Question,
147-
':' => TokenKind::Colon,
148186
'$' => {
149187
// Dollar quoted strings
150188
if is_ident_start(self.first()) || self.first() == '$' {
@@ -613,6 +651,31 @@ mod tests {
613651
}
614652
tokens
615653
}
654+
655+
#[test]
656+
fn named_param_at() {
657+
let result = lex("select 1 from c where id = @id;");
658+
assert_debug_snapshot!(result);
659+
}
660+
661+
#[test]
662+
fn named_param_colon_raw() {
663+
let result = lex("select 1 from c where id = :id;");
664+
assert_debug_snapshot!(result);
665+
}
666+
667+
#[test]
668+
fn named_param_colon_string() {
669+
let result = lex("select 1 from c where id = :'id';");
670+
assert_debug_snapshot!(result);
671+
}
672+
673+
#[test]
674+
fn named_param_colon_identifier() {
675+
let result = lex("select 1 from c where id = :\"id\";");
676+
assert_debug_snapshot!(result);
677+
}
678+
616679
#[test]
617680
fn lex_statement() {
618681
let result = lex("select 1;");
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgt_tokenizer/src/lib.rs
3+
expression: result
4+
snapshot_kind: text
5+
---
6+
[
7+
"select" @ Ident,
8+
" " @ Space,
9+
"1" @ Literal { kind: Int { base: Decimal, empty_int: false } },
10+
" " @ Space,
11+
"from" @ Ident,
12+
" " @ Space,
13+
"c" @ Ident,
14+
" " @ Space,
15+
"where" @ Ident,
16+
" " @ Space,
17+
"id" @ Ident,
18+
" " @ Space,
19+
"=" @ Eq,
20+
" " @ Space,
21+
"@id" @ NamedParam { kind: AtPrefix },
22+
";" @ Semi,
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgt_tokenizer/src/lib.rs
3+
expression: result
4+
snapshot_kind: text
5+
---
6+
[
7+
"select" @ Ident,
8+
" " @ Space,
9+
"1" @ Literal { kind: Int { base: Decimal, empty_int: false } },
10+
" " @ Space,
11+
"from" @ Ident,
12+
" " @ Space,
13+
"c" @ Ident,
14+
" " @ Space,
15+
"where" @ Ident,
16+
" " @ Space,
17+
"id" @ Ident,
18+
" " @ Space,
19+
"=" @ Eq,
20+
" " @ Space,
21+
":\"id\"" @ NamedParam { kind: ColonIdentifier { terminated: true } },
22+
";" @ Semi,
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgt_tokenizer/src/lib.rs
3+
expression: result
4+
snapshot_kind: text
5+
---
6+
[
7+
"select" @ Ident,
8+
" " @ Space,
9+
"1" @ Literal { kind: Int { base: Decimal, empty_int: false } },
10+
" " @ Space,
11+
"from" @ Ident,
12+
" " @ Space,
13+
"c" @ Ident,
14+
" " @ Space,
15+
"where" @ Ident,
16+
" " @ Space,
17+
"id" @ Ident,
18+
" " @ Space,
19+
"=" @ Eq,
20+
" " @ Space,
21+
":id" @ NamedParam { kind: ColonRaw },
22+
";" @ Semi,
23+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
source: crates/pgt_tokenizer/src/lib.rs
3+
expression: result
4+
snapshot_kind: text
5+
---
6+
[
7+
"select" @ Ident,
8+
" " @ Space,
9+
"1" @ Literal { kind: Int { base: Decimal, empty_int: false } },
10+
" " @ Space,
11+
"from" @ Ident,
12+
" " @ Space,
13+
"c" @ Ident,
14+
" " @ Space,
15+
"where" @ Ident,
16+
" " @ Space,
17+
"id" @ Ident,
18+
" " @ Space,
19+
"=" @ Eq,
20+
" " @ Space,
21+
":'id'" @ NamedParam { kind: ColonString { terminated: true } },
22+
";" @ Semi,
23+
]

crates/pgt_tokenizer/src/token.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ pub enum TokenKind {
9494
///
9595
/// see: <https://www.postgresql.org/docs/16/sql-expressions.html#SQL-EXPRESSIONS-PARAMETERS-POSITIONAL>
9696
PositionalParam,
97+
/// Named Parameter, e.g., `@name`
98+
///
99+
/// This is used in some ORMs and query builders, like sqlc.
100+
NamedParam {
101+
kind: NamedParamKind,
102+
},
97103
/// Quoted Identifier, e.g., `"update"` in `update "my_table" set "a" = 5;`
98104
///
99105
/// These are case-sensitive, unlike [`TokenKind::Ident`]
@@ -104,6 +110,30 @@ pub enum TokenKind {
104110
},
105111
}
106112

113+
#[derive(Debug, PartialEq, Clone, Copy)]
114+
pub enum NamedParamKind {
115+
/// e.g. `@name`
116+
///
117+
/// Used in:
118+
/// - sqlc: https://docs.sqlc.dev/en/latest/howto/named_parameters.html
119+
AtPrefix,
120+
121+
/// e.g. `:name` (raw substitution)
122+
///
123+
/// Used in: psql
124+
ColonRaw,
125+
126+
/// e.g. `:'name'` (quoted string substitution)
127+
///
128+
/// Used in: psql
129+
ColonString { terminated: bool },
130+
131+
/// e.g. `:"name"` (quoted identifier substitution)
132+
///
133+
/// Used in: psql
134+
ColonIdentifier { terminated: bool },
135+
}
136+
107137
/// Parsed token.
108138
/// It doesn't contain information about data that has been parsed,
109139
/// only the type of the token and its size.

0 commit comments

Comments
 (0)