sqlite: basic editing support

This commit is contained in:
mappu 2024-07-18 17:46:13 +12:00
parent 0f2a3e021a
commit eca27dcd4f

View File

@ -1,10 +1,12 @@
package main package main
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"unsafe" "unsafe"
_ "yvbolt/sqliteclidriver" _ "yvbolt/sqliteclidriver"
@ -187,6 +189,127 @@ func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringG
return nil 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) { func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
if len(ndata.bucketPath) == 0 { if len(ndata.bucketPath) == 0 {