Skip to content

Commit 0c40983

Browse files
psteinroeclaude
andcommitted
fix: improve dollar quote handling and identifier quoting in pretty printer
- Use dollar quotes for function/procedure bodies instead of single quotes - Add DollarQuoteHint enum to select context-appropriate delimiters ($function$ for functions, $procedure$ for procedures, $do$ for DO blocks) - Handle nested dollar quotes by falling back to alternative delimiters - Use emit_identifier_maybe_quoted for identifiers that don't need quoting (lowercase names without special characters or reserved keywords) - Add tests for dollar quoted functions, nested dollar quotes, and lowercase/quoted table identifiers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a996747 commit 0c40983

File tree

366 files changed

+9993
-9813
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

366 files changed

+9993
-9813
lines changed

crates/pgls_pretty_print/src/nodes/alter_function_stmt.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,20 @@ pub(super) fn emit_alter_function_stmt(e: &mut EventEmitter, n: &AlterFunctionSt
2727
emit_object_with_args(e, func);
2828
}
2929

30+
// Determine the dollar quote hint based on whether this is a procedure or function
31+
// ObjectType: ObjectFunction=20, ObjectProcedure=30
32+
let dollar_hint = if n.objtype == 30 {
33+
super::DollarQuoteHint::Procedure
34+
} else {
35+
super::DollarQuoteHint::Function
36+
};
37+
3038
// Emit actions (function options like IMMUTABLE, SECURITY DEFINER, etc.)
3139
if !n.actions.is_empty() {
3240
e.line(LineType::SoftOrSpace);
3341
emit_comma_separated_list(e, &n.actions, |node, e| {
3442
let def_elem = assert_node_variant!(DefElem, node);
35-
super::create_function_stmt::format_function_option(e, def_elem);
43+
super::create_function_stmt::format_function_option(e, def_elem, dollar_hint);
3644
});
3745
}
3846

crates/pgls_pretty_print/src/nodes/create_function_stmt.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,18 @@ pub(super) fn emit_create_function_stmt(e: &mut EventEmitter, n: &CreateFunction
8383
}
8484
}
8585

86+
// Determine the dollar quote hint based on whether this is a procedure or function
87+
let dollar_hint = if n.is_procedure {
88+
super::DollarQuoteHint::Procedure
89+
} else {
90+
super::DollarQuoteHint::Function
91+
};
92+
8693
// Options
8794
for option in &n.options {
8895
if let Some(pgls_query::NodeEnum::DefElem(def_elem)) = &option.node {
8996
e.line(LineType::Hard);
90-
format_function_option(e, def_elem);
97+
format_function_option(e, def_elem, dollar_hint);
9198
}
9299
}
93100

@@ -176,7 +183,11 @@ fn emit_function_parameter_list(e: &mut EventEmitter, params: &[&FunctionParamet
176183
}
177184
}
178185

