1251881Speter/* strings-table.c : operations on the `strings' table
2251881Speter *
3251881Speter * ====================================================================
4251881Speter *    Licensed to the Apache Software Foundation (ASF) under one
5251881Speter *    or more contributor license agreements.  See the NOTICE file
6251881Speter *    distributed with this work for additional information
7251881Speter *    regarding copyright ownership.  The ASF licenses this file
8251881Speter *    to you under the Apache License, Version 2.0 (the
9251881Speter *    "License"); you may not use this file except in compliance
10251881Speter *    with the License.  You may obtain a copy of the License at
11251881Speter *
12251881Speter *      http://www.apache.org/licenses/LICENSE-2.0
13251881Speter *
14251881Speter *    Unless required by applicable law or agreed to in writing,
15251881Speter *    software distributed under the License is distributed on an
16251881Speter *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17251881Speter *    KIND, either express or implied.  See the License for the
18251881Speter *    specific language governing permissions and limitations
19251881Speter *    under the License.
20251881Speter * ====================================================================
21251881Speter */
22251881Speter
23251881Speter#include "bdb_compat.h"
24251881Speter#include "svn_fs.h"
25251881Speter#include "svn_pools.h"
26251881Speter#include "../fs.h"
27251881Speter#include "../err.h"
28251881Speter#include "dbt.h"
29251881Speter#include "../trail.h"
30251881Speter#include "../key-gen.h"
31251881Speter#include "../../libsvn_fs/fs-loader.h"
32251881Speter#include "bdb-err.h"
33251881Speter#include "strings-table.h"
34251881Speter
35251881Speter#include "svn_private_config.h"
36251881Speter
37251881Speter
38251881Speter/*** Creating and opening the strings table. ***/
39251881Speter
40251881Speterint
41251881Spetersvn_fs_bdb__open_strings_table(DB **strings_p,
42251881Speter                               DB_ENV *env,
43251881Speter                               svn_boolean_t create)
44251881Speter{
45251881Speter  const u_int32_t open_flags = (create ? (DB_CREATE | DB_EXCL) : 0);
46251881Speter  DB *strings;
47251881Speter
48251881Speter  BDB_ERR(svn_fs_bdb__check_version());
49251881Speter  BDB_ERR(db_create(&strings, env, 0));
50251881Speter
51251881Speter  /* Enable duplicate keys. This allows the data to be spread out across
52251881Speter     multiple records. Note: this must occur before ->open().  */
53251881Speter  BDB_ERR(strings->set_flags(strings, DB_DUP));
54251881Speter
55251881Speter  BDB_ERR((strings->open)(SVN_BDB_OPEN_PARAMS(strings, NULL),
56251881Speter                          "strings", 0, DB_BTREE,
57251881Speter                          open_flags, 0666));
58251881Speter
59251881Speter  if (create)
60251881Speter    {
61251881Speter      DBT key, value;
62251881Speter
63251881Speter      /* Create the `next-key' table entry.  */
64251881Speter      BDB_ERR(strings->put
65251881Speter              (strings, 0,
66251881Speter               svn_fs_base__str_to_dbt(&key, NEXT_KEY_KEY),
67251881Speter               svn_fs_base__str_to_dbt(&value, "0"), 0));
68251881Speter    }
69251881Speter
70251881Speter  *strings_p = strings;
71251881Speter  return 0;
72251881Speter}
73251881Speter
74251881Speter
75251881Speter
76251881Speter/*** Storing and retrieving strings.  ***/
77251881Speter
78251881Speter/* Allocate *CURSOR and advance it to first row in the set of rows
79251881Speter   whose key is defined by QUERY.  Set *LENGTH to the size of that
80251881Speter   first row.  */
81251881Speterstatic svn_error_t *
82251881Speterlocate_key(apr_size_t *length,
83251881Speter           DBC **cursor,
84251881Speter           DBT *query,
85251881Speter           svn_fs_t *fs,
86251881Speter           trail_t *trail,
87251881Speter           apr_pool_t *pool)
88251881Speter{
89251881Speter  base_fs_data_t *bfd = fs->fsap_data;
90251881Speter  int db_err;
91251881Speter  DBT result;
92251881Speter
93251881Speter  svn_fs_base__trail_debug(trail, "strings", "cursor");
94251881Speter  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
95251881Speter                   bfd->strings->cursor(bfd->strings, trail->db_txn,
96251881Speter                                        cursor, 0)));
97251881Speter
98251881Speter  /* Set up the DBT for reading the length of the record. */
99251881Speter  svn_fs_base__clear_dbt(&result);
100251881Speter  result.ulen = 0;
101251881Speter  result.flags |= DB_DBT_USERMEM;
102251881Speter
103251881Speter  /* Advance the cursor to the key that we're looking for. */
104251881Speter  db_err = svn_bdb_dbc_get(*cursor, query, &result, DB_SET);
105251881Speter
106251881Speter  /* We don't need to svn_fs_base__track_dbt() the result, because nothing
107251881Speter     was allocated in it. */
108251881Speter
109251881Speter  /* If there's no such node, return an appropriately specific error.  */
110251881Speter  if (db_err == DB_NOTFOUND)
111251881Speter    {
112251881Speter      svn_bdb_dbc_close(*cursor);
113251881Speter      return svn_error_createf
114251881Speter        (SVN_ERR_FS_NO_SUCH_STRING, 0,
115251881Speter         "No such string '%s'", (const char *)query->data);
116251881Speter    }
117251881Speter  if (db_err)
118251881Speter    {
119251881Speter      DBT rerun;
120251881Speter
121251881Speter      if (db_err != SVN_BDB_DB_BUFFER_SMALL)
122251881Speter        {
123251881Speter          svn_bdb_dbc_close(*cursor);
124251881Speter          return BDB_WRAP(fs, N_("moving cursor"), db_err);
125251881Speter        }
126251881Speter
127251881Speter      /* We got an SVN_BDB_DB_BUFFER_SMALL (typical since we have a
128251881Speter         zero length buf), so we need to re-run the operation to make
129251881Speter         it happen. */
130251881Speter      svn_fs_base__clear_dbt(&rerun);
131251881Speter      rerun.flags |= DB_DBT_USERMEM | DB_DBT_PARTIAL;
132251881Speter      db_err = svn_bdb_dbc_get(*cursor, query, &rerun, DB_SET);
133251881Speter      if (db_err)
134251881Speter        {
135251881Speter          svn_bdb_dbc_close(*cursor);
136251881Speter          return BDB_WRAP(fs, N_("rerunning cursor move"), db_err);
137251881Speter        }
138251881Speter    }
139251881Speter
140251881Speter  /* ### this cast might not be safe? */
141251881Speter  *length = (apr_size_t) result.size;
142251881Speter
143251881Speter  return SVN_NO_ERROR;
144251881Speter}
145251881Speter
146251881Speter
147251881Speter/* Advance CURSOR by a single row in the set of rows whose keys match
148251881Speter   CURSOR's current location.  Set *LENGTH to the size of that next
149251881Speter   row.  If any error occurs, CURSOR will be destroyed.  */
150251881Speterstatic int
151251881Speterget_next_length(apr_size_t *length, DBC *cursor, DBT *query)
152251881Speter{
153251881Speter  DBT result;
154251881Speter  int db_err;
155251881Speter
156251881Speter  /* Set up the DBT for reading the length of the record. */
157251881Speter  svn_fs_base__clear_dbt(&result);
158251881Speter  result.ulen = 0;
159251881Speter  result.flags |= DB_DBT_USERMEM;
160251881Speter
161251881Speter  /* Note: this may change the QUERY DBT, but that's okay: we're going
162251881Speter     to be sticking with the same key anyways.  */
163251881Speter  db_err = svn_bdb_dbc_get(cursor, query, &result, DB_NEXT_DUP);
164251881Speter
165251881Speter  /* Note that we exit on DB_NOTFOUND. The caller uses that to end a loop. */
166251881Speter  if (db_err)
167251881Speter    {
168251881Speter      DBT rerun;
169251881Speter
170251881Speter      if (db_err != SVN_BDB_DB_BUFFER_SMALL)
171251881Speter        {
172251881Speter          svn_bdb_dbc_close(cursor);
173251881Speter          return db_err;
174251881Speter        }
175251881Speter
176251881Speter      /* We got an SVN_BDB_DB_BUFFER_SMALL (typical since we have a
177251881Speter         zero length buf), so we need to re-run the operation to make
178251881Speter         it happen. */
179251881Speter      svn_fs_base__clear_dbt(&rerun);
180251881Speter      rerun.flags |= DB_DBT_USERMEM | DB_DBT_PARTIAL;
181251881Speter      db_err = svn_bdb_dbc_get(cursor, query, &rerun, DB_NEXT_DUP);
182251881Speter      if (db_err)
183251881Speter        svn_bdb_dbc_close(cursor);
184251881Speter    }
185251881Speter
186251881Speter  /* ### this cast might not be safe? */
187251881Speter  *length = (apr_size_t) result.size;
188251881Speter  return db_err;
189251881Speter}
190251881Speter
191251881Speter
192251881Spetersvn_error_t *
193251881Spetersvn_fs_bdb__string_read(svn_fs_t *fs,
194251881Speter                        const char *key,
195251881Speter                        char *buf,
196251881Speter                        svn_filesize_t offset,
197251881Speter                        apr_size_t *len,
198251881Speter                        trail_t *trail,
199251881Speter                        apr_pool_t *pool)
200251881Speter{
201251881Speter  int db_err;
202251881Speter  DBT query, result;
203251881Speter  DBC *cursor;
204251881Speter  apr_size_t length, bytes_read = 0;
205251881Speter
206251881Speter  svn_fs_base__str_to_dbt(&query, key);
207251881Speter
208251881Speter  SVN_ERR(locate_key(&length, &cursor, &query, fs, trail, pool));
209251881Speter
210251881Speter  /* Seek through the records for this key, trying to find the record that
211251881Speter     includes OFFSET. Note that we don't require reading from more than
212251881Speter     one record since we're allowed to return partial reads.  */
213251881Speter  while (length <= offset)
214251881Speter    {
215251881Speter      offset -= length;
216251881Speter
217251881Speter      /* Remember, if any error happens, our cursor has been closed
218251881Speter         for us. */
219251881Speter      db_err = get_next_length(&length, cursor, &query);
220251881Speter
221251881Speter      /* No more records? They tried to read past the end. */
222251881Speter      if (db_err == DB_NOTFOUND)
223251881Speter        {
224251881Speter          *len = 0;
225251881Speter          return SVN_NO_ERROR;
226251881Speter        }
227251881Speter      if (db_err)
228251881Speter        return BDB_WRAP(fs, N_("reading string"), db_err);
229251881Speter    }
230251881Speter
231251881Speter  /* The current record contains OFFSET. Fetch the contents now. Note that
232251881Speter     OFFSET has been moved to be relative to this record. The length could
233251881Speter     quite easily extend past this record, so we use DB_DBT_PARTIAL and
234251881Speter     read successive records until we've filled the request.  */
235251881Speter  while (1)
236251881Speter    {
237251881Speter      svn_fs_base__clear_dbt(&result);
238251881Speter      result.data = buf + bytes_read;
239289180Speter      result.ulen = (u_int32_t)(*len - bytes_read);
240251881Speter      result.doff = (u_int32_t)offset;
241289180Speter      result.dlen = result.ulen;
242251881Speter      result.flags |= (DB_DBT_USERMEM | DB_DBT_PARTIAL);
243251881Speter      db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_CURRENT);
244251881Speter      if (db_err)
245251881Speter        {
246251881Speter          svn_bdb_dbc_close(cursor);
247251881Speter          return BDB_WRAP(fs, N_("reading string"), db_err);
248251881Speter        }
249251881Speter
250251881Speter      bytes_read += result.size;
251251881Speter      if (bytes_read == *len)
252251881Speter        {
253251881Speter          /* Done with the cursor. */
254251881Speter          SVN_ERR(BDB_WRAP(fs, N_("closing string-reading cursor"),
255251881Speter                           svn_bdb_dbc_close(cursor)));
256251881Speter          break;
257251881Speter        }
258251881Speter
259251881Speter      /* Remember, if any error happens, our cursor has been closed
260251881Speter         for us. */
261251881Speter      db_err = get_next_length(&length, cursor, &query);
262251881Speter      if (db_err == DB_NOTFOUND)
263251881Speter        break;
264251881Speter      if (db_err)
265251881Speter        return BDB_WRAP(fs, N_("reading string"), db_err);
266251881Speter
267251881Speter      /* We'll be reading from the beginning of the next record */
268251881Speter      offset = 0;
269251881Speter    }
270251881Speter
271251881Speter  *len = bytes_read;
272251881Speter  return SVN_NO_ERROR;
273251881Speter}
274251881Speter
275251881Speter
276251881Speter/* Get the current 'next-key' value and bump the record. */
277251881Speterstatic svn_error_t *
278251881Speterget_key_and_bump(svn_fs_t *fs,
279251881Speter                 const char **key,
280251881Speter                 trail_t *trail,
281251881Speter                 apr_pool_t *pool)
282251881Speter{
283251881Speter  base_fs_data_t *bfd = fs->fsap_data;
284251881Speter  DBC *cursor;
285251881Speter  char next_key[MAX_KEY_SIZE];
286251881Speter  apr_size_t key_len;
287251881Speter  int db_err;
288251881Speter  DBT query;
289251881Speter  DBT result;
290251881Speter
291251881Speter  /* ### todo: see issue #409 for why bumping the key as part of this
292251881Speter     trail is problematic. */
293251881Speter
294251881Speter  /* Open a cursor and move it to the 'next-key' value. We can then fetch
295251881Speter     the contents and use the cursor to overwrite those contents. Since
296251881Speter     this database allows duplicates, we can't do an arbitrary 'put' to
297251881Speter     write the new value -- that would append, not overwrite.  */
298251881Speter
299251881Speter  svn_fs_base__trail_debug(trail, "strings", "cursor");
300251881Speter  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
301251881Speter                   bfd->strings->cursor(bfd->strings, trail->db_txn,
302251881Speter                                        &cursor, 0)));
303251881Speter
304251881Speter  /* Advance the cursor to 'next-key' and read it. */
305251881Speter
306251881Speter  db_err = svn_bdb_dbc_get(cursor,
307251881Speter                           svn_fs_base__str_to_dbt(&query, NEXT_KEY_KEY),
308251881Speter                           svn_fs_base__result_dbt(&result),
309251881Speter                           DB_SET);
310251881Speter  if (db_err)
311251881Speter    {
312251881Speter      svn_bdb_dbc_close(cursor);
313251881Speter      return BDB_WRAP(fs, N_("getting next-key value"), db_err);
314251881Speter    }
315251881Speter
316251881Speter  svn_fs_base__track_dbt(&result, pool);
317251881Speter  *key = apr_pstrmemdup(pool, result.data, result.size);
318251881Speter
319251881Speter  /* Bump to future key. */
320251881Speter  key_len = result.size;
321251881Speter  svn_fs_base__next_key(result.data, &key_len, next_key);
322251881Speter
323251881Speter  /* Shove the new key back into the database, at the cursor position. */
324251881Speter  db_err = svn_bdb_dbc_put(cursor, &query,
325251881Speter                           svn_fs_base__str_to_dbt(&result, next_key),
326251881Speter                           DB_CURRENT);
327251881Speter  if (db_err)
328251881Speter    {
329251881Speter      svn_bdb_dbc_close(cursor); /* ignore the error, the original is
330251881Speter                                    more important. */
331251881Speter      return BDB_WRAP(fs, N_("bumping next string key"), db_err);
332251881Speter    }
333251881Speter
334251881Speter  return BDB_WRAP(fs, N_("closing string-reading cursor"),
335251881Speter                  svn_bdb_dbc_close(cursor));
336251881Speter}
337251881Speter
338251881Spetersvn_error_t *
339251881Spetersvn_fs_bdb__string_append(svn_fs_t *fs,
340251881Speter                          const char **key,
341251881Speter                          apr_size_t len,
342251881Speter                          const char *buf,
343251881Speter                          trail_t *trail,
344251881Speter                          apr_pool_t *pool)
345251881Speter{
346251881Speter  base_fs_data_t *bfd = fs->fsap_data;
347251881Speter  DBT query, result;
348251881Speter
349251881Speter  /* If the passed-in key is NULL, we graciously generate a new string
350251881Speter     using the value of the `next-key' record in the strings table. */
351251881Speter  if (*key == NULL)
352251881Speter    {
353251881Speter      SVN_ERR(get_key_and_bump(fs, key, trail, pool));
354251881Speter    }
355251881Speter
356251881Speter  /* Store a new record into the database. */
357251881Speter  svn_fs_base__trail_debug(trail, "strings", "put");
358251881Speter  return BDB_WRAP(fs, N_("appending string"),
359251881Speter                  bfd->strings->put
360251881Speter                  (bfd->strings, trail->db_txn,
361251881Speter                   svn_fs_base__str_to_dbt(&query, *key),
362251881Speter                   svn_fs_base__set_dbt(&result, buf, len),
363251881Speter                   0));
364251881Speter}
365251881Speter
366251881Speter
367251881Spetersvn_error_t *
368251881Spetersvn_fs_bdb__string_clear(svn_fs_t *fs,
369251881Speter                         const char *key,
370251881Speter                         trail_t *trail,
371251881Speter                         apr_pool_t *pool)
372251881Speter{
373251881Speter  base_fs_data_t *bfd = fs->fsap_data;
374251881Speter  int db_err;
375251881Speter  DBT query, result;
376251881Speter
377251881Speter  svn_fs_base__str_to_dbt(&query, key);
378251881Speter
379251881Speter  /* Torch the prior contents */
380251881Speter  svn_fs_base__trail_debug(trail, "strings", "del");
381251881Speter  db_err = bfd->strings->del(bfd->strings, trail->db_txn, &query, 0);
382251881Speter
383251881Speter  /* If there's no such node, return an appropriately specific error.  */
384251881Speter  if (db_err == DB_NOTFOUND)
385251881Speter    return svn_error_createf
386251881Speter      (SVN_ERR_FS_NO_SUCH_STRING, 0,
387251881Speter       "No such string '%s'", key);
388251881Speter
389251881Speter  /* Handle any other error conditions.  */
390251881Speter  SVN_ERR(BDB_WRAP(fs, N_("clearing string"), db_err));
391251881Speter
392251881Speter  /* Shove empty data back in for this key. */
393251881Speter  svn_fs_base__clear_dbt(&result);
394251881Speter  result.data = 0;
395251881Speter  result.size = 0;
396251881Speter  result.flags |= DB_DBT_USERMEM;
397251881Speter
398251881Speter  svn_fs_base__trail_debug(trail, "strings", "put");
399251881Speter  return BDB_WRAP(fs, N_("storing empty contents"),
400251881Speter                  bfd->strings->put(bfd->strings, trail->db_txn,
401251881Speter                                    &query, &result, 0));
402251881Speter}
403251881Speter
404251881Speter
405251881Spetersvn_error_t *
406251881Spetersvn_fs_bdb__string_size(svn_filesize_t *size,
407251881Speter                        svn_fs_t *fs,
408251881Speter                        const char *key,
409251881Speter                        trail_t *trail,
410251881Speter                        apr_pool_t *pool)
411251881Speter{
412251881Speter  int db_err;
413251881Speter  DBT query;
414251881Speter  DBC *cursor;
415251881Speter  apr_size_t length;
416251881Speter  svn_filesize_t total;
417251881Speter
418251881Speter  svn_fs_base__str_to_dbt(&query, key);
419251881Speter
420251881Speter  SVN_ERR(locate_key(&length, &cursor, &query, fs, trail, pool));
421251881Speter
422251881Speter  total = length;
423251881Speter  while (1)
424251881Speter    {
425251881Speter      /* Remember, if any error happens, our cursor has been closed
426251881Speter         for us. */
427251881Speter      db_err = get_next_length(&length, cursor, &query);
428251881Speter
429251881Speter      /* No more records? Then return the total length. */
430251881Speter      if (db_err == DB_NOTFOUND)
431251881Speter        {
432251881Speter          *size = total;
433251881Speter          return SVN_NO_ERROR;
434251881Speter        }
435251881Speter      if (db_err)
436251881Speter        return BDB_WRAP(fs, N_("fetching string length"), db_err);
437251881Speter
438251881Speter      total += length;
439251881Speter    }
440251881Speter
441251881Speter  /* NOTREACHED */
442251881Speter}
443251881Speter
444251881Speter
445251881Spetersvn_error_t *
446251881Spetersvn_fs_bdb__string_delete(svn_fs_t *fs,
447251881Speter                          const char *key,
448251881Speter                          trail_t *trail,
449251881Speter                          apr_pool_t *pool)
450251881Speter{
451251881Speter  base_fs_data_t *bfd = fs->fsap_data;
452251881Speter  int db_err;
453251881Speter  DBT query;
454251881Speter
455251881Speter  svn_fs_base__trail_debug(trail, "strings", "del");
456251881Speter  db_err = bfd->strings->del(bfd->strings, trail->db_txn,
457251881Speter                             svn_fs_base__str_to_dbt(&query, key), 0);
458251881Speter
459251881Speter  /* If there's no such node, return an appropriately specific error.  */
460251881Speter  if (db_err == DB_NOTFOUND)
461251881Speter    return svn_error_createf
462251881Speter      (SVN_ERR_FS_NO_SUCH_STRING, 0,
463251881Speter       "No such string '%s'", key);
464251881Speter
465251881Speter  /* Handle any other error conditions.  */
466251881Speter  return BDB_WRAP(fs, N_("deleting string"), db_err);
467251881Speter}
468251881Speter
469251881Speter
470251881Spetersvn_error_t *
471251881Spetersvn_fs_bdb__string_copy(svn_fs_t *fs,
472251881Speter                        const char **new_key,
473251881Speter                        const char *key,
474251881Speter                        trail_t *trail,
475251881Speter                        apr_pool_t *pool)
476251881Speter{
477251881Speter  base_fs_data_t *bfd = fs->fsap_data;
478251881Speter  DBT query;
479251881Speter  DBT result;
480251881Speter  DBT copykey;
481251881Speter  DBC *cursor;
482251881Speter  int db_err;
483251881Speter
484251881Speter  /* Copy off the old key in case the caller is sharing storage
485251881Speter     between the old and new keys. */
486251881Speter  const char *old_key = apr_pstrdup(pool, key);
487251881Speter
488251881Speter  SVN_ERR(get_key_and_bump(fs, new_key, trail, pool));
489251881Speter
490251881Speter  svn_fs_base__trail_debug(trail, "strings", "cursor");
491251881Speter  SVN_ERR(BDB_WRAP(fs, N_("creating cursor for reading a string"),
492251881Speter                   bfd->strings->cursor(bfd->strings, trail->db_txn,
493251881Speter                                        &cursor, 0)));
494251881Speter
495251881Speter  svn_fs_base__str_to_dbt(&query, old_key);
496251881Speter  svn_fs_base__str_to_dbt(&copykey, *new_key);
497251881Speter
498251881Speter  svn_fs_base__clear_dbt(&result);
499251881Speter
500251881Speter  /* Move to the first record and fetch its data (under BDB's mem mgmt). */
501251881Speter  db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_SET);
502251881Speter  if (db_err)
503251881Speter    {
504251881Speter      svn_bdb_dbc_close(cursor);
505251881Speter      return BDB_WRAP(fs, N_("getting next-key value"), db_err);
506251881Speter    }
507251881Speter
508251881Speter  while (1)
509251881Speter    {
510251881Speter      /* ### can we pass a BDB-provided buffer to another BDB function?
511251881Speter         ### they are supposed to have a duration up to certain points
512251881Speter         ### of calling back into BDB, but I'm not sure what the exact
513251881Speter         ### rules are. it is definitely nicer to use BDB buffers here
514251881Speter         ### to simplify things and reduce copies, but... hrm.
515251881Speter      */
516251881Speter
517251881Speter      /* Write the data to the database */
518251881Speter      svn_fs_base__trail_debug(trail, "strings", "put");
519251881Speter      db_err = bfd->strings->put(bfd->strings, trail->db_txn,
520251881Speter                                 &copykey, &result, 0);
521251881Speter      if (db_err)
522251881Speter        {
523251881Speter          svn_bdb_dbc_close(cursor);
524251881Speter          return BDB_WRAP(fs, N_("writing copied data"), db_err);
525251881Speter        }
526251881Speter
527251881Speter      /* Read the next chunk. Terminate loop if we're done. */
528251881Speter      svn_fs_base__clear_dbt(&result);
529251881Speter      db_err = svn_bdb_dbc_get(cursor, &query, &result, DB_NEXT_DUP);
530251881Speter      if (db_err == DB_NOTFOUND)
531251881Speter        break;
532251881Speter      if (db_err)
533251881Speter        {
534251881Speter          svn_bdb_dbc_close(cursor);
535251881Speter          return BDB_WRAP(fs, N_("fetching string data for a copy"), db_err);
536251881Speter        }
537251881Speter    }
538251881Speter
539251881Speter  return BDB_WRAP(fs, N_("closing string-reading cursor"),
540251881Speter                  svn_bdb_dbc_close(cursor));
541251881Speter}
542