|
94 | 94 | exception |
95 | 95 | copy/*root-dir*)))) |
96 | 96 |
|
| 97 | +(defn- bisect-batch! |
| 98 | + "Binary search over batch[start..end) to find and reject all bad rows. |
| 99 | + Each good sub-range is committed independently so no good rows are lost. |
| 100 | + At the base case (one row) the row is tried individually and, if it fails, |
| 101 | + written to the reject files. |
| 102 | + Complexity: O(k · log N) COPY round-trips for k bad rows in a range of N. |
| 103 | + Returns {:errors n :rows-ok n :reject-paths p}." |
| 104 | + [batch table-spec ^PGConnection pg-conn ^String copy-sql start end reject-paths] |
| 105 | + (let [cnt (- end start)] |
| 106 | + (if (<= cnt 1) |
| 107 | + ;; Base case: try the single row to obtain the exact exception for the |
| 108 | + ;; reject log, then reject it if it still fails. |
| 109 | + (do |
| 110 | + (log/debug (str "bisect: trying 1 row at position " start)) |
| 111 | + (try |
| 112 | + (send-rows! pg-conn batch start end copy-sql) |
| 113 | + (.commit ^Connection pg-conn) |
| 114 | + {:errors 0 :rows-ok 1 :reject-paths reject-paths} |
| 115 | + (catch PSQLException e |
| 116 | + (.rollback ^Connection pg-conn) |
| 117 | + (log/warn (str "Row permanently rejected (bisect) for " |
| 118 | + (:target-table table-spec))) |
| 119 | + (let [paths (write-reject-row! batch start table-spec e)] |
| 120 | + {:errors 1 :rows-ok 0 :reject-paths (or paths reject-paths)})))) |
| 121 | + ;; Recursive case: split and try each half independently. |
| 122 | + (let [mid (+ start (quot cnt 2)) |
| 123 | + _ (log/debug (str "bisect: trying " (- mid start) |
| 124 | + " rows [" start ", " mid ")")) |
| 125 | + first-result (try |
| 126 | + (send-rows! pg-conn batch start mid copy-sql) |
| 127 | + (.commit ^Connection pg-conn) |
| 128 | + {:errors 0 :rows-ok (- mid start) :reject-paths reject-paths} |
| 129 | + (catch PSQLException _e |
| 130 | + (.rollback ^Connection pg-conn) |
| 131 | + (bisect-batch! batch table-spec pg-conn copy-sql |
| 132 | + start mid reject-paths))) |
| 133 | + _ (log/debug (str "bisect: trying " (- end mid) |
| 134 | + " rows [" mid ", " end ")")) |
| 135 | + second-result (try |
| 136 | + (send-rows! pg-conn batch mid end copy-sql) |
| 137 | + (.commit ^Connection pg-conn) |
| 138 | + {:errors 0 :rows-ok (- end mid) |
| 139 | + :reject-paths (:reject-paths first-result)} |
| 140 | + (catch PSQLException _e |
| 141 | + (.rollback ^Connection pg-conn) |
| 142 | + (bisect-batch! batch table-spec pg-conn copy-sql |
| 143 | + mid end (:reject-paths first-result))))] |
| 144 | + {:errors (+ (:errors first-result) (:errors second-result)) |
| 145 | + :rows-ok (+ (:rows-ok first-result) (:rows-ok second-result)) |
| 146 | + :reject-paths (:reject-paths second-result)})))) |
| 147 | + |
97 | 148 | (defn- retry-loop |
98 | 149 | "Retry helper: attempts to send a sub-batch [pos, total-rows). |
99 | | - On success, commits and returns. On PSQLException, rolls back, |
100 | | - identifies the bad row, writes it to reject files, and recurses. |
101 | | - Each sub-batch is independently committed (matching copy-partial-batch |
102 | | - in the CL implementation). |
| 150 | + When PostgreSQL supplies a COPY line number in the error CONTEXT, skips |
| 151 | + directly to that row and commits all preceding good rows in one shot. |
| 152 | + When no line number is available (e.g. FK violations), delegates to |
| 153 | + bisect-batch! which finds bad rows in O(k·log N) round-trips. |
| 154 | + Each sub-batch is independently committed. |
103 | 155 | Returns {:errors n :rows-ok n :reject-paths {...}}." |
104 | 156 | [batch table-spec ^PGConnection pg-conn |
105 | 157 | ^String copy-sql pos errors rows-ok total-rows reject-paths] |
|
117 | 169 | (if (= :ok result) |
118 | 170 | {:errors errors :rows-ok (+ rows-ok (- total-rows pos)) |
119 | 171 | :reject-paths reject-paths} |
120 | | - (let [e (:error result) |
121 | | - err-line (parse-copy-line e) |
122 | | - bad-idx (if (and err-line |
123 | | - (< (+ pos err-line) total-rows) |
124 | | - (>= (+ pos err-line) pos)) |
125 | | - (+ pos err-line) |
126 | | - pos)] |
127 | | - ;; Try to send good rows before the bad row as a committed sub-batch |
128 | | - (if (< pos bad-idx) |
129 | | - ;; There are good rows before the bad row. Try to commit them. |
130 | | - (let [sub-result |
131 | | - (try |
132 | | - (send-rows! pg-conn batch pos bad-idx copy-sql) |
133 | | - (.commit ^Connection pg-conn) |
134 | | - :ok |
135 | | - (catch PSQLException inner-e |
136 | | - (.rollback ^Connection pg-conn) |
137 | | - (let [inner-err-line (parse-copy-line inner-e) |
138 | | - inner-bad-idx (if (and inner-err-line |
139 | | - (< (+ pos inner-err-line) bad-idx) |
140 | | - (>= (+ pos inner-err-line) pos)) |
141 | | - (+ pos inner-err-line) |
142 | | - pos)] |
143 | | - (let [paths (write-reject-row! batch inner-bad-idx |
144 | | - table-spec inner-e)] |
145 | | - (log/warn (str "Row permanently rejected for " |
146 | | - (:target-table table-spec))) |
147 | | - {:sub-error true |
148 | | - :new-pos (inc inner-bad-idx) |
149 | | - :new-errors (inc errors) |
150 | | - :new-rows-ok rows-ok |
151 | | - :new-reject-paths (or paths reject-paths)}))))] |
152 | | - (if (= :ok sub-result) |
153 | | - ;; Good rows before bad row committed. Reject bad row and continue. |
154 | | - (let [sub-rows (- bad-idx pos) |
155 | | - paths (write-reject-row! batch bad-idx table-spec e)] |
| 172 | + (let [e (:error result) |
| 173 | + err-line (parse-copy-line e)] |
| 174 | + (if (nil? err-line) |
| 175 | + ;; No COPY line number in the error (e.g. FK violation). |
| 176 | + ;; Use binary search to locate and reject bad rows. |
| 177 | + (let [bisect (bisect-batch! batch table-spec pg-conn copy-sql |
| 178 | + pos total-rows reject-paths)] |
| 179 | + {:errors (+ errors (:errors bisect)) |
| 180 | + :rows-ok (+ rows-ok (:rows-ok bisect)) |
| 181 | + :reject-paths (:reject-paths bisect)}) |
| 182 | + ;; We have a line number — skip directly to the bad row. |
| 183 | + (let [bad-idx (if (and (< (+ pos err-line) total-rows) |
| 184 | + (>= (+ pos err-line) pos)) |
| 185 | + (+ pos err-line) |
| 186 | + pos)] |
| 187 | + (if (< pos bad-idx) |
| 188 | + ;; Commit good rows before the bad row, then reject it. |
| 189 | + (let [sub-result |
| 190 | + (try |
| 191 | + (send-rows! pg-conn batch pos bad-idx copy-sql) |
| 192 | + (.commit ^Connection pg-conn) |
| 193 | + :ok |
| 194 | + (catch PSQLException inner-e |
| 195 | + (.rollback ^Connection pg-conn) |
| 196 | + ;; Sub-batch itself failed — bisect it. |
| 197 | + (bisect-batch! batch table-spec pg-conn copy-sql |
| 198 | + pos bad-idx reject-paths)))] |
| 199 | + (if (= :ok sub-result) |
| 200 | + (let [sub-rows (- bad-idx pos) |
| 201 | + paths (write-reject-row! batch bad-idx table-spec e)] |
| 202 | + (log/warn (str "Row permanently rejected for " |
| 203 | + (:target-table table-spec))) |
| 204 | + (recur (inc bad-idx) (inc errors) (+ rows-ok sub-rows) |
| 205 | + (or paths reject-paths))) |
| 206 | + ;; sub-result is a bisect result map |
| 207 | + (recur (inc bad-idx) |
| 208 | + (+ errors (:errors sub-result)) |
| 209 | + (+ rows-ok (:rows-ok sub-result)) |
| 210 | + (:reject-paths sub-result)))) |
| 211 | + ;; Bad row is right at pos — reject it and continue. |
| 212 | + (let [paths (write-reject-row! batch bad-idx table-spec e)] |
156 | 213 | (log/warn (str "Row permanently rejected for " |
157 | 214 | (:target-table table-spec))) |
158 | | - (recur (inc bad-idx) (inc errors) (+ rows-ok sub-rows) |
159 | | - (or paths reject-paths))) |
160 | | - ;; Sub-batch failed. Use state from inner failure. |
161 | | - (recur (:new-pos sub-result) (:new-errors sub-result) |
162 | | - (:new-rows-ok sub-result) |
163 | | - (:new-reject-paths sub-result)))) |
164 | | - ;; No good rows before the bad row. Just reject and continue. |
165 | | - (let [paths (write-reject-row! batch bad-idx table-spec e)] |
166 | | - (log/warn (str "Row permanently rejected for " |
167 | | - (:target-table table-spec))) |
168 | | - (recur (inc bad-idx) (inc errors) rows-ok |
169 | | - (or paths reject-paths)))))))))) |
| 215 | + (recur (inc bad-idx) (inc errors) rows-ok |
| 216 | + (or paths reject-paths)))))))))))) |
170 | 217 |
|
171 | 218 | (defn retry-batch! |
172 | 219 | "Entry point for error recovery. Called after the outer batch has been |
173 | 220 | rolled back. The connection is in autoCommit=false with no active |
174 | 221 | transaction. Each sub-batch in the retry gets its own commit. |
175 | 222 | Returns {:errors n :rows-ok n :reject-paths {:reject-data string :reject-log string}}." |
176 | 223 | [batch table-spec ^PSQLException first-exception ^PGConnection pg-conn] |
| 224 | + (log/info "Entering error recovery.") |
177 | 225 | (retry-loop batch table-spec pg-conn |
178 | 226 | (copy/copy-sql table-spec) |
179 | 227 | 0 0 0 (:row-count batch) nil)) |
0 commit comments