11use std:: io;
22
3+ use once_cell:: sync:: Lazy ;
34use pgt_console:: markup;
45use pgt_diagnostics:: { Advices , Diagnostic , LogCategory , MessageAndDescription , Severity , Visit } ;
56use pgt_text_size:: { TextRange , TextSize } ;
7+ use regex:: Regex ;
68use sqlx:: postgres:: { PgDatabaseError , PgSeverity } ;
79
8- use crate :: { IdentifierType , TypedReplacement } ;
10+ use crate :: typed_identifier :: { IdentifierReplacement , TypedReplacement } ;
911
1012/// A specialized diagnostic for the typechecker.
1113///
@@ -96,36 +98,65 @@ impl Advices for TypecheckAdvices {
9698 }
9799}
98100
99- /// Finds the original type at the given position in the adjusted text
100- fn find_type_at_position (
101- adjusted_position : TextSize ,
102- type_info : & [ ( TextRange , IdentifierType ) ] ,
103- ) -> Option < & IdentifierType > {
104- type_info
105- . iter ( )
106- . find ( |( range, _) | range. contains ( adjusted_position) )
107- . map ( |( _, type_) | type_)
101+ /// Pattern and rewrite rule for error messages
102+ struct ErrorRewriteRule {
103+ pattern : Regex ,
104+ rewrite : fn ( & regex:: Captures , & IdentifierReplacement ) -> String ,
108105}
109106
110- /// Rewrites error messages to show the original type name instead of the replaced literal value
111- fn rewrite_error_message ( original_message : & str , identifier_type : & IdentifierType ) -> String {
112- // pattern: invalid input syntax for type X: "literal_value"
113- // we want to replace "literal_value" with the type name
114-
115- if let Some ( colon_pos) = original_message. rfind ( ": " ) {
116- let before_value = & original_message[ ..colon_pos] ;
117-
118- // build the type name, including schema if present
119- let type_name = if let Some ( schema) = & identifier_type. schema {
120- format ! ( "{}.{}" , schema, identifier_type. name)
121- } else {
122- identifier_type. name . clone ( )
123- } ;
107+ static ERROR_RULES : Lazy < Vec < ErrorRewriteRule > > = Lazy :: new ( || {
108+ vec ! [
109+ ErrorRewriteRule {
110+ pattern: Regex :: new( r#"invalid input syntax for type (\w+): "([^"]*)""# ) . unwrap( ) ,
111+ rewrite: |caps, replacement| {
112+ let expected_type = & caps[ 1 ] ;
113+ format!(
114+ "`{}` is of type {}, not {}" ,
115+ replacement. original_name, replacement. type_name, expected_type
116+ )
117+ } ,
118+ } ,
119+ ErrorRewriteRule {
120+ pattern: Regex :: new(
121+ r#"column "([^"]*)" is of type (\w+) but expression is of type (\w+)"# ,
122+ )
123+ . unwrap( ) ,
124+ rewrite: |caps, replacement| {
125+ let column = & caps[ 1 ] ;
126+ let expected_type = & caps[ 2 ] ;
127+ format!(
128+ "column `{}` expects {}, but `{}` is of type {}" ,
129+ column, expected_type, replacement. original_name, replacement. type_name
130+ )
131+ } ,
132+ } ,
133+ ErrorRewriteRule {
134+ pattern: Regex :: new( r#"operator does not exist: (.+)"# ) . unwrap( ) ,
135+ rewrite: |caps, replacement| {
136+ let operator_expr = & caps[ 1 ] ;
137+ format!(
138+ "operator does not exist: {} (parameter `{}` is of type {})" ,
139+ operator_expr, replacement. original_name, replacement. type_name
140+ )
141+ } ,
142+ } ,
143+ ]
144+ } ) ;
124145
125- format ! ( "{}: {}" , before_value, type_name)
126- } else {
127- original_message. to_string ( )
146+ /// Rewrites Postgres error messages to be more user-friendly
147+ fn rewrite_error_message ( pg_error_message : & str , replacement : & IdentifierReplacement ) -> String {
148+ // try each rule
149+ for rule in ERROR_RULES . iter ( ) {
150+ if let Some ( caps) = rule. pattern . captures ( pg_error_message) {
151+ return ( rule. rewrite ) ( & caps, replacement) ;
152+ }
128153 }
154+
155+ // fallback: generic value replacement
156+ let unquoted_default = replacement. default_value . trim_matches ( '\'' ) ;
157+ pg_error_message
158+ . replace ( & format ! ( "\" {}\" " , unquoted_default) , & replacement. type_name )
159+ . replace ( & format ! ( "'{}'" , unquoted_default) , & replacement. type_name )
129160}
130161
131162pub ( crate ) fn create_type_error (
@@ -138,11 +169,17 @@ pub(crate) fn create_type_error(
138169 _ => None ,
139170 } ) ;
140171
141- let range = position. and_then ( |pos| {
142- let adjusted = typed_replacement. replacement . to_original_position ( TextSize :: new ( pos. try_into ( ) . unwrap ( ) ) ) ;
172+ let original_position = position. map ( |p| {
173+ let pos = TextSize :: new ( p. try_into ( ) . unwrap ( ) ) ;
174+
175+ typed_replacement
176+ . text_replacement ( )
177+ . to_original_position ( pos)
178+ } ) ;
143179
180+ let range = original_position. and_then ( |pos| {
144181 ts. root_node ( )
145- . named_descendant_for_byte_range ( adjusted . into ( ) , adjusted . into ( ) )
182+ . named_descendant_for_byte_range ( pos . into ( ) , pos . into ( ) )
146183 . map ( |node| {
147184 TextRange :: new (
148185 node. start_byte ( ) . try_into ( ) . unwrap ( ) ,
@@ -162,11 +199,9 @@ pub(crate) fn create_type_error(
162199 PgSeverity :: Log => Severity :: Information ,
163200 } ;
164201
165- // check if the error position corresponds to a replaced parameter
166- let message = if let Some ( pos) = position {
167- let adjusted_pos = TextSize :: new ( pos. try_into ( ) . unwrap ( ) ) ;
168- if let Some ( original_type) = find_type_at_position ( adjusted_pos, & typed_replacement. type_info ) {
169- rewrite_error_message ( pg_err. message ( ) , original_type)
202+ let message = if let Some ( pos) = original_position {
203+ if let Some ( replacement) = typed_replacement. find_type_at_position ( pos) {
204+ rewrite_error_message ( pg_err. message ( ) , replacement)
170205 } else {
171206 pg_err. to_string ( )
172207 }
0 commit comments