@neverinfamous/postgres-mcp 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +68 -62
  2. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  3. package/dist/adapters/postgresql/PostgresAdapter.js +53 -3
  4. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  5. package/dist/adapters/postgresql/prompts/ltree.js +2 -2
  6. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  7. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  8. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  9. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  10. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  11. package/dist/adapters/postgresql/schemas/backup.d.ts +8 -4
  12. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  13. package/dist/adapters/postgresql/schemas/backup.js +11 -4
  14. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  15. package/dist/adapters/postgresql/schemas/core.d.ts +53 -19
  16. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  17. package/dist/adapters/postgresql/schemas/core.js +61 -17
  18. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  19. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  20. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  21. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  22. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  23. package/dist/adapters/postgresql/schemas/extensions.d.ts +168 -73
  24. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  25. package/dist/adapters/postgresql/schemas/extensions.js +177 -60
  26. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  27. package/dist/adapters/postgresql/schemas/index.d.ts +5 -5
  28. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  29. package/dist/adapters/postgresql/schemas/index.js +9 -7
  30. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  31. package/dist/adapters/postgresql/schemas/jsonb.d.ts +94 -42
  32. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  33. package/dist/adapters/postgresql/schemas/jsonb.js +101 -30
  34. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  35. package/dist/adapters/postgresql/schemas/monitoring.d.ts +28 -11
  36. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  37. package/dist/adapters/postgresql/schemas/monitoring.js +49 -24
  38. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  39. package/dist/adapters/postgresql/schemas/partitioning.d.ts +5 -4
  40. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  41. package/dist/adapters/postgresql/schemas/partitioning.js +5 -4
  42. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  43. package/dist/adapters/postgresql/schemas/performance.d.ts +60 -30
  44. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  45. package/dist/adapters/postgresql/schemas/performance.js +85 -22
  46. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  47. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  48. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  49. package/dist/adapters/postgresql/schemas/postgis.js +20 -0
  50. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  51. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +35 -23
  52. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  53. package/dist/adapters/postgresql/schemas/schema-mgmt.js +69 -26
  54. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  55. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  56. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  57. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  58. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  59. package/dist/adapters/postgresql/schemas/text-search.d.ts +8 -5
  60. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  61. package/dist/adapters/postgresql/schemas/text-search.js +11 -4
  62. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  63. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  64. package/dist/adapters/postgresql/tools/admin.js +211 -140
  65. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  66. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  67. package/dist/adapters/postgresql/tools/backup/dump.js +360 -337
  68. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  69. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  70. package/dist/adapters/postgresql/tools/citext.js +221 -163
  71. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  72. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  73. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  74. package/dist/adapters/postgresql/tools/core/convenience.js +78 -11
  75. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  76. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  77. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  78. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  79. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  80. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  81. package/dist/adapters/postgresql/tools/core/health.js +18 -4
  82. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  83. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  84. package/dist/adapters/postgresql/tools/core/indexes.js +45 -4
  85. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  86. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  87. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  88. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  89. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  90. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  91. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  92. package/dist/adapters/postgresql/tools/core/schemas.d.ts +51 -25
  93. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  94. package/dist/adapters/postgresql/tools/core/schemas.js +51 -25
  95. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  96. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  97. package/dist/adapters/postgresql/tools/core/tables.js +68 -28
  98. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  99. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  100. package/dist/adapters/postgresql/tools/cron.js +274 -179
  101. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  102. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  103. package/dist/adapters/postgresql/tools/jsonb/advanced.js +372 -284
  104. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  105. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  106. package/dist/adapters/postgresql/tools/jsonb/basic.js +617 -398
  107. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  108. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  109. package/dist/adapters/postgresql/tools/kcache.js +278 -246
  110. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  111. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  112. package/dist/adapters/postgresql/tools/ltree.js +121 -35
  113. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  114. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  115. package/dist/adapters/postgresql/tools/monitoring.js +54 -34
  116. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  117. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  118. package/dist/adapters/postgresql/tools/partitioning.js +79 -15
  119. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  120. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  121. package/dist/adapters/postgresql/tools/partman/management.js +11 -4
  122. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  123. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  124. package/dist/adapters/postgresql/tools/partman/operations.js +132 -19
  125. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  126. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  127. package/dist/adapters/postgresql/tools/performance/analysis.js +264 -160
  128. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  129. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  130. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  131. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  132. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  133. package/dist/adapters/postgresql/tools/performance/monitoring.js +44 -7
  134. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  135. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  136. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  137. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  138. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  139. package/dist/adapters/postgresql/tools/performance/stats.js +124 -36
  140. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  141. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  142. package/dist/adapters/postgresql/tools/pgcrypto.js +244 -89
  143. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  144. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  145. package/dist/adapters/postgresql/tools/postgis/advanced.js +285 -222
  146. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  147. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  148. package/dist/adapters/postgresql/tools/postgis/basic.js +359 -249
  149. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  150. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  151. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  152. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  153. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  154. package/dist/adapters/postgresql/tools/schema.js +504 -231
  155. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  156. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  157. package/dist/adapters/postgresql/tools/stats/advanced.js +515 -476
  158. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  159. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  160. package/dist/adapters/postgresql/tools/stats/basic.js +302 -293
  161. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  162. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  163. package/dist/adapters/postgresql/tools/text.js +392 -218
  164. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  165. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  166. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  167. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  168. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  169. package/dist/adapters/postgresql/tools/vector/advanced.js +18 -0
  170. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  171. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  172. package/dist/adapters/postgresql/tools/vector/basic.js +100 -53
  173. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  174. package/dist/codemode/api.js +1 -1
  175. package/dist/codemode/api.js.map +1 -1
  176. package/dist/constants/ServerInstructions.d.ts +1 -1
  177. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  178. package/dist/constants/ServerInstructions.js +45 -7
  179. package/dist/constants/ServerInstructions.js.map +1 -1
  180. package/package.json +9 -5
