Skip to content

Commit e478563

Browse files
authored
add export log for Turso databases (#1056)
Just issues an export command to the server for the tursodb log
2 parents 58dc630 + 77a0cc5 commit e478563

2 files changed

Lines changed: 57 additions & 9 deletions

File tree

internal/cmd/db_export.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ var outputFile string
1313

1414
var exportCmd = &cobra.Command{
1515
Use: "export <database>",
16-
Short: "Export a database snapshot and WAL from Turso to SQLite files.",
17-
Long: `Export a database snapshot and WAL from Turso to SQLite files.
16+
Short: "Export a database snapshot and its log from Turso to local files.",
17+
Long: `Export a database snapshot and its log from Turso to local files.
1818
1919
This command exports a snapshot of the current generation of a Turso database
20-
to a local SQLite file, along with any WAL (Write-Ahead Log) frames. The WAL
21-
file will be saved as <database>.db-wal alongside the main database file.`,
20+
to a local database file. For SQLite-backed
21+
databases the WAL (Write-Ahead Log) frames are saved as <database>.db-wal;
22+
for tursodb databases the logical log is saved as <database>.db-log alongside
23+
the main database file.`,
2224
Args: cobra.ExactArgs(1),
2325
RunE: func(cmd *cobra.Command, args []string) error {
2426
cmd.SilenceUsage = true

internal/turso/tursoServer.go

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,8 @@ func (i *TursoServerClient) finalizeUpload(uploadID string) error {
442442
}
443443

444444
type ExportInfo struct {
445-
CurrentGeneration int `json:"current_generation"`
445+
CurrentGeneration int `json:"current_generation"`
446+
DbType string `json:"db_type"`
446447
}
447448

448449
const EncryptionKeyHeader = "x-turso-encryption-key"
@@ -484,11 +485,23 @@ func (i *TursoServerClient) Export(outputFile string, withMetadata bool, remoteE
484485
return fmt.Errorf("failed to write export to file: %w", err)
485486
}
486487

487-
lastFrameNo, err := i.ExportWAL(outputFile, &info, remoteEncryptionKey)
488-
if err != nil {
489-
return fmt.Errorf("failed to export WAL: %w", err)
488+
lastFrameNo := 0
489+
if info.DbType == "tursodb" {
490+
// tursodb databases persist writes in a logical log rather than a
491+
// SQLite WAL: fetch the canonical log the server assembles from its
492+
// durable log fragments.
493+
if err := i.ExportLog(outputFile, &info, remoteEncryptionKey); err != nil {
494+
return fmt.Errorf("failed to export logical log: %w", err)
495+
}
496+
} else {
497+
lastFrameNo, err = i.ExportWAL(outputFile, &info, remoteEncryptionKey)
498+
if err != nil {
499+
return fmt.Errorf("failed to export WAL: %w", err)
500+
}
490501
}
491-
if withMetadata {
502+
if withMetadata && info.DbType == "tursodb" {
503+
fmt.Fprintln(os.Stderr, "Warning: --with-metadata is not supported for tursodb databases and will be ignored.")
504+
} else if withMetadata {
492505
if err := i.ExportMetadata(outputFile, &info, lastFrameNo); err != nil {
493506
return fmt.Errorf("failed to export metadata: %w", err)
494507
}
@@ -497,6 +510,39 @@ func (i *TursoServerClient) Export(outputFile string, withMetadata bool, remoteE
497510
return nil
498511
}
499512

513+
// ExportLog downloads the canonical tursodb logical log for the generation
514+
// into <outputFile>-log. The server assembles it from its durable log
515+
// fragments with the same last-attempt-wins chain walk its own recovery
516+
// uses, so the file matches the .db-log a server would rebuild for this
517+
// generation.
518+
func (i *TursoServerClient) ExportLog(outputFile string, info *ExportInfo, remoteEncryptionKey string) error {
519+
headers := map[string]string{}
520+
if remoteEncryptionKey != "" {
521+
headers[EncryptionKeyHeader] = remoteEncryptionKey
522+
}
523+
logRes, err := i.client.GetWithHeaders(fmt.Sprintf("/export/%d/log", info.CurrentGeneration), nil, headers)
524+
if err != nil {
525+
return fmt.Errorf("failed to fetch logical log: %w", err)
526+
}
527+
defer logRes.Body.Close()
528+
if logRes.StatusCode != http.StatusOK {
529+
return parseResponseError(logRes)
530+
}
531+
532+
out, err := os.Create(outputFile + "-log")
533+
if err != nil {
534+
return fmt.Errorf("failed to create log file: %w", err)
535+
}
536+
defer out.Close()
537+
if _, err := io.Copy(out, logRes.Body); err != nil {
538+
return fmt.Errorf("failed to write logical log to file: %w", err)
539+
}
540+
if err := out.Sync(); err != nil {
541+
return fmt.Errorf("failed to sync log file: %w", err)
542+
}
543+
return nil
544+
}
545+
500546
func (i *TursoServerClient) ExportWAL(outputFile string, info *ExportInfo, remoteEncryptionKey string) (int, error) {
501547
walFile := outputFile + "-wal"
502548
walOut, err := os.Create(walFile)

0 commit comments

Comments
 (0)