sqlite: basic editing support
This commit is contained in:
parent
0f2a3e021a
commit
eca27dcd4f
123
db_sqlite.go
123
db_sqlite.go
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user