@@ -4,9 +4,10 @@
4
4
  * Text processing, FTS, trigrams, and fuzzy matching.
5
5
  * 14 tools total.
6
6
  */
7
- import { z } from "zod";
7
+ import { z, ZodError } from "zod";
8
8
  import { readOnly, write } from "../../../utils/annotations.js";
9
9
  import { getToolIcons } from "../../../utils/icons.js";
10
+ import { formatPostgresError } from "./core/error-helpers.js";
10
11
  import { sanitizeIdentifier, sanitizeIdentifiers, sanitizeTableName, } from "../../../utils/identifiers.js";
11
12
  import { sanitizeFtsConfig } from "../../../utils/fts-config.js";
12
13
  import { sanitizeWhereClause } from "../../../utils/where-clause.js";
@@ -43,43 +44,65 @@ function createTextSearchTool(adapter) {
43
44
  annotations: readOnly("Full-Text Search"),
44
45
  icons: getToolIcons("text", readOnly("Full-Text Search")),
45
46
  handler: async (params, _context) => {
46
- const parsed = TextSearchSchema.parse(params);
47
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
48
- // Handle both column (string) and columns (array) parameters
49
- // The preprocessor converts column columns, but we handle both for safety
50
- let cols;
51
- if (parsed.columns !== undefined && parsed.columns.length > 0) {
52
- cols = parsed.columns;
53
- }
54
- else if (parsed.column !== undefined) {
55
- cols = [parsed.column];
56
- }
57
- else {
58
- throw new Error("Either 'columns' (array) or 'column' (string) is required");
59
- }
60
- // Build qualified table name with schema support
61
- // The preprocessor guarantees table is set (converts tableName → table)
62
- const resolvedTable = parsed.table ?? parsed.tableName;
63
- if (!resolvedTable) {
64
- throw new Error("Either 'table' or 'tableName' is required");
65
- }
66
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
67
- const sanitizedCols = sanitizeIdentifiers(cols);
68
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
69
- ? sanitizeIdentifiers(parsed.select).join(", ")
70
- : "*";
71
- const tsvector = sanitizedCols
72
- .map((c) => `coalesce(${c}, '')`)
73
- .join(" || ' ' || ");
74
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
75
- ? ` LIMIT ${String(parsed.limit)}`
76
- : "";
77
- const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1)) as rank
47
+ try {
48
+ const parsed = TextSearchSchema.parse(params);
49
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
50
+ // Handle both column (string) and columns (array) parameters
51
+ // The preprocessor converts column → columns, but we handle both for safety
52
+ let cols;
53
+ if (parsed.columns !== undefined && parsed.columns.length > 0) {
54
+ cols = parsed.columns;
55
+ }
56
+ else if (parsed.column !== undefined) {
57
+ cols = [parsed.column];
58
+ }
59
+ else {
60
+ return {
61
+ success: false,
62
+ error: "Either 'columns' (array) or 'column' (string) is required",
63
+ };
64
+ }
65
+ // Build qualified table name with schema support
66
+ // The preprocessor guarantees table is set (converts tableName → table)
67
+ const resolvedTable = parsed.table ?? parsed.tableName;
68
+ if (!resolvedTable) {
69
+ return {
70
+ success: false,
71
+ error: "Either 'table' or 'tableName' is required",
72
+ };
73
+ }
74
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
75
+ const sanitizedCols = sanitizeIdentifiers(cols);
76
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
77
+ ? sanitizeIdentifiers(parsed.select).join(", ")
78
+ : "*";
79
+ const tsvector = sanitizedCols
80
+ .map((c) => `coalesce(${c}, '')`)
81
+ .join(" || ' ' || ");
82
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
83
+ ? ` LIMIT ${String(parsed.limit)}`
84
+ : "";
85
+ const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1)) as rank
78
86
  FROM ${tableName}
