...
Tawesoft Logo

Source file src/tawesoft.co.uk/go/sqlp/db.go

Documentation: src/tawesoft.co.uk/go/sqlp/db.go

     1  package sqlp
     2  
     3  import (
     4      "context"
     5      "database/sql"
     6      "fmt"
     7      "strings"
     8      "syscall"
     9  )
    10  
    11  // Queryable is an interface describing the intersection of the methods
    12  // implemented by sql.DB, sql.Tx, and sql.Stmt.
    13  type Queryable interface {
    14      Exec(query string, args ...interface{}) (sql.Result, error)
    15      ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
    16      Query(query string, args ...interface{}) (*sql.Rows, error)
    17      QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    18      QueryRow(query string, args ...interface{}) *sql.Row
    19      QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
    20  }
    21  
    22  // Transactionable is an interface describing a Queryable that can start
    23  // (possibly nested) transactions. A sql.Tx is not transactable, but a sql.DB
    24  // is.
    25  type Transactionable interface {
    26      Queryable
    27      Begin() (*sql.Tx, error)
    28      BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
    29  }
    30  
    31  // Features are a common set of features with driver-specific implementations.
    32  // e.g. `import tawesoft.co.uk/go/sqlp/sqlite3` and use sqlite3.Features
    33  type Features struct {
    34      EscapeIdentifier func(s string) (string, error)
    35      EscapeString func(s string) (string, error)
    36      IsUniqueConstraintError func(err error) bool
    37  }
    38  
    39  // IsUniqueConstraintError returns true iff the database driver implements a
    40  // check for unique constraint errors and the error indicates a statement
    41  // did not execute because it would violate a uniqueness constraint e.g. when
    42  // attempting to insert a duplicate item.
    43  func IsUniqueConstraintError(f *Features, err error) bool {
    44      if f.IsUniqueConstraintError == nil { return false }
    45      return f.IsUniqueConstraintError(err)
    46  }
    47  
    48  // EscapeIdentifier returns a SQL identifier (such as a column name) for a
    49  // given database driver.
    50  func EscapeIdentifier(f *Features, s string) (string, error) {
    51      if f.EscapeIdentifier == nil { return s, fmt.Errorf("not implemented") }
    52      return f.EscapeIdentifier(s)
    53  }
    54  
    55  // EscapeString returns a quoted string literal for a given database driver.
    56  func EscapeString(f *Features, s string) (string, error) {
    57      if f.EscapeString == nil { return s, fmt.Errorf("not implemented") }
    58      return f.EscapeString(s)
    59  }
    60  
    61  // OpenMode wraps a database opener (e.g. a sqlite3.Opener) with a syscall to
    62  // set the file permissions to a unix mode when the file is created (e.g. mode
    63  // 0600 for user read/write only) and, additionally, checks the connection using
    64  // db.Ping().
    65  //
    66  // Note - NOT safe to be used concurrently with other I/O due to use of syscall
    67  func OpenMode(
    68      driverName string,
    69      dataSource string,
    70      mode int,
    71      Opener func(string, string) (*sql.DB, error),
    72  ) (*sql.DB, error) {
    73      oldMask := syscall.Umask(mode)
    74      defer syscall.Umask(oldMask)
    75      
    76      db, err := Opener(driverName, dataSource)
    77      if err != nil {
    78          return nil, fmt.Errorf("error opening %s database %s: %+v",
    79              driverName, dataSource, err)
    80      }
    81      
    82      // For a file data source, ensure creation.
    83      // For a network source, ensure connection.
    84      err = db.Ping()
    85      if err != nil {
    86          db.Close()
    87          return nil, fmt.Errorf("error connecting to %s database %s: %+v",
    88              driverName, dataSource, err)
    89      }
    90      
    91      return db, nil
    92  }
    93  
    94  // RowsAffectedBetween returns true iff the result rows affected is not an
    95  // error and falls between min and max (inclusive). Otherwise, returns false and
    96  // the first argument is the number of rows actually affected.
    97  //
    98  // Rarely, returns -1 and false if there was an error counting how many rows
    99  // were affected (which should only ever happen if there is a bug in your code
   100  // e.g. trying to count rows affected by a DDL command (such as a CREATE TABLE)
   101  // or not checking a previous error and using an invalid result).
   102  func RowsAffectedBetween(result sql.Result, min int, max int) (int64, bool) {
   103      affected, err := result.RowsAffected()
   104      if err != nil { return -1, false }
   105      if affected < int64(min) { return affected, false }
   106      if affected > int64(max) { return affected, false }
   107      return 0, true
   108  }
   109  
   110  // RepeatString returns a repeating, comma-separted string of `n` instances of
   111  // `x`, for building queries with multiple arguments.
   112  //
   113  // e.g. RepeatString('?', 3) returns "?, ?, ?"
   114  func RepeatString(x string, n int) string {
   115      if n <= 0 { return "" }
   116      return strings.Repeat(x + ", ", n - 1) + " " + x
   117  }
   118  

View as plain text