ExplainVisualizer.utils.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {
  2. Activity,
  3. Database,
  4. GitMerge,
  5. Hash,
  6. Layers,
  7. ListFilter,
  8. SortAsc,
  9. Zap,
  10. type LucideIcon,
  11. } from 'lucide-react'
  12. // Get human-readable description for an operation
  13. export function getOperationDescription(operation: string): string {
  14. const op = operation.toLowerCase()
  15. if (op.includes('seq scan')) {
  16. return 'Reads entire table row by row'
  17. }
  18. if (op.includes('index only scan')) {
  19. return 'Reads data directly from index (fastest)'
  20. }
  21. if (op.includes('bitmap index scan')) {
  22. return 'Builds bitmap of matching rows from index'
  23. }
  24. if (op.includes('bitmap heap scan')) {
  25. return 'Fetches rows using bitmap'
  26. }
  27. if (op.includes('index scan')) {
  28. return 'Uses index to find matching rows'
  29. }
  30. if (op.includes('hash left join')) {
  31. return 'Returns all left rows with matching right rows via hash'
  32. }
  33. if (op.includes('hash right join')) {
  34. return 'Returns all right rows with matching left rows via hash'
  35. }
  36. if (op.includes('hash full join')) {
  37. return 'Returns all rows from both tables via hash'
  38. }
  39. if (op.includes('hash anti join')) {
  40. return 'Returns rows without matches via hash'
  41. }
  42. if (op.includes('hash semi join')) {
  43. return 'Returns rows with at least one match via hash'
  44. }
  45. if (op.includes('hash join')) {
  46. return 'Joins tables using hash lookup'
  47. }
  48. if (op.includes('merge left join')) {
  49. return 'Returns all left rows with matching right rows via merge'
  50. }
  51. if (op.includes('merge right join')) {
  52. return 'Returns all right rows with matching left rows via merge'
  53. }
  54. if (op.includes('merge full join')) {
  55. return 'Returns all rows from both tables via merge'
  56. }
  57. if (op.includes('merge anti join')) {
  58. return 'Returns rows without matches via merge'
  59. }
  60. if (op.includes('merge semi join')) {
  61. return 'Returns rows with at least one match via merge'
  62. }
  63. if (op.includes('merge join')) {
  64. return 'Joins pre-sorted tables'
  65. }
  66. if (op.includes('nested loop left join')) {
  67. return 'Returns all left rows with matching right rows via loop'
  68. }
  69. if (op.includes('nested loop anti join')) {
  70. return 'Returns rows without matches via loop'
  71. }
  72. if (op.includes('nested loop semi join')) {
  73. return 'Returns rows with at least one match via loop'
  74. }
  75. if (op.includes('nested loop')) {
  76. return 'Joins by looping through each row'
  77. }
  78. if (op === 'hash') {
  79. return 'Builds hash table for fast lookups'
  80. }
  81. if (op.includes('sort')) {
  82. return 'Sorts rows for output or join'
  83. }
  84. if (op.includes('aggregate') || op.includes('group')) {
  85. return 'Groups rows and calculates aggregates'
  86. }
  87. if (op.includes('limit')) {
  88. return 'Returns only first N rows'
  89. }
  90. if (op.includes('materialize')) {
  91. return 'Stores results in memory for reuse'
  92. }
  93. if (op.includes('gather')) {
  94. return 'Collects results from parallel workers'
  95. }
  96. return ''
  97. }
  98. // Get an icon for the operation type
  99. export function getOperationIcon(operation: string): LucideIcon {
  100. const op = operation.toLowerCase()
  101. if (op === 'hash') return Hash
  102. if (op.includes('hash join')) return GitMerge
  103. if (op.includes('merge join')) return GitMerge
  104. if (op.includes('nested loop')) return GitMerge
  105. if (op.includes('join')) return Layers
  106. if (op.includes('index')) return Zap
  107. if (op.includes('seq scan')) return Database
  108. if (op.includes('scan')) return Database
  109. if (op.includes('filter')) return ListFilter
  110. if (op.includes('sort')) return SortAsc
  111. if (op.includes('aggregate') || op.includes('group')) return Activity
  112. return Database
  113. }
  114. // Get a color class for the operation type
  115. export function getOperationColor(operation: string): string {
  116. const op = operation.toLowerCase()
  117. if (op.includes('seq scan')) return 'text-warning'
  118. if (op.includes('index')) return 'text-brand'
  119. if (op.includes('join')) return 'text-foreground-light'
  120. if (op.includes('sort') || op.includes('aggregate')) return 'text-foreground-light'
  121. return 'text-foreground-light'
  122. }
  123. export function isExplainQuery(rows: readonly unknown[]): boolean {
  124. if (rows.length === 0) return false
  125. const firstRow = rows[0]
  126. if (typeof firstRow !== 'object' || firstRow === null) return false
  127. return 'QUERY PLAN' in firstRow && Object.keys(firstRow).length === 1
  128. }
  129. export function isTextFormatExplain(rows: readonly unknown[]): boolean {
  130. if (!isExplainQuery(rows)) return false
  131. const firstRow = rows[0] as Record<string, unknown>
  132. return typeof firstRow['QUERY PLAN'] === 'string'
  133. }
  134. export function isExplainSql(sql: string): boolean {
  135. return /^\s*explain\b/i.test(sql)
  136. }
  137. export function formatNodeDuration(ms: number | undefined): string {
  138. if (ms === undefined) return '-'
  139. if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`
  140. if (ms >= 1) return `${ms.toFixed(2)}ms`
  141. if (ms >= 0.01) return `${ms.toFixed(2)}ms`
  142. if (ms >= 0.001) return `${ms.toFixed(3)}ms`
  143. const us = ms * 1000
  144. if (us >= 0.1) return `${us.toFixed(1)}µs`
  145. return `${us.toFixed(2)}µs`
  146. }
  147. export function getScanBarColor(operation: string): string {
  148. const op = operation.toLowerCase()
  149. // Index scans are green
  150. if (
  151. op.includes('index scan') ||
  152. op.includes('index only scan') ||
  153. op.includes('bitmap index scan')
  154. ) {
  155. return 'bg-brand/20'
  156. }
  157. // Sequential scans are yellow
  158. if (op.includes('seq scan') || op.includes('sequential scan')) {
  159. return 'bg-warning/20'
  160. }
  161. // Default neutral color for other operations
  162. return 'bg-foreground/6'
  163. }
  164. export function getScanBorderColor(operation: string): string {
  165. const op = operation.toLowerCase()
  166. // Index scans are green
  167. if (
  168. op.includes('index scan') ||
  169. op.includes('index only scan') ||
  170. op.includes('bitmap index scan')
  171. ) {
  172. return 'border-l-brand'
  173. }
  174. // Sequential scans are yellow
  175. if (op.includes('seq scan') || op.includes('sequential scan')) {
  176. return 'border-l-warning'
  177. }
  178. // Default neutral color for other operations
  179. return 'border-l-border-muted'
  180. }
  181. export function splitSqlStatements(sql: string): string[] {
  182. // Enhanced tokenizer that handles:
  183. // - Single-quoted strings: '...' (with '' escaping)
  184. // - Double-quoted strings: "..." (with "" escaping)
  185. // - Dollar-quoted strings: $tag$...$tag$
  186. // - Line comments: -- (until end of line)
  187. // - Block comments: /* ... */ (may be multiline)
  188. // - Semicolons: ;
  189. const tokens =
  190. sql.match(
  191. /'([^']|'')*'|"([^"]|"")*"|\$[a-zA-Z0-9_]*\$[\s\S]*?\$[a-zA-Z0-9_]*\$|--[^\r\n]*|\/\*[\s\S]*?\*\/|;|[^'"$;\-\/]+|./g
  192. ) || []
  193. const statements: string[] = []
  194. let current = ''
  195. for (const token of tokens) {
  196. if (token === ';') {
  197. if (current.trim()) statements.push(current.trim())
  198. current = ''
  199. } else {
  200. current += token
  201. }
  202. }
  203. if (current.trim()) {
  204. statements.push(current.trim())
  205. }
  206. return statements
  207. }