From eca27dcd4f5ccdc4ad24365ac9bf2fe2c9bbcb11 Mon Sep 17 00:00:00 2001 From: mappu Date: Thu, 18 Jul 2024 17:46:13 +1200 Subject: [PATCH] sqlite: basic editing support --- db_sqlite.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/db_sqlite.go b/db_sqlite.go index 68f3065..be23f4e 100644 --- a/db_sqlite.go +++ b/db_sqlite.go @@ -1,10 +1,12 @@ package main import ( + "context" "database/sql" "errors" "fmt" "path/filepath" + "strings" "unsafe" _ "yvbolt/sqliteclidriver" @@ -187,6 +189,127 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringG return nil } +func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) (retErr error) { + + if len(ndata.bucketPath) != 2 { + return errors.New("invalid selection") + } + tableName := ndata.bucketPath[1] + + // We have rendered row IDs, need to convert back to an SQLite primary key + // TODO stash the real key inside f.contentBox.Objects() + // FIXME breaks if you try and edit the primary key(!) + + ctx := context.Background() + + tx, err := n.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + var commitOK bool = false + defer func() { + if !commitOK { + err := tx.Rollback() + if err != nil { + retErr = err + } + } + }() + + // Data grid properties + var columnNames []string + for i := int32(0); i < f.contentBox.ColCount(); i++ { + columnNames = append(columnNames, f.contentBox.Columns().Items(i).Title().Caption()) + } + + // Query sqlite table metadata to determine which of these is the PRIMARY KEY + var primaryColumnName string + err = tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName) + if err != nil { + return fmt.Errorf("Finding primary key for update: %w", err) + } + + // Convert it to an index + var primaryColumnIdx int = -1 + for i := 0; i < len(columnNames); i++ { + if columnNames[i] == primaryColumnName { + primaryColumnIdx = i + break + } + } + if primaryColumnIdx == -1 { + return fmt.Errorf("Primary key %q missing from available columns", primaryColumnName) + } + + // SQLite can only LIMIT 1 on update/delete if it was compiled with + // SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn + // cgo library + // Skip that, and just rely on primary key uniqueness + + // Edit + for rowid, editcells := range f.updateRows { + stmt := `UPDATE "` + tableName + `" SET ` + params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) + + for ct, cell := range editcells { + if ct > 0 { + stmt += `, ` + } + stmt += `"` + columnNames[cell] + `" = ?` + params = append(params, f.contentBox.Cells(cell, rowid)) + } + stmt += ` WHERE "` + primaryColumnName + `" = ?` + pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid) + params = append(params, pkVal) + + _, err = tx.ExecContext(ctx, stmt, params...) + if err != nil { + return fmt.Errorf("Updating row %q: %w", pkVal, err) + } + } + + // Delete by key (affects rowids after re-render) + for rowid, _ := range f.deleteRows { + pkVal := f.contentBox.Cells(int32(primaryColumnIdx), rowid) + + stmt := `DELETE FROM "` + tableName + `" WHERE "` + primaryColumnName + `" = ?` + + _, err = tx.ExecContext(ctx, stmt, pkVal) + if err != nil { + return fmt.Errorf("Deleting row %q: %w", pkVal, err) + } + } + + // Insert all new entries + for rowid, _ := range f.insertRows { + stmt := `INSERT INTO "` + tableName + `" (` + strings.Join(columnNames, `, `) + `) VALUES (` + params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind) + + for colid := 0; colid < len(columnNames); colid++ { + if colid > 0 { + stmt += `, ` + } + stmt += "?" + params = append(params, f.contentBox.Cells(int32(colid), rowid)) + } + stmt += `)` + + _, err = tx.ExecContext(ctx, stmt, params...) + if err != nil { + return fmt.Errorf("Inserting row: %w", err) + } + } + + err = tx.Commit() + if err != nil { + return err + } + + commitOK = true // no need for rollback + return nil +} + func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { if len(ndata.bucketPath) == 0 {