79
87
  WHERE to_tsvector('${cfg}', ${tsvector}) @@ plainto_tsquery('${cfg}', $1)
80
88
  ORDER BY rank DESC${limitClause}`;
81
- const result = await adapter.executeQuery(sql, [parsed.query]);
82
- return { rows: result.rows, count: result.rows?.length ?? 0 };
89
+ const result = await adapter.executeQuery(sql, [parsed.query]);
90
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
91
+ }
92
+ catch (error) {
93
+ if (error instanceof ZodError) {
94
+ return {
95
+ success: false,
96
+ error: `pg_text_search validation error: ${error.issues.map((e) => e.message).join(", ")}`,
97
+ };
98
+ }
99
+ return {
100
+ success: false,
101
+ error: formatPostgresError(error, {
102
+ tool: "pg_text_search",
103
+ }),
104
+ };
105
+ }
83
106
  },
84
107
  };
85
108
  }
@@ -115,42 +138,64 @@ function createTextRankTool(adapter) {
115
138
  annotations: readOnly("Text Rank"),
116
139
  icons: getToolIcons("text", readOnly("Text Rank")),
117
140
  handler: async (params, _context) => {
118
- const parsed = TextRankSchema.parse(params);
119
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
120
- const norm = parsed.normalization ?? 0;
121
- // Handle both column (string) and columns (array) parameters
122
- let cols;
123
- if (parsed.columns !== undefined && parsed.columns.length > 0) {
124
- cols = parsed.columns;
125
- }
126
- else if (parsed.column !== undefined) {
127
- cols = [parsed.column];
128
- }
129
- else {
130
- throw new Error("Either column or columns parameter is required");
131
- }
132
- // The preprocessor guarantees table is set (converts tableName → table)
133
- const resolvedTable = parsed.table ?? parsed.tableName;
134
- if (!resolvedTable) {
135
- throw new Error("Either 'table' or 'tableName' is required");
136
- }
137
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
138
- const sanitizedCols = sanitizeIdentifiers(cols);
139
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
140
- ? sanitizeIdentifiers(parsed.select).join(", ")
141
- : "*";
142
- const tsvector = sanitizedCols
143
- .map((c) => `coalesce(${c}, '')`)
144
- .join(" || ' ' || ");
145
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
146
- ? ` LIMIT ${String(parsed.limit)}`
147
- : "";
148
- const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1), ${String(norm)}) as rank
141
+ try {
142
+ const parsed = TextRankSchema.parse(params);
143
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
144
+ const norm = parsed.normalization ?? 0;
145
+ // Handle both column (string) and columns (array) parameters
146
+ let cols;
147
+ if (parsed.columns !== undefined && parsed.columns.length > 0) {
148
+ cols = parsed.columns;
149
+ }
150
+ else if (parsed.column !== undefined) {
151
+ cols = [parsed.column];
152
+ }
153
+ else {
154
+ return {
155
+ success: false,
156
+ error: "Either column or columns parameter is required",
157
+ };
158
+ }
159
+ // The preprocessor guarantees table is set (converts tableName → table)
160
+ const resolvedTable = parsed.table ?? parsed.tableName;
161
+ if (!resolvedTable) {
162
+ return {
163
+ success: false,
164
+ error: "Either 'table' or 'tableName' is required",
165
+ };
166
+ }
167
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
168
+ const sanitizedCols = sanitizeIdentifiers(cols);
169
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
170
+ ? sanitizeIdentifiers(parsed.select).join(", ")
171
+ : "*";
172
+ const tsvector = sanitizedCols
173
+ .map((c) => `coalesce(${c}, '')`)
174
+ .join(" || ' ' || ");
175
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
176
+ ? ` LIMIT ${String(parsed.limit)}`
177
+ : "";
178
+ const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1), ${String(norm)}) as rank
149
179
  FROM ${tableName}
150
180
  WHERE to_tsvector('${cfg}', ${tsvector}) @@ plainto_tsquery('${cfg}', $1)
151
181
  ORDER BY rank DESC${limitClause}`;
