@@ -68,6 +68,20 @@ impl ToHoverMarkdown for Table {
6868 write ! ( writer, "typeid:{}" , column. type_id) ?;
6969 }
7070
71+ if column. is_nullable {
72+ write ! ( writer, " - nullable" ) ?;
73+ } else {
74+ write ! ( writer, " - not null" ) ?;
75+ }
76+
77+ if let Some ( default_expr) = column
78+ . default_expr
79+ . as_deref ( )
80+ . and_then ( extract_basic_default_literal)
81+ {
82+ write ! ( writer, " - default: {default_expr}" ) ?;
83+ }
84+
7185 writeln ! ( writer) ?;
7286 }
7387
@@ -99,6 +113,258 @@ impl ToHoverMarkdown for Table {
99113 }
100114}
101115
116+ fn extract_basic_default_literal ( default_expr : & str ) -> Option < String > {
117+ let mut candidate = default_expr. trim ( ) ;
118+
119+ loop {
120+ let mut changed = false ;
121+
122+ if let Some ( unwrapped) = strip_outer_parentheses ( candidate) {
123+ candidate = unwrapped. trim ( ) ;
124+ changed = true ;
125+ }
126+
127+ if let Some ( without_cast) = strip_trailing_top_level_casts ( candidate) {
128+ candidate = without_cast. trim ( ) ;
129+ changed = true ;
130+ }
131+
132+ if !changed {
133+ break ;
134+ }
135+ }
136+
137+ if is_basic_literal ( candidate) {
138+ Some ( candidate. to_string ( ) )
139+ } else {
140+ None
141+ }
142+ }
143+
144+ fn strip_outer_parentheses ( value : & str ) -> Option < & str > {
145+ let value = value. trim ( ) ;
146+
147+ if !value. starts_with ( '(' ) || !value. ends_with ( ')' ) {
148+ return None ;
149+ }
150+
151+ let mut depth = 0_i32 ;
152+ let mut in_single_quote = false ;
153+ let mut in_double_quote = false ;
154+ let bytes = value. as_bytes ( ) ;
155+ let mut idx = 0_usize ;
156+
157+ while idx < bytes. len ( ) {
158+ let ch = bytes[ idx] as char ;
159+
160+ if in_single_quote {
161+ if ch == '\'' {
162+ if idx + 1 < bytes. len ( ) && bytes[ idx + 1 ] as char == '\'' {
163+ idx += 2 ;
164+ continue ;
165+ }
166+
167+ in_single_quote = false ;
168+ }
169+
170+ idx += 1 ;
171+ continue ;
172+ }
173+
174+ if in_double_quote {
175+ if ch == '"' {
176+ in_double_quote = false ;
177+ }
178+
179+ idx += 1 ;
180+ continue ;
181+ }
182+
183+ match ch {
184+ '\'' => in_single_quote = true ,
185+ '"' => in_double_quote = true ,
186+ '(' => depth += 1 ,
187+ ')' => {
188+ depth -= 1 ;
189+ if depth == 0 && idx != bytes. len ( ) - 1 {
190+ return None ;
191+ }
192+ }
193+ _ => { }
194+ }
195+
196+ idx += 1 ;
197+ }
198+
199+ if depth != 0 || in_single_quote || in_double_quote {
200+ return None ;
201+ }
202+
203+ Some ( & value[ 1 ..value. len ( ) - 1 ] )
204+ }
205+
206+ fn strip_trailing_top_level_casts ( value : & str ) -> Option < & str > {
207+ let bytes = value. as_bytes ( ) ;
208+ let mut depth = 0_i32 ;
209+ let mut in_single_quote = false ;
210+ let mut in_double_quote = false ;
211+ let mut idx = 0_usize ;
212+
213+ while idx + 1 < bytes. len ( ) {
214+ let ch = bytes[ idx] as char ;
215+
216+ if in_single_quote {
217+ if ch == '\'' {
218+ if idx + 1 < bytes. len ( ) && bytes[ idx + 1 ] as char == '\'' {
219+ idx += 2 ;
220+ continue ;
221+ }
222+ in_single_quote = false ;
223+ }
224+ idx += 1 ;
225+ continue ;
226+ }
227+
228+ if in_double_quote {
229+ if ch == '"' {
230+ in_double_quote = false ;
231+ }
232+ idx += 1 ;
233+ continue ;
234+ }
235+
236+ match ch {
237+ '\'' => {
238+ in_single_quote = true ;
239+ idx += 1 ;
240+ continue ;
241+ }
242+ '"' => {
243+ in_double_quote = true ;
244+ idx += 1 ;
245+ continue ;
246+ }
247+ '(' => {
248+ depth += 1 ;
249+ idx += 1 ;
250+ continue ;
251+ }
252+ ')' => {
253+ depth -= 1 ;
254+ idx += 1 ;
255+ continue ;
256+ }
257+ ':' if depth == 0 && bytes[ idx + 1 ] as char == ':' => {
258+ let suffix = & value[ idx..] ;
259+ if suffix
260+ . chars ( )
261+ . all ( |c| c. is_ascii_alphanumeric ( ) || ":_\" .()[] ,\t " . contains ( c) )
262+ {
263+ return Some ( value[ ..idx] . trim_end ( ) ) ;
264+ }
265+ return None ;
266+ }
267+ _ => {
268+ idx += 1 ;
269+ continue ;
270+ }
271+ }
272+ }
273+
274+ None
275+ }
276+
277+ fn is_basic_literal ( value : & str ) -> bool {
278+ let value = value. trim ( ) ;
279+
280+ if value. eq_ignore_ascii_case ( "true" )
281+ || value. eq_ignore_ascii_case ( "false" )
282+ || value. eq_ignore_ascii_case ( "null" )
283+ {
284+ return true ;
285+ }
286+
287+ is_numeric_literal ( value) || is_single_quoted_literal ( value)
288+ }
289+
290+ fn is_single_quoted_literal ( value : & str ) -> bool {
291+ let bytes = value. as_bytes ( ) ;
292+
293+ if bytes. len ( ) < 2 || bytes. first ( ) != Some ( & b'\'' ) || bytes. last ( ) != Some ( & b'\'' ) {
294+ return false ;
295+ }
296+
297+ let mut idx = 1_usize ;
298+ let end = bytes. len ( ) - 1 ;
299+
300+ while idx < end {
301+ if bytes[ idx] == b'\'' {
302+ if idx + 1 < end && bytes[ idx + 1 ] == b'\'' {
303+ idx += 2 ;
304+ } else {
305+ return false ;
306+ }
307+ } else {
308+ idx += 1 ;
309+ }
310+ }
311+
312+ true
313+ }
314+
315+ fn is_numeric_literal ( value : & str ) -> bool {
316+ let bytes = value. as_bytes ( ) ;
317+ if bytes. is_empty ( ) {
318+ return false ;
319+ }
320+
321+ let mut idx = 0_usize ;
322+
323+ if matches ! ( bytes[ idx] , b'+' | b'-' ) {
324+ idx += 1 ;
325+ if idx >= bytes. len ( ) {
326+ return false ;
327+ }
328+ }
329+
330+ let integer_start = idx;
331+ while idx < bytes. len ( ) && bytes[ idx] . is_ascii_digit ( ) {
332+ idx += 1 ;
333+ }
334+ let has_integer_digits = idx > integer_start;
335+
336+ if idx < bytes. len ( ) && bytes[ idx] == b'.' {
337+ idx += 1 ;
338+ let fractional_start = idx;
339+ while idx < bytes. len ( ) && bytes[ idx] . is_ascii_digit ( ) {
340+ idx += 1 ;
341+ }
342+ if !has_integer_digits && idx == fractional_start {
343+ return false ;
344+ }
345+ } else if !has_integer_digits {
346+ return false ;
347+ }
348+
349+ if idx < bytes. len ( ) && matches ! ( bytes[ idx] , b'e' | b'E' ) {
350+ idx += 1 ;
351+ if idx < bytes. len ( ) && matches ! ( bytes[ idx] , b'+' | b'-' ) {
352+ idx += 1 ;
353+ }
354+
355+ let exponent_start = idx;
356+ while idx < bytes. len ( ) && bytes[ idx] . is_ascii_digit ( ) {
357+ idx += 1 ;
358+ }
359+
360+ if idx == exponent_start {
361+ return false ;
362+ }
363+ }
364+
365+ idx == bytes. len ( )
366+ }
367+
102368impl ContextualPriority for Table {
103369 fn relevance_score ( & self , ctx : & TreesitterContext ) -> f32 {
104370 let mut score = 0.0 ;
@@ -127,3 +393,38 @@ impl ContextualPriority for Table {
127393 score
128394 }
129395}
396+
397+ #[ cfg( test) ]
398+ mod tests {
399+ use super :: extract_basic_default_literal;
400+
401+ #[ test]
402+ fn extracts_basic_defaults_with_optional_casts ( ) {
403+ assert_eq ! (
404+ extract_basic_default_literal( "'anonymous'::text" ) ,
405+ Some ( "'anonymous'" . to_string( ) )
406+ ) ;
407+ assert_eq ! ( extract_basic_default_literal( "(42)::int8" ) , Some ( "42" . to_string( ) ) ) ;
408+ assert_eq ! (
409+ extract_basic_default_literal( "NULL::character varying" ) ,
410+ Some ( "NULL" . to_string( ) )
411+ ) ;
412+ assert_eq ! (
413+ extract_basic_default_literal( "false::boolean" ) ,
414+ Some ( "false" . to_string( ) )
415+ ) ;
416+ }
417+
418+ #[ test]
419+ fn ignores_non_basic_defaults ( ) {
420+ assert_eq ! (
421+ extract_basic_default_literal( "nextval('users_id_seq'::regclass)" ) ,
422+ None
423+ ) ;
424+ assert_eq ! ( extract_basic_default_literal( "now()" ) , None ) ;
425+ assert_eq ! (
426+ extract_basic_default_literal( "'a'::text || 'b'::text" ) ,
427+ None
428+ ) ;
429+ }
430+ }
0 commit comments