ToSQL generates PostgreSQL-compatible SQL with $1, $2, ... placeholders. Returns the SQL string, parameters slice, and any error. Placeholder rules: - Single ? → parameter placeholder (converted to $1, $2, etc.) - Double ?? → literal ? in SQL (for PostgreSQL JSONB operators like ?, ?|, ?&)
()
| 154 | // - Single ? → parameter placeholder (converted to $1, $2, etc.) |
| 155 | // - Double ?? → literal ? in SQL (for PostgreSQL JSONB operators like ?, ?|, ?&) |
| 156 | func (q *Query) ToSQL() (string, []any, error) { |
| 157 | if q == nil { |
| 158 | return "", nil, errors.New("cannot generate SQL from nil Query") |
| 159 | } |
| 160 | |
| 161 | // First pass: concatenate all parts and collect params |
| 162 | var sqlBuilder strings.Builder |
| 163 | params := make([]any, 0) |
| 164 | |
| 165 | for _, part := range q.parts { |
| 166 | // Check for errors in this part |
| 167 | if len(part.Errs) > 0 { |
| 168 | // Return the first error encountered |
| 169 | return "", nil, part.Errs[0] |
| 170 | } |
| 171 | sqlBuilder.WriteString(part.Text) |
| 172 | params = append(params, part.Params...) |
| 173 | } |
| 174 | |
| 175 | sql := sqlBuilder.String() |
| 176 | |
| 177 | // Second pass: replace ? placeholders with $1, $2, etc., handling ?? escape sequence |
| 178 | // Use a unique delimiter that won't appear in SQL |
| 179 | const delimiter = "\x00" |
| 180 | // Replace ?? with delimiter temporarily |
| 181 | sql = strings.ReplaceAll(sql, "??", delimiter) |
| 182 | // Split on remaining ? (these are all placeholders) |
| 183 | parts := strings.Split(sql, "?") |
| 184 | |
| 185 | // Verify parameter count matches |
| 186 | if len(parts)-1 != len(params) { |
| 187 | return "", nil, errors.New("mismatched parameters: ? placeholders count does not match params count") |
| 188 | } |
| 189 | |
| 190 | // Build final SQL with $1, $2, etc. |
| 191 | var builder strings.Builder |
| 192 | for i := range params { |
| 193 | builder.WriteString(parts[i]) |
| 194 | builder.WriteString("$") |
| 195 | builder.WriteString(strconv.Itoa(i + 1)) |
| 196 | } |
| 197 | builder.WriteString(parts[len(parts)-1]) |
| 198 | |
| 199 | // Replace delimiter back to ? |
| 200 | finalSQL := strings.ReplaceAll(builder.String(), delimiter, "?") |
| 201 | |
| 202 | return finalSQL, params, nil |
| 203 | } |