152
- const result = await adapter.executeQuery(sql, [parsed.query]);
153
- return { rows: result.rows, count: result.rows?.length ?? 0 };
182
+ const result = await adapter.executeQuery(sql, [parsed.query]);
183
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
184
+ }
185
+ catch (error) {
186
+ if (error instanceof ZodError) {
187
+ return {
188
+ success: false,
189
+ error: `pg_text_rank validation error: ${error.issues.map((e) => e.message).join(", ")}`,
190
+ };
191
+ }
192
+ return {
193
+ success: false,
194
+ error: formatPostgresError(error, {
195
+ tool: "pg_text_rank",
196
+ }),
197
+ };
198
+ }
154
199
  },
155
200
  };
156
201
  }
@@ -164,29 +209,48 @@ function createTrigramSimilarityTool(adapter) {
164
209
  annotations: readOnly("Trigram Similarity"),
165
210
  icons: getToolIcons("text", readOnly("Trigram Similarity")),
166
211
  handler: async (params, _context) => {
167
- const parsed = TrigramSimilaritySchema.parse(params);
168
- const thresh = parsed.threshold ?? 0.3;
169
- // Default limit to 100 to prevent large payloads
170
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
171
- // The preprocessor guarantees table is set (converts tableName table)
172
- const resolvedTable = parsed.table ?? parsed.tableName;
173
- if (!resolvedTable) {
174
- throw new Error("Either 'table' or 'tableName' is required");
175
- }
176
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
177
- const columnName = sanitizeIdentifier(parsed.column);
178
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
179
- ? sanitizeIdentifiers(parsed.select).join(", ")
180
- : "*";
181
- const additionalWhere = parsed.where
182
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
183
- : "";
184
- const sql = `SELECT ${selectCols}, similarity(${columnName}, $1) as similarity
212
+ try {
213
+ const parsed = TrigramSimilaritySchema.parse(params);
214
+ const thresh = parsed.threshold ?? 0.3;
215
+ // Default limit to 100 to prevent large payloads
216
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
217
+ // The preprocessor guarantees table is set (converts tableName → table)
218
+ const resolvedTable = parsed.table ?? parsed.tableName;
219
+ if (!resolvedTable) {
220
+ return {
221
+ success: false,
222
+ error: "Either 'table' or 'tableName' is required",
223
+ };
224
+ }
225
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
226
+ const columnName = sanitizeIdentifier(parsed.column);
227
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
228
+ ? sanitizeIdentifiers(parsed.select).join(", ")
229
+ : "*";
230
+ const additionalWhere = parsed.where
231
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
232
+ : "";
233
+ const sql = `SELECT ${selectCols}, similarity(${columnName}, $1) as similarity
185
234
  FROM ${tableName}
186
235
  WHERE similarity(${columnName}, $1) > ${String(thresh)}${additionalWhere}
187
236
  ORDER BY similarity DESC LIMIT ${String(limitVal)}`;
188
- const result = await adapter.executeQuery(sql, [parsed.value]);
189
- return { rows: result.rows, count: result.rows?.length ?? 0 };
237
+ const result = await adapter.executeQuery(sql, [parsed.value]);
238
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
239
+ }
240
+ catch (error) {
241
+ if (error instanceof ZodError) {
242
+ return {
243
+ success: false,
244
+ error: `pg_trigram_similarity validation error: ${error.issues.map((e) => e.message).join(", ")}`,
245
+ };
246
+ }
247
+ return {
248
+ success: false,
249
+ error: formatPostgresError(error, {
250
+ tool: "pg_trigram_similarity",
251
+ }),
252
+ };
253
+ }
190
254
  },
191
255
  };
192
256
  }
@@ -198,7 +262,10 @@ function createFuzzyMatchTool(adapter) {
198
262
  tableName: z.string().optional().describe("Table name (alias for table)"),
199
263
  column: z.string(),
200
264
  value: z.string(),
201
- method: z.enum(["soundex", "levenshtein", "metaphone"]).optional(),
265
+ method: z
266
+ .string()
267
+ .optional()
268
+ .describe("Fuzzy match method (default: levenshtein). Valid: soundex, levenshtein, metaphone"),
202
269
  maxDistance: z
203
270
  .number()
204
271
  .optional()
@@ -225,37 +292,68 @@ function createFuzzyMatchTool(adapter) {
225
292
  annotations: readOnly("Fuzzy Match"),
226
293
  icons: getToolIcons("text", readOnly("Fuzzy Match")),
227
294
  handler: async (params, _context) => {
228
- const parsed = FuzzyMatchSchema.parse(params);
229
- // Method is already validated by zod enum, default to levenshtein if not provided
230
- const method = parsed.method ?? "levenshtein";
231
- const maxDist = parsed.maxDistance ?? 3;
232
- // Default limit to 100 to prevent large payloads
233
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
234
- // The preprocessor guarantees table is set (converts tableName → table)
235
- const resolvedTable = parsed.table ?? parsed.tableName;
236
- if (!resolvedTable) {
237
- throw new Error("Either 'table' or 'tableName' is required");
238
- }
239
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
240
- const columnName = sanitizeIdentifier(parsed.column);
241
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
242
- ? sanitizeIdentifiers(parsed.select).join(", ")
243
- : "*";
244
- const additionalWhere = parsed.where
245
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
246
- : "";
247
- let sql;
248
- if (method === "soundex") {
249
- sql = `SELECT ${selectCols}, soundex(${columnName}) as code FROM ${tableName} WHERE soundex(${columnName}) = soundex($1)${additionalWhere} LIMIT ${String(limitVal)}`;
295
+ try {
296
+ const parsed = FuzzyMatchSchema.parse(params);
297
+ // Validate method (moved from z.enum to handler for structured error)
298
+ const VALID_METHODS = [
299
+ "levenshtein",
300
+ "soundex",
301
+ "metaphone",
302
+ ];
303
+ const rawMethod = parsed.method ?? "levenshtein";
304
+ if (!VALID_METHODS.includes(rawMethod)) {
305
+ return {
306
+ success: false,
307
+ error: `Invalid method "${rawMethod}". Valid methods: ${VALID_METHODS.join(", ")}`,
308
+ };
309
+ }
310
+ const method = rawMethod;
311
+ const maxDist = parsed.maxDistance ?? 3;
312
+ // Default limit to 100 to prevent large payloads
313
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
314
+ // The preprocessor guarantees table is set (converts tableName → table)
315
+ const resolvedTable = parsed.table ?? parsed.tableName;
316
+ if (!resolvedTable) {
317
+ return {
318
+ success: false,
319
+ error: "Either 'table' or 'tableName' is required",
320
+ };
321
+ }
322
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
323
+ const columnName = sanitizeIdentifier(parsed.column);
324
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
325
+ ? sanitizeIdentifiers(parsed.select).join(", ")
326
+ : "*";
327
+ const additionalWhere = parsed.where
328
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
329
+ : "";
330
+ let sql;
331
+ if (method === "soundex") {
332
+ sql = `SELECT ${selectCols}, soundex(${columnName}) as code FROM ${tableName} WHERE soundex(${columnName}) = soundex($1)${additionalWhere} LIMIT ${String(limitVal)}`;
333
+ }
334
+ else if (method === "metaphone") {
335
+ sql = `SELECT ${selectCols}, metaphone(${columnName}, 10) as code FROM ${tableName} WHERE metaphone(${columnName}, 10) = metaphone($1, 10)${additionalWhere} LIMIT ${String(limitVal)}`;
336
+ }
337
+ else {
338
+ sql = `SELECT ${selectCols}, levenshtein(${columnName}, $1) as distance FROM ${tableName} WHERE levenshtein(${columnName}, $1) <= ${String(maxDist)}${additionalWhere} ORDER BY distance LIMIT ${String(limitVal)}`;
339
+ }
340
+ const result = await adapter.executeQuery(sql, [parsed.value]);
341
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
250
342
  }
251
- else if (method === "metaphone") {
252
- sql = `SELECT ${selectCols}, metaphone(${columnName}, 10) as code FROM ${tableName} WHERE metaphone(${columnName}, 10) = metaphone($1, 10)${additionalWhere} LIMIT ${String(limitVal)}`;
343
+ catch (error) {
344
+ if (error instanceof ZodError) {
345
+ return {
346
+ success: false,
347
+ error: `pg_fuzzy_match validation error: ${error.issues.map((e) => e.message).join(", ")}`,
348
+ };
349
+ }
350
+ return {
351
+ success: false,
352
+ error: formatPostgresError(error, {
353
+ tool: "pg_fuzzy_match",
354
+ }),
355
+ };
253
356
  }
254
- else {
255
- sql = `SELECT ${selectCols}, levenshtein(${columnName}, $1) as distance FROM ${tableName} WHERE levenshtein(${columnName}, $1) <= ${String(maxDist)}${additionalWhere} ORDER BY distance LIMIT ${String(limitVal)}`;
256
- }
257
- const result = await adapter.executeQuery(sql, [parsed.value]);
258
- return { rows: result.rows, count: result.rows?.length ?? 0 };
259
357
  },
260
358
  };
261
359
  }
@@ -269,27 +367,46 @@ function createRegexpMatchTool(adapter) {
269
367
  annotations: readOnly("Regexp Match"),
270
368
  icons: getToolIcons("text", readOnly("Regexp Match")),
271
369
  handler: async (params, _context) => {
272
- const parsed = RegexpMatchSchema.parse(params);
273
- // The preprocessor guarantees table is set (converts tableName → table)
274
- const resolvedTable = parsed.table ?? parsed.tableName;
275
- if (!resolvedTable) {
276
- throw new Error("Either 'table' or 'tableName' is required");
370
+ try {
371
+ const parsed = RegexpMatchSchema.parse(params);
372
+ // The preprocessor guarantees table is set (converts tableName → table)
373
+ const resolvedTable = parsed.table ?? parsed.tableName;
374
+ if (!resolvedTable) {
375
+ return {
376
+ success: false,
377
+ error: "Either 'table' or 'tableName' is required",
378
+ };
379
+ }
380
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
381
+ const columnName = sanitizeIdentifier(parsed.column);
382
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
383
+ ? sanitizeIdentifiers(parsed.select).join(", ")
384
+ : "*";
385
+ const op = parsed.flags?.includes("i") ? "~*" : "~";
386
+ const additionalWhere = parsed.where
387
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
388
+ : "";
389
+ // Default limit to 100 to prevent large payloads
390
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
391
+ const limitClause = ` LIMIT ${String(limitVal)}`;
392
+ const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
393
+ const result = await adapter.executeQuery(sql, [parsed.pattern]);
394
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
395
+ }
396
+ catch (error) {
397
+ if (error instanceof ZodError) {
398
+ return {
399
+ success: false,
400
+ error: `pg_regexp_match validation error: ${error.issues.map((e) => e.message).join(", ")}`,
401
+ };
402
+ }
403
+ return {
404
+ success: false,
405
+ error: formatPostgresError(error, {
406
+ tool: "pg_regexp_match",
407
+ }),
408
+ };
277
409
  }
278
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
279
- const columnName = sanitizeIdentifier(parsed.column);
280
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
281
- ? sanitizeIdentifiers(parsed.select).join(", ")
282
- : "*";
283
- const op = parsed.flags?.includes("i") ? "~*" : "~";
284
- const additionalWhere = parsed.where
285
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
286
- : "";
287
- // Default limit to 100 to prevent large payloads
288
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
289
- const limitClause = ` LIMIT ${String(limitVal)}`;
290
- const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
291
- const result = await adapter.executeQuery(sql, [parsed.pattern]);
292
- return { rows: result.rows, count: result.rows?.length ?? 0 };
293
410
  },
294
411
  };
295
412
  }
@@ -327,27 +444,46 @@ function createLikeSearchTool(adapter) {
327
444
  annotations: readOnly("LIKE Search"),
328
445
  icons: getToolIcons("text", readOnly("LIKE Search")),
329
446
  handler: async (params, _context) => {
330
- const parsed = LikeSearchSchema.parse(params);
331
- // The preprocessor guarantees table is set (converts tableName → table)
332
- const resolvedTable = parsed.table ?? parsed.tableName;
333
- if (!resolvedTable) {
334
- throw new Error("Either 'table' or 'tableName' is required");
447
+ try {
448
+ const parsed = LikeSearchSchema.parse(params);
449
+ // The preprocessor guarantees table is set (converts tableName → table)
450
+ const resolvedTable = parsed.table ?? parsed.tableName;
451
+ if (!resolvedTable) {
452
+ return {
453
+ success: false,
454
+ error: "Either 'table' or 'tableName' is required",
455
+ };
456
+ }
457
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
458
+ const columnName = sanitizeIdentifier(parsed.column);
459
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
460
+ ? sanitizeIdentifiers(parsed.select).join(", ")
461
+ : "*";
462
+ const op = parsed.caseSensitive === true ? "LIKE" : "ILIKE";
463
+ const additionalWhere = parsed.where
464
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
465
+ : "";
466
+ // Default limit to 100 to prevent large payloads
467
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
468
+ const limitClause = ` LIMIT ${String(limitVal)}`;
469
+ const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
470
+ const result = await adapter.executeQuery(sql, [parsed.pattern]);
471
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
472
+ }
473
+ catch (error) {
474
+ if (error instanceof ZodError) {
475
+ return {
476
+ success: false,
477
+ error: `pg_like_search validation error: ${error.issues.map((e) => e.message).join(", ")}`,
478
+ };
479
+ }
480
+ return {
481
+ success: false,
482
+ error: formatPostgresError(error, {
483
+ tool: "pg_like_search",
484
+ }),
485
+ };
335
486
  }
336
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
337
- const columnName = sanitizeIdentifier(parsed.column);
338
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
339
- ? sanitizeIdentifiers(parsed.select).join(", ")
340
- : "*";
341
- const op = parsed.caseSensitive === true ? "LIKE" : "ILIKE";
342
- const additionalWhere = parsed.where
343
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
344
- : "";
345
- // Default limit to 100 to prevent large payloads
346
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
347
- const limitClause = ` LIMIT ${String(limitVal)}`;
348
- const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
349
- const result = await adapter.executeQuery(sql, [parsed.pattern]);
350
- return { rows: result.rows, count: result.rows?.length ?? 0 };
351
487
  },
352
488
  };
353
489
  }
@@ -395,40 +531,59 @@ function createTextHeadlineTool(adapter) {
395
531
  annotations: readOnly("Text Headline"),
396
532
  icons: getToolIcons("text", readOnly("Text Headline")),
397
533
  handler: async (params, _context) => {
398
- const parsed = HeadlineSchema.parse(params);
399
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
400
- // Build options string from individual params or use provided options
401
- let opts;
402
- if (parsed.options) {
403
- opts = parsed.options;
404
- }
405
- else {
406
- const optParts = [];
407
- optParts.push(`StartSel=${parsed.startSel ?? "<b>"}`);
408
- optParts.push(`StopSel=${parsed.stopSel ?? "</b>"}`);
409
- optParts.push(`MaxWords=${String(parsed.maxWords ?? 35)}`);
410
- optParts.push(`MinWords=${String(parsed.minWords ?? 15)}`);
411
- opts = optParts.join(", ");
412
- }
413
- // The preprocessor guarantees table is set (converts tableName → table)
414
- const resolvedTable = parsed.table ?? parsed.tableName;
415
- if (!resolvedTable) {
416
- throw new Error("Either 'table' or 'tableName' is required");
417
- }
418
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
419
- const columnName = sanitizeIdentifier(parsed.column);
420
- // Use provided select columns, or default to * (user should specify PK for stable identification)
421
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
422
- ? sanitizeIdentifiers(parsed.select).join(", ") + ", "
423
- : "";
424
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
425
- ? ` LIMIT ${String(parsed.limit)}`
426
- : "";
427
- const sql = `SELECT ${selectCols}ts_headline('${cfg}', ${columnName}, plainto_tsquery('${cfg}', $1), '${opts}') as headline
534
+ try {
535
+ const parsed = HeadlineSchema.parse(params);
536
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
537
+ // Build options string from individual params or use provided options
538
+ let opts;
539
+ if (parsed.options) {
540
+ opts = parsed.options;
541
+ }
542
+ else {
543
+ const optParts = [];
544
+ optParts.push(`StartSel=${parsed.startSel ?? "<b>"}`);
545
+ optParts.push(`StopSel=${parsed.stopSel ?? "</b>"}`);
546
+ optParts.push(`MaxWords=${String(parsed.maxWords ?? 35)}`);
547
+ optParts.push(`MinWords=${String(parsed.minWords ?? 15)}`);
548
+ opts = optParts.join(", ");
549
+ }
550
+ // The preprocessor guarantees table is set (converts tableName → table)
551
+ const resolvedTable = parsed.table ?? parsed.tableName;
552
+ if (!resolvedTable) {
553
+ return {
554
+ success: false,
555
+ error: "Either 'table' or 'tableName' is required",
556
+ };
557
+ }
558
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
559
+ const columnName = sanitizeIdentifier(parsed.column);
560
+ // Use provided select columns, or default to * (user should specify PK for stable identification)
561
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
562
+ ? sanitizeIdentifiers(parsed.select).join(", ") + ", "
563
+ : "";
564
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
565
+ ? ` LIMIT ${String(parsed.limit)}`
566
+ : "";
567
+ const sql = `SELECT ${selectCols}ts_headline('${cfg}', ${columnName}, plainto_tsquery('${cfg}', $1), '${opts}') as headline
428
568
  FROM ${tableName}
429
569
  WHERE to_tsvector('${cfg}', ${columnName}) @@ plainto_tsquery('${cfg}', $1)${limitClause}`;
430
- const result = await adapter.executeQuery(sql, [parsed.query]);
431
- return { rows: result.rows, count: result.rows?.length ?? 0 };
570
+ const result = await adapter.executeQuery(sql, [parsed.query]);
571
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
572
+ }
573
+ catch (error) {
574
+ if (error instanceof ZodError) {
575
+ return {
576
+ success: false,
577
+ error: `pg_text_headline validation error: ${error.issues.map((e) => e.message).join(", ")}`,
578
+ };
579
+ }
580
+ return {
581
+ success: false,
582
+ error: formatPostgresError(error, {
583
+ tool: "pg_text_headline",
584
+ }),
585
+ };
586
+ }
432
587
  },
433
588
  };