179-
pub(super) fn format_function_option(e: &mut EventEmitter, d: &pgls_query::protobuf::DefElem) {
186+
pub(super) fn format_function_option(
187+
e: &mut EventEmitter,
188+
d: &pgls_query::protobuf::DefElem,
189+
hint: super::DollarQuoteHint,
190+
) {
180191
let defname_lower = d.defname.to_lowercase();
181192

182193
match defname_lower.as_str() {
@@ -190,12 +201,14 @@ pub(super) fn format_function_option(e: &mut EventEmitter, d: &pgls_query::proto
190201
if list.items.len() == 1 {
191202
// Single item: either library name (C) or SQL body (SQL/plpgsql)
192203
if let Some(pgls_query::NodeEnum::String(s)) = &list.items[0].node {
193-
super::emit_string_literal(e, s);
204+
// Use dollar quotes for function bodies (they handle embedded quotes better)
205+
super::emit_dollar_quoted_str_with_hint(e, &s.sval, hint);
194206
} else {
195207
super::emit_node(&list.items[0], e);
196208
}
197209
} else if list.items.len() == 2 {
198210
// Two items: library and symbol for C functions
211+
// C functions use regular string literals for library paths
199212
if let Some(pgls_query::NodeEnum::String(s)) = &list.items[0].node {
200213
super::emit_string_literal(e, s);
201214
} else {
@@ -222,7 +235,8 @@ pub(super) fn format_function_option(e: &mut EventEmitter, d: &pgls_query::proto
222235
e.space();
223236
if let Some(ref arg) = d.arg {
224237
if let Some(pgls_query::NodeEnum::String(s)) = &arg.node {
225-
super::emit_identifier(e, &s.sval);
238+
// Language names like 'sql', 'plpgsql', 'c' don't need quotes
239+
super::emit_identifier_maybe_quoted(e, &s.sval);
226240
} else {
227241
super::emit_node(arg, e);
228242
}

crates/pgls_pretty_print/src/nodes/do_stmt.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use crate::{
55
emitter::{EventEmitter, GroupKind, LineType},
66
};
77

8-
use super::string::{emit_dollar_quoted_str, emit_identifier_maybe_quoted, emit_keyword};
8+
use super::string::{
9+
DollarQuoteHint, emit_dollar_quoted_str_with_hint, emit_identifier_maybe_quoted, emit_keyword,
10+
};
911

1012
pub(super) fn emit_do_stmt(e: &mut EventEmitter, n: &DoStmt) {
1113
e.group_start(GroupKind::DoStmt);
@@ -71,12 +73,12 @@ pub(super) fn emit_do_stmt(e: &mut EventEmitter, n: &DoStmt) {
7173
}
7274
if let Some((code, _)) = &body {
7375
e.line(LineType::SoftOrSpace);
74-
emit_dollar_quoted_str(e, code);
76+
emit_dollar_quoted_str_with_hint(e, code, DollarQuoteHint::Do);
7577
}
7678
} else {
7779
if let Some((code, _)) = &body {
7880
e.line(LineType::SoftOrSpace);
79-
emit_dollar_quoted_str(e, code);
81+
emit_dollar_quoted_str_with_hint(e, code, DollarQuoteHint::Do);
8082
}
8183
if let Some((lang, _)) = &language {
8284
e.line(LineType::SoftOrSpace);

crates/pgls_pretty_print/src/nodes/drop_stmt.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub(super) fn emit_drop_stmt(e: &mut EventEmitter, n: &DropStmt) {
128128
fn emit_dot_separated_identifiers(e: &mut EventEmitter, items: &[pgls_query::protobuf::Node]) {
129129
emit_dot_separated_list_with(e, items, |item, e| {
130130
if let Some(pgls_query::NodeEnum::String(s)) = item.node.as_ref() {
131-
super::string::emit_identifier(e, &s.sval);
131+
super::string::emit_identifier_maybe_quoted(e, &s.sval);
132132
} else {
133133
super::emit_node(item, e);
134134
}
@@ -167,7 +167,7 @@ fn emit_drop_opclass_object(node: &pgls_query::protobuf::Node, e: &mut EventEmit
167167
e.token(TokenKind::IDENT("USING".to_string()));
168168
e.space();
169169
if let Some(pgls_query::NodeEnum::String(s)) = list.items[0].node.as_ref() {
170-
super::string::emit_identifier(e, &s.sval);
170+
super::string::emit_identifier_maybe_quoted(e, &s.sval);
171171
}
172172
return;
173173
}
@@ -190,7 +190,7 @@ fn emit_drop_on_object(node: &pgls_query::protobuf::Node, e: &mut EventEmitter)
190190

191191
// Emit object name first
192192
if let Some(pgls_query::NodeEnum::String(s)) = object_name.node.as_ref() {
193-
super::string::emit_identifier(e, &s.sval);
193+
super::string::emit_identifier_maybe_quoted(e, &s.sval);
194194
} else {
195195
super::emit_node(object_name, e);
196196
}

crates/pgls_pretty_print/src/nodes/inline_code_block.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ use pgls_query::protobuf::InlineCodeBlock;
22

33
use crate::emitter::{EventEmitter, GroupKind};
44

5+
use super::string::{DollarQuoteHint, emit_dollar_quoted_str_with_hint};
6+
57
pub(super) fn emit_inline_code_block(e: &mut EventEmitter, n: &InlineCodeBlock) {
68
e.group_start(GroupKind::InlineCodeBlock);
79

810
if !n.source_text.is_empty() {
9-
super::string::emit_dollar_quoted_str(e, &n.source_text);
11+
// InlineCodeBlock is typically used for anonymous code blocks (like DO)
12+
emit_dollar_quoted_str_with_hint(e, &n.source_text, DollarQuoteHint::Do);
1013
}
1114

1215
e.group_end();

crates/pgls_pretty_print/src/nodes/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,8 @@ use sort_group_clause::emit_sort_group_clause;
492492
use sql_value_function::emit_sql_value_function;
493493
use stats_elem::emit_stats_elem;
494494
use string::{
495-
emit_identifier, emit_identifier_maybe_quoted, emit_string, emit_string_identifier,
496-
emit_string_literal,
495+
DollarQuoteHint, emit_dollar_quoted_str_with_hint, emit_identifier,
496+
emit_identifier_maybe_quoted, emit_string, emit_string_identifier, emit_string_literal,
497497
};
498498
use sub_link::emit_sub_link;
499499
use sub_plan::{emit_alternative_sub_plan, emit_sub_plan};

crates/pgls_pretty_print/src/nodes/string.rs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use pgls_query::protobuf::String as PgString;
22

33
use crate::{
4-
TokenKind,
54
emitter::{EventEmitter, GroupKind},
5+
TokenKind,
66
};
77

88
const RESERVED_KEYWORDS: &[&str] = &[
@@ -142,8 +142,24 @@ pub(super) fn emit_single_quoted_str(e: &mut EventEmitter, value: &str) {
142142
e.token(TokenKind::STRING(format!("'{escaped}'")));
143143
}
144144

145-
pub(super) fn emit_dollar_quoted_str(e: &mut EventEmitter, value: &str) {
146-
let delimiter = pick_dollar_delimiter(value);
145+
/// Hint for what kind of dollar-quoted content we're emitting.
146+
/// Used to pick a more meaningful delimiter tag.
147+
#[derive(Debug, Clone, Copy)]
148+
pub enum DollarQuoteHint {
149+
/// Function body - prefer $function$ or $body$
150+
Function,
151+
/// Procedure body - prefer $procedure$ or $body$
152+
Procedure,
153+
/// DO block - prefer $do$ or $body$
154+
Do,
155+
}
156+
157+
pub(super) fn emit_dollar_quoted_str_with_hint(
158+
e: &mut EventEmitter,
159+
value: &str,
160+
hint: DollarQuoteHint,
161+
) {
162+
let delimiter = pick_dollar_delimiter(value, hint);
147163
e.token(TokenKind::DOLLAR_QUOTED_STRING(format!(
148164
"{delimiter}{value}{delimiter}"
149165
)));
@@ -179,23 +195,56 @@ fn needs_quoting(value: &str) -> bool {
179195
RESERVED_KEYWORDS.binary_search(&lower.as_str()).is_ok()
180196
}
181197

182-
fn pick_dollar_delimiter(body: &str) -> String {
183-
if !body.contains("$$") {
184-
return "$$".to_string();
198+
/// Pick a dollar quote delimiter that doesn't conflict with the body content.
199+
/// Uses the hint to prefer context-appropriate tags.
200+
fn pick_dollar_delimiter(body: &str, hint: DollarQuoteHint) -> String {
201+
// Order tags based on the hint - put the most appropriate ones first
202+
let preferred_tags: &[&str] = match hint {
203+
DollarQuoteHint::Function => &[
204+
"$function$",
205+
"$body$",
206+
"$$",
207+
"$procedure$",
208+
"$do$",
209+
"$sql$",
210+
"$code$",
211+
"$_$",
212+
],
213+
DollarQuoteHint::Procedure => &[
214+
"$procedure$",
215+
"$body$",
216+
"$$",
217+
"$function$",
218+
"$do$",
219+
"$sql$",
220+
"$code$",
221+
"$_$",
222+
],
223+
DollarQuoteHint::Do => &[
224+
"$do$",
225+
"$body$",
226+
"$$",
227+
"$function$",
228+
"$procedure$",
229+
"$sql$",
230+
"$code$",
231+
"$_$",
232+
],
233+
};
234+
235+
for tag in preferred_tags {
236+
if !body.contains(tag) {
237+
return (*tag).to_string();
238+
}
185239
}
186240

241+
// Fall back to numbered tags if all preferred ones conflict
187242
let mut counter = 0usize;
188243
loop {
189-
let tag = if counter == 0 {
190-
"$pg$".to_string()
191-
} else {
192-
format!("$pg{counter}$")
193-
};
194-
244+
let tag = format!("$f{counter}$");
195245
if !body.contains(&tag) {
196246
return tag;
197247
}
198-
199248
counter += 1;
200249
}
201250
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE FUNCTION test_func() RETURNS void LANGUAGE plpgsql AS $$
2+
BEGIN
3+
RAISE NOTICE 'Hello, world!';
4+
END;
5+
$$;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE my_table;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE "MyTable";

0 commit comments

Comments
 (0)