434
589
  }
@@ -461,36 +616,55 @@ function createFtsIndexTool(adapter) {
461
616
  annotations: write("Create FTS Index"),
462
617
  icons: getToolIcons("text", write("Create FTS Index")),
463
618
  handler: async (params, _context) => {
464
- const parsed = FtsIndexSchema.parse(params);
465
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
466
- // The preprocessor guarantees table is set (converts tableName → table)
467
- const resolvedTable = parsed.table ?? parsed.tableName;
468
- if (!resolvedTable) {
469
- throw new Error("Either 'table' or 'tableName' is required");
619
+ try {
620
+ const parsed = FtsIndexSchema.parse(params);
621
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
622
+ // The preprocessor guarantees table is set (converts tableName → table)
623
+ const resolvedTable = parsed.table ?? parsed.tableName;
624
+ if (!resolvedTable) {
625
+ return {
626
+ success: false,
627
+ error: "Either 'table' or 'tableName' is required",
628
+ };
629
+ }
630
+ const defaultIndexName = `idx_${resolvedTable}_${parsed.column}_fts`;
631
+ const resolvedIndexName = parsed.name ?? defaultIndexName;
632
+ const indexName = sanitizeIdentifier(resolvedIndexName);
633
+ // Default to IF NOT EXISTS for safer operation (skip existing indexes)
634
+ const useIfNotExists = parsed.ifNotExists !== false;
635
+ const ifNotExists = useIfNotExists ? "IF NOT EXISTS " : "";
636
+ // Build qualified table name with schema support
637
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
638
+ const columnName = sanitizeIdentifier(parsed.column);
639
+ // Check if index exists before creation (to accurately report 'skipped')
640
+ let existedBefore = false;
641
+ if (useIfNotExists) {
642
+ const checkResult = await adapter.executeQuery(`SELECT 1 FROM pg_indexes WHERE indexname = $1 LIMIT 1`, [resolvedIndexName]);
643
+ existedBefore = (checkResult.rows?.length ?? 0) > 0;
644
+ }
645
+ const sql = `CREATE INDEX ${ifNotExists}${indexName} ON ${tableName} USING gin(to_tsvector('${cfg}', ${columnName}))`;
646
+ await adapter.executeQuery(sql);
647
+ return {
648
+ success: true,
649
+ index: resolvedIndexName,
650
+ config: cfg,
651
+ skipped: existedBefore,
652
+ };
470
653
  }
471
- const defaultIndexName = `idx_${resolvedTable}_${parsed.column}_fts`;
472
- const resolvedIndexName = parsed.name ?? defaultIndexName;
473
- const indexName = sanitizeIdentifier(resolvedIndexName);
474
- // Default to IF NOT EXISTS for safer operation (skip existing indexes)
475
- const useIfNotExists = parsed.ifNotExists !== false;
476
- const ifNotExists = useIfNotExists ? "IF NOT EXISTS " : "";
477
- // Build qualified table name with schema support
478
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
479
- const columnName = sanitizeIdentifier(parsed.column);
480
- // Check if index exists before creation (to accurately report 'skipped')
481
- let existedBefore = false;
482
- if (useIfNotExists) {
483
- const checkResult = await adapter.executeQuery(`SELECT 1 FROM pg_indexes WHERE indexname = $1 LIMIT 1`, [resolvedIndexName]);
484
- existedBefore = (checkResult.rows?.length ?? 0) > 0;
654
+ catch (error) {
655
+ if (error instanceof ZodError) {
656
+ return {
657
+ success: false,
658
+ error: `pg_create_fts_index validation error: ${error.issues.map((e) => e.message).join(", ")}`,
659
+ };
660
+ }
661
+ return {
662
+ success: false,
663
+ error: formatPostgresError(error, {
664
+ tool: "pg_create_fts_index",
665
+ }),
666
+ };
485
667
  }
486
- const sql = `CREATE INDEX ${ifNotExists}${indexName} ON ${tableName} USING gin(to_tsvector('${cfg}', ${columnName}))`;
487
- await adapter.executeQuery(sql);
488
- return {
489
- success: true,
490
- index: resolvedIndexName,
491
- config: cfg,
492
- skipped: existedBefore,
493
- };
494
668
  },
495
669
  };
496
670
  }