mirror of https://github.com/status-im/fathom.git
first stab at supporting postgres driver. #9
This commit is contained in:
parent
db170276fb
commit
46f954cc6e
9
ana.go
9
ana.go
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"github.com/dannyvankooten/ana/commands"
|
||||
"github.com/dannyvankooten/ana/count"
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/robfig/cron"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
|
@ -32,11 +32,8 @@ func main() {
|
|||
}
|
||||
|
||||
// setup database connection
|
||||
conn, err := db.SetupDatabaseConnection()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
db := datastore.Init()
|
||||
defer db.Close()
|
||||
|
||||
// setup cron to run count.Archive every hour
|
||||
c := cron.New()
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
"github.com/dannyvankooten/ana/models"
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -26,7 +26,7 @@ var LoginHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)
|
|||
json.NewDecoder(r.Body).Decode(&l)
|
||||
var hashedPassword string
|
||||
var u models.User
|
||||
stmt, _ := db.Conn.Prepare("SELECT id, email, password FROM users WHERE email = ? LIMIT 1")
|
||||
stmt, _ := datastore.DB.Prepare("SELECT id, email, password FROM users WHERE email = ? LIMIT 1")
|
||||
err := stmt.QueryRow(l.Email).Scan(&u.ID, &u.Email, &hashedPassword)
|
||||
|
||||
// compare pwd
|
||||
|
@ -69,7 +69,7 @@ func Authorize(next http.Handler) http.Handler {
|
|||
|
||||
// find user
|
||||
var u models.User
|
||||
stmt, _ := db.Conn.Prepare("SELECT id, email FROM users WHERE id = ? LIMIT 1")
|
||||
stmt, _ := datastore.DB.Prepare("SELECT id, email FROM users WHERE id = ? LIMIT 1")
|
||||
err := stmt.QueryRow(userID).Scan(&u.ID, &u.Email)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
"github.com/dannyvankooten/ana/models"
|
||||
"github.com/mssola/user_agent"
|
||||
)
|
||||
|
@ -40,12 +40,12 @@ func CollectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Title: q.Get("t"),
|
||||
Hostname: q.Get("h"),
|
||||
}
|
||||
stmt, _ := db.Conn.Prepare("SELECT p.id FROM pages p WHERE p.hostname = ? AND p.path = ? LIMIT 1")
|
||||
stmt, _ := datastore.DB.Prepare("SELECT p.id FROM pages p WHERE p.hostname = ? AND p.path = ? LIMIT 1")
|
||||
defer stmt.Close()
|
||||
err := stmt.QueryRow(page.Hostname, page.Path).Scan(&page.ID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
page.Save(db.Conn)
|
||||
page.Save(datastore.DB)
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -69,12 +69,12 @@ func CollectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// query by unique visitor key
|
||||
visitor.Key = visitor.GenerateKey(now.Format("2006-01-02"), visitor.IpAddress, r.UserAgent())
|
||||
|
||||
stmt, _ = db.Conn.Prepare("SELECT v.id FROM visitors v WHERE v.visitor_key = ? LIMIT 1")
|
||||
stmt, _ = datastore.DB.Prepare("SELECT v.id FROM visitors v WHERE v.visitor_key = ? LIMIT 1")
|
||||
defer stmt.Close()
|
||||
err = stmt.QueryRow(visitor.Key).Scan(&visitor.ID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
err = visitor.Save(db.Conn)
|
||||
err = visitor.Save(datastore.DB)
|
||||
checkError(err)
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
|
@ -94,7 +94,7 @@ func CollectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
pageview.ReferrerUrl = ""
|
||||
}
|
||||
|
||||
err = pageview.Save(db.Conn)
|
||||
err = pageview.Save(datastore.DB)
|
||||
checkError(err)
|
||||
|
||||
// don't you cache this
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/dannyvankooten/ana/count"
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
type pageviews struct {
|
||||
|
@ -18,7 +18,7 @@ type pageviews struct {
|
|||
var GetPageviewsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
before, after := getRequestedPeriods(r)
|
||||
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
p.hostname,
|
||||
p.path,
|
||||
|
|
|
@ -2,12 +2,12 @@ package commands
|
|||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/count"
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// Seed creates n database records with dummy data
|
||||
func Seed(n int) {
|
||||
db.Seed(n)
|
||||
datastore.Seed(n)
|
||||
}
|
||||
|
||||
// Archive processes unarchived data (pageviews to aggeegated count tables)
|
||||
|
|
|
@ -2,7 +2,7 @@ package commands
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
"github.com/dannyvankooten/ana/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -14,7 +14,7 @@ func Register(email string, password string) {
|
|||
Email: email,
|
||||
Password: string(hash),
|
||||
}
|
||||
user.Save(db.Conn)
|
||||
user.Save(datastore.DB)
|
||||
|
||||
fmt.Printf("User %s #%d created.\n", email, user.ID)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package count
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// TotalUniqueBrowsers returns the total # of unique browsers between two given timestamps
|
||||
func TotalUniqueBrowsers(before int64, after int64) int {
|
||||
var total int
|
||||
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
IFNULL( SUM(t.count_unique), 0 )
|
||||
FROM total_browser_names t
|
||||
|
@ -24,7 +24,7 @@ func TotalUniqueBrowsers(before int64, after int64) int {
|
|||
|
||||
// Browsers returns a point slice containing browser data per browser name
|
||||
func Browsers(before int64, after int64, limit int) []Point {
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
t.value,
|
||||
SUM(t.count_unique) AS count
|
||||
|
|
|
@ -5,8 +5,7 @@ import (
|
|||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/options"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// Total represents a daily aggregated total for a metric
|
||||
|
@ -27,7 +26,7 @@ type Point struct {
|
|||
}
|
||||
|
||||
func getLastArchivedDate() string {
|
||||
value := options.Get("last_archived")
|
||||
value := datastore.GetOption("last_archived")
|
||||
return value
|
||||
}
|
||||
|
||||
|
@ -42,13 +41,13 @@ func Archive() {
|
|||
CreateBrowserTotals(lastArchived)
|
||||
CreateReferrerTotals(lastArchived)
|
||||
|
||||
err := options.Set("last_archived", time.Now().Format("2006-01-02"))
|
||||
err := datastore.SetOption("last_archived", time.Now().Format("2006-01-02"))
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
// Save the Total in the given database connection + table
|
||||
func (t *Total) Save(Conn *sql.DB, table string) error {
|
||||
stmt, err := db.Conn.Prepare(`INSERT INTO ` + table + `(
|
||||
stmt, err := datastore.DB.Prepare(`INSERT INTO ` + table + `(
|
||||
value,
|
||||
count,
|
||||
count_unique,
|
||||
|
@ -136,7 +135,7 @@ func fill(start int64, end int64, points []Point) []Point {
|
|||
}
|
||||
|
||||
func queryTotalRows(sql string, lastArchived string) *sql.Rows {
|
||||
stmt, err := db.Conn.Prepare(sql)
|
||||
stmt, err := datastore.DB.Prepare(sql)
|
||||
checkError(err)
|
||||
defer stmt.Close()
|
||||
|
||||
|
@ -146,14 +145,14 @@ func queryTotalRows(sql string, lastArchived string) *sql.Rows {
|
|||
}
|
||||
|
||||
func processTotalRows(rows *sql.Rows, table string) {
|
||||
db.Conn.Exec("START TRANSACTION")
|
||||
datastore.DB.Exec("START TRANSACTION")
|
||||
for rows.Next() {
|
||||
var t Total
|
||||
err := rows.Scan(&t.Value, &t.Count, &t.CountUnique, &t.Date)
|
||||
checkError(err)
|
||||
t.Save(db.Conn, table)
|
||||
t.Save(datastore.DB, table)
|
||||
}
|
||||
db.Conn.Exec("COMMIT")
|
||||
datastore.DB.Exec("COMMIT")
|
||||
|
||||
rows.Close()
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package count
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// TotalUniqueLanguages returns the total # of unique browser languages between two given timestamps
|
||||
func TotalUniqueLanguages(before int64, after int64) int {
|
||||
var total int
|
||||
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
IFNULL( SUM(t.count_unique), 0 )
|
||||
FROM total_browser_languages t
|
||||
|
@ -25,7 +25,7 @@ func TotalUniqueLanguages(before int64, after int64) int {
|
|||
// Languages returns a point slice containing language data per language
|
||||
func Languages(before int64, after int64, limit int) []Point {
|
||||
// TODO: Calculate total instead of requiring it as a parameter.
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
t.value,
|
||||
SUM(t.count_unique) AS count
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package count
|
||||
|
||||
import "github.com/dannyvankooten/ana/db"
|
||||
import "github.com/dannyvankooten/ana/datastore"
|
||||
|
||||
// Pageviews returns the total number of pageviews between the given timestamps
|
||||
func Pageviews(before int64, after int64) int {
|
||||
var total int
|
||||
|
||||
// get total
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
IFNULL( SUM(t.count), 0 )
|
||||
FROM total_pageviews t
|
||||
|
@ -21,7 +21,7 @@ func Pageviews(before int64, after int64) int {
|
|||
|
||||
// PageviewsPerDay returns a slice of data points representing the number of pageviews per day
|
||||
func PageviewsPerDay(before int64, after int64) []Point {
|
||||
stmt, err := db.Conn.Prepare(`SELECT
|
||||
stmt, err := datastore.DB.Prepare(`SELECT
|
||||
SUM(t.count) AS count,
|
||||
DATE_FORMAT(t.date, '%Y-%m-%d') AS date_group
|
||||
FROM total_pageviews t
|
||||
|
@ -49,7 +49,7 @@ func PageviewsPerDay(before int64, after int64) []Point {
|
|||
|
||||
// CreatePageviewTotals aggregates pageview data for each page into daily totals
|
||||
func CreatePageviewTotals(since string) {
|
||||
stmt, err := db.Conn.Prepare(`SELECT
|
||||
stmt, err := datastore.DB.Prepare(`SELECT
|
||||
pv.page_id,
|
||||
COUNT(*) AS count,
|
||||
COUNT(DISTINCT(pv.visitor_id)) AS count_unique,
|
||||
|
@ -64,15 +64,15 @@ func CreatePageviewTotals(since string) {
|
|||
checkError(err)
|
||||
defer rows.Close()
|
||||
|
||||
db.Conn.Begin()
|
||||
datastore.DB.Begin()
|
||||
|
||||
db.Conn.Exec("START TRANSACTION")
|
||||
datastore.DB.Exec("START TRANSACTION")
|
||||
for rows.Next() {
|
||||
var t Total
|
||||
err = rows.Scan(&t.PageID, &t.Count, &t.CountUnique, &t.Date)
|
||||
checkError(err)
|
||||
|
||||
stmt, err := db.Conn.Prepare(`INSERT INTO total_pageviews(
|
||||
stmt, err := datastore.DB.Prepare(`INSERT INTO total_pageviews(
|
||||
page_id,
|
||||
count,
|
||||
count_unique,
|
||||
|
@ -91,5 +91,5 @@ func CreatePageviewTotals(since string) {
|
|||
)
|
||||
checkError(err)
|
||||
}
|
||||
db.Conn.Exec("COMMIT")
|
||||
datastore.DB.Exec("COMMIT")
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package count
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// TotalReferrers returns the total # of referrers between two given timestamps
|
||||
func TotalReferrers(before int64, after int64) int {
|
||||
var total int
|
||||
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
IFNULL( SUM(t.count), 0 )
|
||||
FROM total_referrers t
|
||||
|
@ -24,7 +24,7 @@ func TotalReferrers(before int64, after int64) int {
|
|||
|
||||
// Referrers returns a point slice containing browser data per browser name
|
||||
func Referrers(before int64, after int64, limit int) []Point {
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
t.value,
|
||||
SUM(t.count) AS count
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package count
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// TotalUniqueScreens returns the total # of screens between two given timestamps
|
||||
func TotalUniqueScreens(before int64, after int64) int {
|
||||
var total int
|
||||
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
IFNULL( SUM(t.count_unique), 0 )
|
||||
FROM total_screens t
|
||||
|
@ -24,7 +24,7 @@ func TotalUniqueScreens(before int64, after int64) int {
|
|||
|
||||
// Screens returns a point slice containing screen data per size
|
||||
func Screens(before int64, after int64, limit int) []Point {
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
t.value,
|
||||
SUM(t.count_unique) AS count
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package count
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
"github.com/dannyvankooten/ana/datastore"
|
||||
)
|
||||
|
||||
// RealtimeVisitors returns the total number of visitors in the last 3 minutes
|
||||
func RealtimeVisitors() int {
|
||||
var result int
|
||||
db.Conn.QueryRow(`
|
||||
datastore.DB.QueryRow(`
|
||||
SELECT COUNT(DISTINCT(pv.visitor_id))
|
||||
FROM pageviews pv
|
||||
WHERE pv.timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE) AND pv.timestamp <= CURRENT_TIMESTAMP`).Scan(&result)
|
||||
|
@ -17,7 +17,7 @@ func RealtimeVisitors() int {
|
|||
// Visitors returns the number of total visitors between the given timestamps
|
||||
func Visitors(before int64, after int64) int {
|
||||
// get total
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
SUM(t.count)
|
||||
FROM total_visitors t
|
||||
|
@ -31,7 +31,7 @@ func Visitors(before int64, after int64) int {
|
|||
|
||||
// VisitorsPerDay returns a point slice containing visitor data per day
|
||||
func VisitorsPerDay(before int64, after int64) []Point {
|
||||
stmt, err := db.Conn.Prepare(`SELECT
|
||||
stmt, err := datastore.DB.Prepare(`SELECT
|
||||
SUM(t.count) AS count,
|
||||
DATE_FORMAT(t.date, '%Y-%m-%d') AS date_group
|
||||
FROM total_visitors t
|
||||
|
@ -59,7 +59,7 @@ func VisitorsPerDay(before int64, after int64) []Point {
|
|||
|
||||
// CreateVisitorTotals aggregates visitor data into daily totals
|
||||
func CreateVisitorTotals(since string) {
|
||||
stmt, err := db.Conn.Prepare(`
|
||||
stmt, err := datastore.DB.Prepare(`
|
||||
SELECT
|
||||
COUNT(DISTINCT(pv.visitor_id)) AS count,
|
||||
DATE_FORMAT(pv.timestamp, "%Y-%m-%d") AS date_group
|
||||
|
@ -73,13 +73,13 @@ func CreateVisitorTotals(since string) {
|
|||
checkError(err)
|
||||
defer rows.Close()
|
||||
|
||||
db.Conn.Exec("START TRANSACTION")
|
||||
datastore.DB.Exec("START TRANSACTION")
|
||||
for rows.Next() {
|
||||
var t Total
|
||||
err = rows.Scan(&t.Count, &t.Date)
|
||||
checkError(err)
|
||||
|
||||
stmt, err := db.Conn.Prepare(`INSERT INTO total_visitors(
|
||||
stmt, err := datastore.DB.Prepare(`INSERT INTO total_visitors(
|
||||
count,
|
||||
date
|
||||
) VALUES( ?, ? ) ON DUPLICATE KEY UPDATE count = ?`)
|
||||
|
@ -93,5 +93,5 @@ func CreateVisitorTotals(since string) {
|
|||
)
|
||||
checkError(err)
|
||||
}
|
||||
db.Conn.Exec("COMMIT")
|
||||
datastore.DB.Exec("COMMIT")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package datastore
|
||||
|
||||
// GetOption returns an option value by its name
|
||||
func GetOption(name string) string {
|
||||
var value string
|
||||
|
||||
stmt, _ := DB.Prepare(`SELECT o.value FROM options o WHERE o.name = ? LIMIT 1`)
|
||||
defer stmt.Close()
|
||||
stmt.QueryRow(name).Scan(&value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// SetOption updates an option by its name
|
||||
func SetOption(name string, value string) error {
|
||||
stmt, err := DB.Prepare(`INSERT INTO options(name, value) VALUES(?, ?) ON DUPLICATE KEY UPDATE value = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(name, value, value)
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package options
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,4 +1,4 @@
|
|||
package db
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -57,28 +57,28 @@ func seedPages() []models.Page {
|
|||
Path: "/",
|
||||
Title: "Homepage",
|
||||
}
|
||||
homepage.Save(Conn)
|
||||
homepage.Save(DB)
|
||||
|
||||
contactPage := models.Page{
|
||||
Hostname: "wordpress.dev",
|
||||
Path: "/contact/",
|
||||
Title: "Contact",
|
||||
}
|
||||
contactPage.Save(Conn)
|
||||
contactPage.Save(DB)
|
||||
|
||||
aboutPage := models.Page{
|
||||
Hostname: "wordpress.dev",
|
||||
Path: "/about/",
|
||||
Title: "About Me",
|
||||
}
|
||||
aboutPage.Save(Conn)
|
||||
aboutPage.Save(DB)
|
||||
|
||||
portfolioPage := models.Page{
|
||||
Hostname: "wordpress.dev",
|
||||
Path: "/portfolio/",
|
||||
Title: "Portfolio",
|
||||
}
|
||||
portfolioPage.Save(Conn)
|
||||
portfolioPage.Save(DB)
|
||||
|
||||
pages = append(pages, homepage)
|
||||
pages = append(pages, homepage)
|
||||
|
@ -88,10 +88,11 @@ func seedPages() []models.Page {
|
|||
return pages
|
||||
}
|
||||
|
||||
// Seed inserts n random pageviews in the database.
|
||||
func Seed(n int) {
|
||||
pages := seedPages()
|
||||
|
||||
stmtVisitor, _ := Conn.Prepare("SELECT v.id FROM visitors v WHERE v.visitor_key = ? LIMIT 1")
|
||||
stmtVisitor, _ := DB.Prepare("SELECT v.id FROM visitors v WHERE v.visitor_key = ? LIMIT 1")
|
||||
defer stmtVisitor.Close()
|
||||
|
||||
// insert X random hits
|
||||
|
@ -116,7 +117,7 @@ func Seed(n int) {
|
|||
|
||||
err := stmtVisitor.QueryRow(visitor.Key).Scan(&visitor.ID)
|
||||
if err != nil {
|
||||
visitor.Save(Conn)
|
||||
visitor.Save(DB)
|
||||
}
|
||||
|
||||
// generate random timestamp
|
||||
|
@ -129,16 +130,16 @@ func Seed(n int) {
|
|||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
Conn.Exec("START TRANSACTION")
|
||||
DB.Exec("START TRANSACTION")
|
||||
|
||||
// insert between 1-4 pageviews for this visitor
|
||||
for j := 0; j <= randInt(1, 4); j++ {
|
||||
page := pages[randInt(0, len(pages))]
|
||||
pv.PageID = page.ID
|
||||
pv.Save(Conn)
|
||||
pv.Save(DB)
|
||||
}
|
||||
|
||||
Conn.Exec("COMMIT")
|
||||
DB.Exec("COMMIT")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package datastore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // mysql driver
|
||||
_ "github.com/lib/pq" // postgresql driver
|
||||
)
|
||||
|
||||
// DB ...
|
||||
var DB *sql.DB
|
||||
|
||||
// Init creates a database connection pool
|
||||
func Init() *sql.DB {
|
||||
driver := os.Getenv("ANA_DATABASE_DRIVER")
|
||||
if driver == "" {
|
||||
driver = "mysql"
|
||||
}
|
||||
|
||||
DB = New(driver, getDSN(driver))
|
||||
return DB
|
||||
}
|
||||
|
||||
// New creates a new database pool
|
||||
func New(driver string, config string) *sql.DB {
|
||||
db, err := sql.Open(driver, config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func getDSN(driver string) string {
|
||||
var dsn = fmt.Sprintf(
|
||||
"%s:%s@%s/%s",
|
||||
os.Getenv("ANA_DATABASE_USER"),
|
||||
os.Getenv("ANA_DATABASE_PASSWORD"),
|
||||
os.Getenv("ANA_DATABASE_HOST"),
|
||||
os.Getenv("ANA_DATABASE_NAME"),
|
||||
)
|
||||
|
||||
if driver == "postgres" {
|
||||
dsn = "postgres://" + dsn
|
||||
}
|
||||
|
||||
return dsn
|
||||
}
|
40
db/db.go
40
db/db.go
|
@ -1,40 +0,0 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
var Conn *sql.DB
|
||||
|
||||
func getDSN() string {
|
||||
var dsn = fmt.Sprintf(
|
||||
"%s:%s@%s/%s",
|
||||
os.Getenv("ANA_DATABASE_USER"),
|
||||
os.Getenv("ANA_DATABASE_PASSWORD"),
|
||||
os.Getenv("ANA_DATABASE_HOST"),
|
||||
os.Getenv("ANA_DATABASE_NAME"),
|
||||
)
|
||||
return dsn
|
||||
}
|
||||
|
||||
// SetupDatabaseConnection opens up & returns a SQL connection
|
||||
func SetupDatabaseConnection() (*sql.DB, error) {
|
||||
var err error
|
||||
|
||||
Conn, err = sql.Open("mysql", getDSN())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open doesn't open a connection right away. Validate DSN by calling Ping().
|
||||
err = Conn.Ping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Conn, nil
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package options
|
||||
|
||||
import (
|
||||
"github.com/dannyvankooten/ana/db"
|
||||
)
|
||||
|
||||
// Get returns an option value by its name
|
||||
func Get(name string) string {
|
||||
var value string
|
||||
|
||||
stmt, _ := db.Conn.Prepare(`SELECT o.value FROM options o WHERE o.name = ? LIMIT 1`)
|
||||
defer stmt.Close()
|
||||
stmt.QueryRow(name).Scan(&value)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Set updates an option by its name
|
||||
func Set(name string, value string) error {
|
||||
stmt, err := db.Conn.Prepare(`INSERT INTO options(name, value) VALUES(?, ?) ON DUPLICATE KEY UPDATE value = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(name, value, value)
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
## Contributing to pq
|
||||
|
||||
`pq` has a backlog of pull requests, but contributions are still very
|
||||
much welcome. You can help with patch review, submitting bug reports,
|
||||
or adding new functionality. There is no formal style guide, but
|
||||
please conform to the style of existing code and general Go formatting
|
||||
conventions when submitting patches.
|
||||
|
||||
### Patch review
|
||||
|
||||
Help review existing open pull requests by commenting on the code or
|
||||
proposed functionality.
|
||||
|
||||
### Bug reports
|
||||
|
||||
We appreciate any bug reports, but especially ones with self-contained
|
||||
(doesn't depend on code outside of pq), minimal (can't be simplified
|
||||
further) test cases. It's especially helpful if you can submit a pull
|
||||
request with just the failing test case (you'll probably want to
|
||||
pattern it after the tests in
|
||||
[conn_test.go](https://github.com/lib/pq/blob/master/conn_test.go).
|
||||
|
||||
### New functionality
|
||||
|
||||
There are a number of pending patches for new functionality, so
|
||||
additional feature patches will take a while to merge. Still, patches
|
||||
are generally reviewed based on usefulness and complexity in addition
|
||||
to time-in-queue, so if you have a knockout idea, take a shot. Feel
|
||||
free to open an issue discussion your proposed patch beforehand.
|
|
@ -0,0 +1,8 @@
|
|||
Copyright (c) 2011-2013, 'pq' Contributors
|
||||
Portions Copyright (C) 2011 Blake Mizerany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,105 @@
|
|||
# pq - A pure Go postgres driver for Go's database/sql package
|
||||
|
||||
[![Build Status](https://travis-ci.org/lib/pq.png?branch=master)](https://travis-ci.org/lib/pq)
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/lib/pq
|
||||
|
||||
## Docs
|
||||
|
||||
For detailed documentation and basic usage examples, please see the package
|
||||
documentation at <http://godoc.org/github.com/lib/pq>.
|
||||
|
||||
## Tests
|
||||
|
||||
`go test` is used for testing. A running PostgreSQL server is
|
||||
required, with the ability to log in. The default database to connect
|
||||
to test with is "pqgotest," but it can be overridden using environment
|
||||
variables.
|
||||
|
||||
Example:
|
||||
|
||||
PGHOST=/run/postgresql go test github.com/lib/pq
|
||||
|
||||
Optionally, a benchmark suite can be run as part of the tests:
|
||||
|
||||
PGHOST=/run/postgresql go test -bench .
|
||||
|
||||
## Features
|
||||
|
||||
* SSL
|
||||
* Handles bad connections for `database/sql`
|
||||
* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`)
|
||||
* Scan binary blobs correctly (i.e. `bytea`)
|
||||
* Package for `hstore` support
|
||||
* COPY FROM support
|
||||
* pq.ParseURL for converting urls to connection strings for sql.Open.
|
||||
* Many libpq compatible environment variables
|
||||
* Unix socket support
|
||||
* Notifications: `LISTEN`/`NOTIFY`
|
||||
* pgpass support
|
||||
|
||||
## Future / Things you can help with
|
||||
|
||||
* Better COPY FROM / COPY TO (see discussion in #181)
|
||||
|
||||
## Thank you (alphabetical)
|
||||
|
||||
Some of these contributors are from the original library `bmizerany/pq.go` whose
|
||||
code still exists in here.
|
||||
|
||||
* Andy Balholm (andybalholm)
|
||||
* Ben Berkert (benburkert)
|
||||
* Benjamin Heatwole (bheatwole)
|
||||
* Bill Mill (llimllib)
|
||||
* Bjørn Madsen (aeons)
|
||||
* Blake Gentry (bgentry)
|
||||
* Brad Fitzpatrick (bradfitz)
|
||||
* Charlie Melbye (cmelbye)
|
||||
* Chris Bandy (cbandy)
|
||||
* Chris Gilling (cgilling)
|
||||
* Chris Walsh (cwds)
|
||||
* Dan Sosedoff (sosedoff)
|
||||
* Daniel Farina (fdr)
|
||||
* Eric Chlebek (echlebek)
|
||||
* Eric Garrido (minusnine)
|
||||
* Eric Urban (hydrogen18)
|
||||
* Everyone at The Go Team
|
||||
* Evan Shaw (edsrzf)
|
||||
* Ewan Chou (coocood)
|
||||
* Fazal Majid (fazalmajid)
|
||||
* Federico Romero (federomero)
|
||||
* Fumin (fumin)
|
||||
* Gary Burd (garyburd)
|
||||
* Heroku (heroku)
|
||||
* James Pozdena (jpoz)
|
||||
* Jason McVetta (jmcvetta)
|
||||
* Jeremy Jay (pbnjay)
|
||||
* Joakim Sernbrant (serbaut)
|
||||
* John Gallagher (jgallagher)
|
||||
* Jonathan Rudenberg (titanous)
|
||||
* Joël Stemmer (jstemmer)
|
||||
* Kamil Kisiel (kisielk)
|
||||
* Kelly Dunn (kellydunn)
|
||||
* Keith Rarick (kr)
|
||||
* Kir Shatrov (kirs)
|
||||
* Lann Martin (lann)
|
||||
* Maciek Sakrejda (uhoh-itsmaciek)
|
||||
* Marc Brinkmann (mbr)
|
||||
* Marko Tiikkaja (johto)
|
||||
* Matt Newberry (MattNewberry)
|
||||
* Matt Robenolt (mattrobenolt)
|
||||
* Martin Olsen (martinolsen)
|
||||
* Mike Lewis (mikelikespie)
|
||||
* Nicolas Patry (Narsil)
|
||||
* Oliver Tonnhofer (olt)
|
||||
* Patrick Hayes (phayes)
|
||||
* Paul Hammond (paulhammond)
|
||||
* Ryan Smith (ryandotsmith)
|
||||
* Samuel Stauffer (samuel)
|
||||
* Timothée Peignier (cyberdelia)
|
||||
* Travis Cline (tmc)
|
||||
* TruongSinh Tran-Nguyen (truongsinh)
|
||||
* Yaismel Miranda (ympons)
|
||||
* notedit (notedit)
|
|
@ -0,0 +1,756 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var typeByteSlice = reflect.TypeOf([]byte{})
|
||||
var typeDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
||||
var typeSqlScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
|
||||
|
||||
// Array returns the optimal driver.Valuer and sql.Scanner for an array or
|
||||
// slice of any dimension.
|
||||
//
|
||||
// For example:
|
||||
// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401}))
|
||||
//
|
||||
// var x []sql.NullInt64
|
||||
// db.QueryRow('SELECT ARRAY[235, 401]').Scan(pq.Array(&x))
|
||||
//
|
||||
// Scanning multi-dimensional arrays is not supported. Arrays where the lower
|
||||
// bound is not one (such as `[0:0]={1}') are not supported.
|
||||
func Array(a interface{}) interface {
|
||||
driver.Valuer
|
||||
sql.Scanner
|
||||
} {
|
||||
switch a := a.(type) {
|
||||
case []bool:
|
||||
return (*BoolArray)(&a)
|
||||
case []float64:
|
||||
return (*Float64Array)(&a)
|
||||
case []int64:
|
||||
return (*Int64Array)(&a)
|
||||
case []string:
|
||||
return (*StringArray)(&a)
|
||||
|
||||
case *[]bool:
|
||||
return (*BoolArray)(a)
|
||||
case *[]float64:
|
||||
return (*Float64Array)(a)
|
||||
case *[]int64:
|
||||
return (*Int64Array)(a)
|
||||
case *[]string:
|
||||
return (*StringArray)(a)
|
||||
}
|
||||
|
||||
return GenericArray{a}
|
||||
}
|
||||
|
||||
// ArrayDelimiter may be optionally implemented by driver.Valuer or sql.Scanner
|
||||
// to override the array delimiter used by GenericArray.
|
||||
type ArrayDelimiter interface {
|
||||
// ArrayDelimiter returns the delimiter character(s) for this element's type.
|
||||
ArrayDelimiter() string
|
||||
}
|
||||
|
||||
// BoolArray represents a one-dimensional array of the PostgreSQL boolean type.
|
||||
type BoolArray []bool
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *BoolArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to BoolArray", src)
|
||||
}
|
||||
|
||||
func (a *BoolArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "BoolArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(BoolArray, len(elems))
|
||||
for i, v := range elems {
|
||||
if len(v) != 1 {
|
||||
return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v)
|
||||
}
|
||||
switch v[0] {
|
||||
case 't':
|
||||
b[i] = true
|
||||
case 'f':
|
||||
b[i] = false
|
||||
default:
|
||||
return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a BoolArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be exactly two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1+2*n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
b[2*i] = ','
|
||||
if a[i] {
|
||||
b[1+2*i] = 't'
|
||||
} else {
|
||||
b[1+2*i] = 'f'
|
||||
}
|
||||
}
|
||||
|
||||
b[0] = '{'
|
||||
b[2*n] = '}'
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// ByteaArray represents a one-dimensional array of the PostgreSQL bytea type.
|
||||
type ByteaArray [][]byte
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *ByteaArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to ByteaArray", src)
|
||||
}
|
||||
|
||||
func (a *ByteaArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "ByteaArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(ByteaArray, len(elems))
|
||||
for i, v := range elems {
|
||||
b[i], err = parseBytea(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse bytea array index %d: %s", i, err.Error())
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface. It uses the "hex" format which
|
||||
// is only supported on PostgreSQL 9.0 or newer.
|
||||
func (a ByteaArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, 2*N bytes of quotes,
|
||||
// 3*N bytes of hex formatting, and N-1 bytes of delimiters.
|
||||
size := 1 + 6*n
|
||||
for _, x := range a {
|
||||
size += hex.EncodedLen(len(x))
|
||||
}
|
||||
|
||||
b := make([]byte, size)
|
||||
|
||||
for i, s := 0, b; i < n; i++ {
|
||||
o := copy(s, `,"\\x`)
|
||||
o += hex.Encode(s[o:], a[i])
|
||||
s[o] = '"'
|
||||
s = s[o+1:]
|
||||
}
|
||||
|
||||
b[0] = '{'
|
||||
b[size-1] = '}'
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// Float64Array represents a one-dimensional array of the PostgreSQL double
|
||||
// precision type.
|
||||
type Float64Array []float64
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *Float64Array) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to Float64Array", src)
|
||||
}
|
||||
|
||||
func (a *Float64Array) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "Float64Array")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(Float64Array, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i], err = strconv.ParseFloat(string(v), 64); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a Float64Array) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+2*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = strconv.AppendFloat(b, a[0], 'f', -1, 64)
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = strconv.AppendFloat(b, a[i], 'f', -1, 64)
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// GenericArray implements the driver.Valuer and sql.Scanner interfaces for
|
||||
// an array or slice of any dimension.
|
||||
type GenericArray struct{ A interface{} }
|
||||
|
||||
func (GenericArray) evaluateDestination(rt reflect.Type) (reflect.Type, func([]byte, reflect.Value) error, string) {
|
||||
var assign func([]byte, reflect.Value) error
|
||||
var del = ","
|
||||
|
||||
// TODO calculate the assign function for other types
|
||||
// TODO repeat this section on the element type of arrays or slices (multidimensional)
|
||||
{
|
||||
if reflect.PtrTo(rt).Implements(typeSqlScanner) {
|
||||
// dest is always addressable because it is an element of a slice.
|
||||
assign = func(src []byte, dest reflect.Value) (err error) {
|
||||
ss := dest.Addr().Interface().(sql.Scanner)
|
||||
if src == nil {
|
||||
err = ss.Scan(nil)
|
||||
} else {
|
||||
err = ss.Scan(src)
|
||||
}
|
||||
return
|
||||
}
|
||||
goto FoundType
|
||||
}
|
||||
|
||||
assign = func([]byte, reflect.Value) error {
|
||||
return fmt.Errorf("pq: scanning to %s is not implemented; only sql.Scanner", rt)
|
||||
}
|
||||
}
|
||||
|
||||
FoundType:
|
||||
|
||||
if ad, ok := reflect.Zero(rt).Interface().(ArrayDelimiter); ok {
|
||||
del = ad.ArrayDelimiter()
|
||||
}
|
||||
|
||||
return rt, assign, del
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a GenericArray) Scan(src interface{}) error {
|
||||
dpv := reflect.ValueOf(a.A)
|
||||
switch {
|
||||
case dpv.Kind() != reflect.Ptr:
|
||||
return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A)
|
||||
case dpv.IsNil():
|
||||
return fmt.Errorf("pq: destination %T is nil", a.A)
|
||||
}
|
||||
|
||||
dv := dpv.Elem()
|
||||
switch dv.Kind() {
|
||||
case reflect.Slice:
|
||||
case reflect.Array:
|
||||
default:
|
||||
return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A)
|
||||
}
|
||||
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src, dv)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src), dv)
|
||||
case nil:
|
||||
if dv.Kind() == reflect.Slice {
|
||||
dv.Set(reflect.Zero(dv.Type()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to %s", src, dv.Type())
|
||||
}
|
||||
|
||||
func (a GenericArray) scanBytes(src []byte, dv reflect.Value) error {
|
||||
dtype, assign, del := a.evaluateDestination(dv.Type().Elem())
|
||||
dims, elems, err := parseArray(src, []byte(del))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO allow multidimensional
|
||||
|
||||
if len(dims) > 1 {
|
||||
return fmt.Errorf("pq: scanning from multidimensional ARRAY%s is not implemented",
|
||||
strings.Replace(fmt.Sprint(dims), " ", "][", -1))
|
||||
}
|
||||
|
||||
// Treat a zero-dimensional array like an array with a single dimension of zero.
|
||||
if len(dims) == 0 {
|
||||
dims = append(dims, 0)
|
||||
}
|
||||
|
||||
for i, rt := 0, dv.Type(); i < len(dims); i, rt = i+1, rt.Elem() {
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
case reflect.Array:
|
||||
if rt.Len() != dims[i] {
|
||||
return fmt.Errorf("pq: cannot convert ARRAY%s to %s",
|
||||
strings.Replace(fmt.Sprint(dims), " ", "][", -1), dv.Type())
|
||||
}
|
||||
default:
|
||||
// TODO handle multidimensional
|
||||
}
|
||||
}
|
||||
|
||||
values := reflect.MakeSlice(reflect.SliceOf(dtype), len(elems), len(elems))
|
||||
for i, e := range elems {
|
||||
if err := assign(e, values.Index(i)); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle multidimensional
|
||||
|
||||
switch dv.Kind() {
|
||||
case reflect.Slice:
|
||||
dv.Set(values.Slice(0, dims[0]))
|
||||
case reflect.Array:
|
||||
for i := 0; i < dims[0]; i++ {
|
||||
dv.Index(i).Set(values.Index(i))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a GenericArray) Value() (driver.Value, error) {
|
||||
if a.A == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(a.A)
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice:
|
||||
if rv.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Array:
|
||||
default:
|
||||
return nil, fmt.Errorf("pq: Unable to convert %T to array", a.A)
|
||||
}
|
||||
|
||||
if n := rv.Len(); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 0, 1+2*n)
|
||||
|
||||
b, _, err := appendArray(b, rv, n)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// Int64Array represents a one-dimensional array of the PostgreSQL integer types.
|
||||
type Int64Array []int64
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *Int64Array) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to Int64Array", src)
|
||||
}
|
||||
|
||||
func (a *Int64Array) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "Int64Array")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(Int64Array, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i], err = strconv.ParseInt(string(v), 10, 64); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a Int64Array) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+2*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = strconv.AppendInt(b, a[0], 10)
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = strconv.AppendInt(b, a[i], 10)
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// StringArray represents a one-dimensional array of the PostgreSQL character types.
|
||||
type StringArray []string
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *StringArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to StringArray", src)
|
||||
}
|
||||
|
||||
func (a *StringArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "StringArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(StringArray, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i] = string(v); v == nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: cannot convert nil to string", i)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a StringArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, 2*N bytes of quotes,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+3*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = appendArrayQuotedBytes(b, []byte(a[0]))
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = appendArrayQuotedBytes(b, []byte(a[i]))
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// appendArray appends rv to the buffer, returning the extended buffer and
|
||||
// the delimiter used between elements.
|
||||
//
|
||||
// It panics when n <= 0 or rv's Kind is not reflect.Array nor reflect.Slice.
|
||||
func appendArray(b []byte, rv reflect.Value, n int) ([]byte, string, error) {
|
||||
var del string
|
||||
var err error
|
||||
|
||||
b = append(b, '{')
|
||||
|
||||
if b, del, err = appendArrayElement(b, rv.Index(0)); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, del...)
|
||||
if b, del, err = appendArrayElement(b, rv.Index(i)); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
}
|
||||
|
||||
return append(b, '}'), del, nil
|
||||
}
|
||||
|
||||
// appendArrayElement appends rv to the buffer, returning the extended buffer
|
||||
// and the delimiter to use before the next element.
|
||||
//
|
||||
// When rv's Kind is neither reflect.Array nor reflect.Slice, it is converted
|
||||
// using driver.DefaultParameterConverter and the resulting []byte or string
|
||||
// is double-quoted.
|
||||
//
|
||||
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
|
||||
func appendArrayElement(b []byte, rv reflect.Value) ([]byte, string, error) {
|
||||
if k := rv.Kind(); k == reflect.Array || k == reflect.Slice {
|
||||
if t := rv.Type(); t != typeByteSlice && !t.Implements(typeDriverValuer) {
|
||||
if n := rv.Len(); n > 0 {
|
||||
return appendArray(b, rv, n)
|
||||
}
|
||||
|
||||
return b, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
var del string = ","
|
||||
var err error
|
||||
var iv interface{} = rv.Interface()
|
||||
|
||||
if ad, ok := iv.(ArrayDelimiter); ok {
|
||||
del = ad.ArrayDelimiter()
|
||||
}
|
||||
|
||||
if iv, err = driver.DefaultParameterConverter.ConvertValue(iv); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
switch v := iv.(type) {
|
||||
case nil:
|
||||
return append(b, "NULL"...), del, nil
|
||||
case []byte:
|
||||
return appendArrayQuotedBytes(b, v), del, nil
|
||||
case string:
|
||||
return appendArrayQuotedBytes(b, []byte(v)), del, nil
|
||||
}
|
||||
|
||||
b, err = appendValue(b, iv)
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
func appendArrayQuotedBytes(b, v []byte) []byte {
|
||||
b = append(b, '"')
|
||||
for {
|
||||
i := bytes.IndexAny(v, `"\`)
|
||||
if i < 0 {
|
||||
b = append(b, v...)
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
b = append(b, v[:i]...)
|
||||
}
|
||||
b = append(b, '\\', v[i])
|
||||
v = v[i+1:]
|
||||
}
|
||||
return append(b, '"')
|
||||
}
|
||||
|
||||
func appendValue(b []byte, v driver.Value) ([]byte, error) {
|
||||
return append(b, encode(nil, v, 0)...), nil
|
||||
}
|
||||
|
||||
// parseArray extracts the dimensions and elements of an array represented in
|
||||
// text format. Only representations emitted by the backend are supported.
|
||||
// Notably, whitespace around brackets and delimiters is significant, and NULL
|
||||
// is case-sensitive.
|
||||
//
|
||||
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
|
||||
func parseArray(src, del []byte) (dims []int, elems [][]byte, err error) {
|
||||
var depth, i int
|
||||
|
||||
if len(src) < 1 || src[0] != '{' {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '{', 0)
|
||||
}
|
||||
|
||||
Open:
|
||||
for i < len(src) {
|
||||
switch src[i] {
|
||||
case '{':
|
||||
depth++
|
||||
i++
|
||||
case '}':
|
||||
elems = make([][]byte, 0)
|
||||
goto Close
|
||||
default:
|
||||
break Open
|
||||
}
|
||||
}
|
||||
dims = make([]int, i)
|
||||
|
||||
Element:
|
||||
for i < len(src) {
|
||||
switch src[i] {
|
||||
case '{':
|
||||
if depth == len(dims) {
|
||||
break Element
|
||||
}
|
||||
depth++
|
||||
dims[depth-1] = 0
|
||||
i++
|
||||
case '"':
|
||||
var elem = []byte{}
|
||||
var escape bool
|
||||
for i++; i < len(src); i++ {
|
||||
if escape {
|
||||
elem = append(elem, src[i])
|
||||
escape = false
|
||||
} else {
|
||||
switch src[i] {
|
||||
default:
|
||||
elem = append(elem, src[i])
|
||||
case '\\':
|
||||
escape = true
|
||||
case '"':
|
||||
elems = append(elems, elem)
|
||||
i++
|
||||
break Element
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
for start := i; i < len(src); i++ {
|
||||
if bytes.HasPrefix(src[i:], del) || src[i] == '}' {
|
||||
elem := src[start:i]
|
||||
if len(elem) == 0 {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
if bytes.Equal(elem, []byte("NULL")) {
|
||||
elem = nil
|
||||
}
|
||||
elems = append(elems, elem)
|
||||
break Element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i < len(src) {
|
||||
if bytes.HasPrefix(src[i:], del) && depth > 0 {
|
||||
dims[depth-1]++
|
||||
i += len(del)
|
||||
goto Element
|
||||
} else if src[i] == '}' && depth > 0 {
|
||||
dims[depth-1]++
|
||||
depth--
|
||||
i++
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
}
|
||||
|
||||
Close:
|
||||
for i < len(src) {
|
||||
if src[i] == '}' && depth > 0 {
|
||||
depth--
|
||||
i++
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
err = fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '}', i)
|
||||
}
|
||||
if err == nil {
|
||||
for _, d := range dims {
|
||||
if (len(elems) % d) != 0 {
|
||||
err = fmt.Errorf("pq: multidimensional arrays must have elements with matching dimensions")
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) {
|
||||
dims, elems, err := parseArray(src, del)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dims) > 1 {
|
||||
return nil, fmt.Errorf("pq: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ)
|
||||
}
|
||||
return elems, err
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/lib/pq/oid"
|
||||
)
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) int32() (n int) {
|
||||
n = int(int32(binary.BigEndian.Uint32(*b)))
|
||||
*b = (*b)[4:]
|
||||
return
|
||||
}
|
||||
|
||||
func (b *readBuf) oid() (n oid.Oid) {
|
||||
n = oid.Oid(binary.BigEndian.Uint32(*b))
|
||||
*b = (*b)[4:]
|
||||
return
|
||||
}
|
||||
|
||||
// N.B: this is actually an unsigned 16-bit integer, unlike int32
|
||||
func (b *readBuf) int16() (n int) {
|
||||
n = int(binary.BigEndian.Uint16(*b))
|
||||
*b = (*b)[2:]
|
||||
return
|
||||
}
|
||||
|
||||
func (b *readBuf) string() string {
|
||||
i := bytes.IndexByte(*b, 0)
|
||||
if i < 0 {
|
||||
errorf("invalid message format; expected string terminator")
|
||||
}
|
||||
s := (*b)[:i]
|
||||
*b = (*b)[i+1:]
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (b *readBuf) next(n int) (v []byte) {
|
||||
v = (*b)[:n]
|
||||
*b = (*b)[n:]
|
||||
return
|
||||
}
|
||||
|
||||
func (b *readBuf) byte() byte {
|
||||
return b.next(1)[0]
|
||||
}
|
||||
|
||||
type writeBuf struct {
|
||||
buf []byte
|
||||
pos int
|
||||
}
|
||||
|
||||
func (b *writeBuf) int32(n int) {
|
||||
x := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(x, uint32(n))
|
||||
b.buf = append(b.buf, x...)
|
||||
}
|
||||
|
||||
func (b *writeBuf) int16(n int) {
|
||||
x := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(x, uint16(n))
|
||||
b.buf = append(b.buf, x...)
|
||||
}
|
||||
|
||||
func (b *writeBuf) string(s string) {
|
||||
b.buf = append(b.buf, (s + "\000")...)
|
||||
}
|
||||
|
||||
func (b *writeBuf) byte(c byte) {
|
||||
b.buf = append(b.buf, c)
|
||||
}
|
||||
|
||||
func (b *writeBuf) bytes(v []byte) {
|
||||
b.buf = append(b.buf, v...)
|
||||
}
|
||||
|
||||
func (b *writeBuf) wrap() []byte {
|
||||
p := b.buf[b.pos:]
|
||||
binary.BigEndian.PutUint32(p, uint32(len(p)))
|
||||
return b.buf
|
||||
}
|
||||
|
||||
func (b *writeBuf) next(c byte) {
|
||||
p := b.buf[b.pos:]
|
||||
binary.BigEndian.PutUint32(p, uint32(len(p)))
|
||||
b.pos = len(b.buf) + 1
|
||||
b.buf = append(b.buf, c, 0, 0, 0, 0)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,282 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
errCopyInClosed = errors.New("pq: copyin statement has already been closed")
|
||||
errBinaryCopyNotSupported = errors.New("pq: only text format supported for COPY")
|
||||
errCopyToNotSupported = errors.New("pq: COPY TO is not supported")
|
||||
errCopyNotSupportedOutsideTxn = errors.New("pq: COPY is only allowed inside a transaction")
|
||||
errCopyInProgress = errors.New("pq: COPY in progress")
|
||||
)
|
||||
|
||||
// CopyIn creates a COPY FROM statement which can be prepared with
|
||||
// Tx.Prepare(). The target table should be visible in search_path.
|
||||
func CopyIn(table string, columns ...string) string {
|
||||
stmt := "COPY " + QuoteIdentifier(table) + " ("
|
||||
for i, col := range columns {
|
||||
if i != 0 {
|
||||
stmt += ", "
|
||||
}
|
||||
stmt += QuoteIdentifier(col)
|
||||
}
|
||||
stmt += ") FROM STDIN"
|
||||
return stmt
|
||||
}
|
||||
|
||||
// CopyInSchema creates a COPY FROM statement which can be prepared with
|
||||
// Tx.Prepare().
|
||||
func CopyInSchema(schema, table string, columns ...string) string {
|
||||
stmt := "COPY " + QuoteIdentifier(schema) + "." + QuoteIdentifier(table) + " ("
|
||||
for i, col := range columns {
|
||||
if i != 0 {
|
||||
stmt += ", "
|
||||
}
|
||||
stmt += QuoteIdentifier(col)
|
||||
}
|
||||
stmt += ") FROM STDIN"
|
||||
return stmt
|
||||
}
|
||||
|
||||
type copyin struct {
|
||||
cn *conn
|
||||
buffer []byte
|
||||
rowData chan []byte
|
||||
done chan bool
|
||||
|
||||
closed bool
|
||||
|
||||
sync.Mutex // guards err
|
||||
err error
|
||||
}
|
||||
|
||||
const ciBufferSize = 64 * 1024
|
||||
|
||||
// flush buffer before the buffer is filled up and needs reallocation
|
||||
const ciBufferFlushSize = 63 * 1024
|
||||
|
||||
func (cn *conn) prepareCopyIn(q string) (_ driver.Stmt, err error) {
|
||||
if !cn.isInTransaction() {
|
||||
return nil, errCopyNotSupportedOutsideTxn
|
||||
}
|
||||
|
||||
ci := ©in{
|
||||
cn: cn,
|
||||
buffer: make([]byte, 0, ciBufferSize),
|
||||
rowData: make(chan []byte),
|
||||
done: make(chan bool, 1),
|
||||
}
|
||||
// add CopyData identifier + 4 bytes for message length
|
||||
ci.buffer = append(ci.buffer, 'd', 0, 0, 0, 0)
|
||||
|
||||
b := cn.writeBuf('Q')
|
||||
b.string(q)
|
||||
cn.send(b)
|
||||
|
||||
awaitCopyInResponse:
|
||||
for {
|
||||
t, r := cn.recv1()
|
||||
switch t {
|
||||
case 'G':
|
||||
if r.byte() != 0 {
|
||||
err = errBinaryCopyNotSupported
|
||||
break awaitCopyInResponse
|
||||
}
|
||||
go ci.resploop()
|
||||
return ci, nil
|
||||
case 'H':
|
||||
err = errCopyToNotSupported
|
||||
break awaitCopyInResponse
|
||||
case 'E':
|
||||
err = parseError(r)
|
||||
case 'Z':
|
||||
if err == nil {
|
||||
ci.setBad()
|
||||
errorf("unexpected ReadyForQuery in response to COPY")
|
||||
}
|
||||
cn.processReadyForQuery(r)
|
||||
return nil, err
|
||||
default:
|
||||
ci.setBad()
|
||||
errorf("unknown response for copy query: %q", t)
|
||||
}
|
||||
}
|
||||
|
||||
// something went wrong, abort COPY before we return
|
||||
b = cn.writeBuf('f')
|
||||
b.string(err.Error())
|
||||
cn.send(b)
|
||||
|
||||
for {
|
||||
t, r := cn.recv1()
|
||||
switch t {
|
||||
case 'c', 'C', 'E':
|
||||
case 'Z':
|
||||
// correctly aborted, we're done
|
||||
cn.processReadyForQuery(r)
|
||||
return nil, err
|
||||
default:
|
||||
ci.setBad()
|
||||
errorf("unknown response for CopyFail: %q", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ci *copyin) flush(buf []byte) {
|
||||
// set message length (without message identifier)
|
||||
binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1))
|
||||
|
||||
_, err := ci.cn.c.Write(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ci *copyin) resploop() {
|
||||
for {
|
||||
var r readBuf
|
||||
t, err := ci.cn.recvMessage(&r)
|
||||
if err != nil {
|
||||
ci.setBad()
|
||||
ci.setError(err)
|
||||
ci.done <- true
|
||||
return
|
||||
}
|
||||
switch t {
|
||||
case 'C':
|
||||
// complete
|
||||
case 'N':
|
||||
// NoticeResponse
|
||||
case 'Z':
|
||||
ci.cn.processReadyForQuery(&r)
|
||||
ci.done <- true
|
||||
return
|
||||
case 'E':
|
||||
err := parseError(&r)
|
||||
ci.setError(err)
|
||||
default:
|
||||
ci.setBad()
|
||||
ci.setError(fmt.Errorf("unknown response during CopyIn: %q", t))
|
||||
ci.done <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ci *copyin) setBad() {
|
||||
ci.Lock()
|
||||
ci.cn.bad = true
|
||||
ci.Unlock()
|
||||
}
|
||||
|
||||
func (ci *copyin) isBad() bool {
|
||||
ci.Lock()
|
||||
b := ci.cn.bad
|
||||
ci.Unlock()
|
||||
return b
|
||||
}
|
||||
|
||||
func (ci *copyin) isErrorSet() bool {
|
||||
ci.Lock()
|
||||
isSet := (ci.err != nil)
|
||||
ci.Unlock()
|
||||
return isSet
|
||||
}
|
||||
|
||||
// setError() sets ci.err if one has not been set already. Caller must not be
|
||||
// holding ci.Mutex.
|
||||
func (ci *copyin) setError(err error) {
|
||||
ci.Lock()
|
||||
if ci.err == nil {
|
||||
ci.err = err
|
||||
}
|
||||
ci.Unlock()
|
||||
}
|
||||
|
||||
func (ci *copyin) NumInput() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (ci *copyin) Query(v []driver.Value) (r driver.Rows, err error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// Exec inserts values into the COPY stream. The insert is asynchronous
|
||||
// and Exec can return errors from previous Exec calls to the same
|
||||
// COPY stmt.
|
||||
//
|
||||
// You need to call Exec(nil) to sync the COPY stream and to get any
|
||||
// errors from pending data, since Stmt.Close() doesn't return errors
|
||||
// to the user.
|
||||
func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) {
|
||||
if ci.closed {
|
||||
return nil, errCopyInClosed
|
||||
}
|
||||
|
||||
if ci.isBad() {
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
defer ci.cn.errRecover(&err)
|
||||
|
||||
if ci.isErrorSet() {
|
||||
return nil, ci.err
|
||||
}
|
||||
|
||||
if len(v) == 0 {
|
||||
return nil, ci.Close()
|
||||
}
|
||||
|
||||
numValues := len(v)
|
||||
for i, value := range v {
|
||||
ci.buffer = appendEncodedText(&ci.cn.parameterStatus, ci.buffer, value)
|
||||
if i < numValues-1 {
|
||||
ci.buffer = append(ci.buffer, '\t')
|
||||
}
|
||||
}
|
||||
|
||||
ci.buffer = append(ci.buffer, '\n')
|
||||
|
||||
if len(ci.buffer) > ciBufferFlushSize {
|
||||
ci.flush(ci.buffer)
|
||||
// reset buffer, keep bytes for message identifier and length
|
||||
ci.buffer = ci.buffer[:5]
|
||||
}
|
||||
|
||||
return driver.RowsAffected(0), nil
|
||||
}
|
||||
|
||||
func (ci *copyin) Close() (err error) {
|
||||
if ci.closed { // Don't do anything, we're already closed
|
||||
return nil
|
||||
}
|
||||
ci.closed = true
|
||||
|
||||
if ci.isBad() {
|
||||
return driver.ErrBadConn
|
||||
}
|
||||
defer ci.cn.errRecover(&err)
|
||||
|
||||
if len(ci.buffer) > 0 {
|
||||
ci.flush(ci.buffer)
|
||||
}
|
||||
// Avoid touching the scratch buffer as resploop could be using it.
|
||||
err = ci.cn.sendSimpleMessage('c')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-ci.done
|
||||
ci.cn.inCopy = false
|
||||
|
||||
if ci.isErrorSet() {
|
||||
err = ci.err
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
Package pq is a pure Go Postgres driver for the database/sql package.
|
||||
|
||||
In most cases clients will use the database/sql package instead of
|
||||
using this package directly. For example:
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
age := 21
|
||||
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
|
||||
…
|
||||
}
|
||||
|
||||
You can also connect to a database using a URL. For example:
|
||||
|
||||
db, err := sql.Open("postgres", "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full")
|
||||
|
||||
|
||||
Connection String Parameters
|
||||
|
||||
|
||||
Similarly to libpq, when establishing a connection using pq you are expected to
|
||||
supply a connection string containing zero or more parameters.
|
||||
A subset of the connection parameters supported by libpq are also supported by pq.
|
||||
Additionally, pq also lets you specify run-time parameters (such as search_path or work_mem)
|
||||
directly in the connection string. This is different from libpq, which does not allow
|
||||
run-time parameters in the connection string, instead requiring you to supply
|
||||
them in the options parameter.
|
||||
|
||||
For compatibility with libpq, the following special connection parameters are
|
||||
supported:
|
||||
|
||||
* dbname - The name of the database to connect to
|
||||
* user - The user to sign in as
|
||||
* password - The user's password
|
||||
* host - The host to connect to. Values that start with / are for unix domain sockets. (default is localhost)
|
||||
* port - The port to bind to. (default is 5432)
|
||||
* sslmode - Whether or not to use SSL (default is require, this is not the default for libpq)
|
||||
* fallback_application_name - An application_name to fall back to if one isn't provided.
|
||||
* connect_timeout - Maximum wait for connection, in seconds. Zero or not specified means wait indefinitely.
|
||||
* sslcert - Cert file location. The file must contain PEM encoded data.
|
||||
* sslkey - Key file location. The file must contain PEM encoded data.
|
||||
* sslrootcert - The location of the root certificate file. The file must contain PEM encoded data.
|
||||
|
||||
Valid values for sslmode are:
|
||||
|
||||
* disable - No SSL
|
||||
* require - Always SSL (skip verification)
|
||||
* verify-ca - Always SSL (verify that the certificate presented by the server was signed by a trusted CA)
|
||||
* verify-full - Always SSL (verify that the certification presented by the server was signed by a trusted CA and the server host name matches the one in the certificate)
|
||||
|
||||
See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
for more information about connection string parameters.
|
||||
|
||||
Use single quotes for values that contain whitespace:
|
||||
|
||||
"user=pqgotest password='with spaces'"
|
||||
|
||||
A backslash will escape the next character in values:
|
||||
|
||||
"user=space\ man password='it\'s valid'
|
||||
|
||||
Note that the connection parameter client_encoding (which sets the
|
||||
text encoding for the connection) may be set but must be "UTF8",
|
||||
matching with the same rules as Postgres. It is an error to provide
|
||||
any other value.
|
||||
|
||||
In addition to the parameters listed above, any run-time parameter that can be
|
||||
set at backend start time can be set in the connection string. For more
|
||||
information, see
|
||||
http://www.postgresql.org/docs/current/static/runtime-config.html.
|
||||
|
||||
Most environment variables as specified at http://www.postgresql.org/docs/current/static/libpq-envars.html
|
||||
supported by libpq are also supported by pq. If any of the environment
|
||||
variables not supported by pq are set, pq will panic during connection
|
||||
establishment. Environment variables have a lower precedence than explicitly
|
||||
provided connection parameters.
|
||||
|
||||
The pgpass mechanism as described in http://www.postgresql.org/docs/current/static/libpq-pgpass.html
|
||||
is supported, but on Windows PGPASSFILE must be specified explicitly.
|
||||
|
||||
|
||||
Queries
|
||||
|
||||
|
||||
database/sql does not dictate any specific format for parameter
|
||||
markers in query strings, and pq uses the Postgres-native ordinal markers,
|
||||
as shown above. The same marker can be reused for the same parameter:
|
||||
|
||||
rows, err := db.Query(`SELECT name FROM users WHERE favorite_fruit = $1
|
||||
OR age BETWEEN $2 AND $2 + 3`, "orange", 64)
|
||||
|
||||
pq does not support the LastInsertId() method of the Result type in database/sql.
|
||||
To return the identifier of an INSERT (or UPDATE or DELETE), use the Postgres
|
||||
RETURNING clause with a standard Query or QueryRow call:
|
||||
|
||||
var userid int
|
||||
err := db.QueryRow(`INSERT INTO users(name, favorite_fruit, age)
|
||||
VALUES('beatrice', 'starfruit', 93) RETURNING id`).Scan(&userid)
|
||||
|
||||
For more details on RETURNING, see the Postgres documentation:
|
||||
|
||||
http://www.postgresql.org/docs/current/static/sql-insert.html
|
||||
http://www.postgresql.org/docs/current/static/sql-update.html
|
||||
http://www.postgresql.org/docs/current/static/sql-delete.html
|
||||
|
||||
For additional instructions on querying see the documentation for the database/sql package.
|
||||
|
||||
|
||||
Data Types
|
||||
|
||||
|
||||
Parameters pass through driver.DefaultParameterConverter before they are handled
|
||||
by this package. When the binary_parameters connection option is enabled,
|
||||
[]byte values are sent directly to the backend as data in binary format.
|
||||
|
||||
This package returns the following types for values from the PostgreSQL backend:
|
||||
|
||||
- integer types smallint, integer, and bigint are returned as int64
|
||||
- floating-point types real and double precision are returned as float64
|
||||
- character types char, varchar, and text are returned as string
|
||||
- temporal types date, time, timetz, timestamp, and timestamptz are returned as time.Time
|
||||
- the boolean type is returned as bool
|
||||
- the bytea type is returned as []byte
|
||||
|
||||
All other types are returned directly from the backend as []byte values in text format.
|
||||
|
||||
|
||||
Errors
|
||||
|
||||
|
||||
pq may return errors of type *pq.Error which can be interrogated for error details:
|
||||
|
||||
if err, ok := err.(*pq.Error); ok {
|
||||
fmt.Println("pq error:", err.Code.Name())
|
||||
}
|
||||
|
||||
See the pq.Error type for details.
|
||||
|
||||
|
||||
Bulk imports
|
||||
|
||||
You can perform bulk imports by preparing a statement returned by pq.CopyIn (or
|
||||
pq.CopyInSchema) in an explicit transaction (sql.Tx). The returned statement
|
||||
handle can then be repeatedly "executed" to copy data into the target table.
|
||||
After all data has been processed you should call Exec() once with no arguments
|
||||
to flush all buffered data. Any call to Exec() might return an error which
|
||||
should be handled appropriately, but because of the internal buffering an error
|
||||
returned by Exec() might not be related to the data passed in the call that
|
||||
failed.
|
||||
|
||||
CopyIn uses COPY FROM internally. It is not possible to COPY outside of an
|
||||
explicit transaction in pq.
|
||||
|
||||
Usage example:
|
||||
|
||||
txn, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err := txn.Prepare(pq.CopyIn("users", "name", "age"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
_, err = stmt.Exec(user.Name, int64(user.Age))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = stmt.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = txn.Commit()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
|
||||
PostgreSQL supports a simple publish/subscribe model over database
|
||||
connections. See http://www.postgresql.org/docs/current/static/sql-notify.html
|
||||
for more information about the general mechanism.
|
||||
|
||||
To start listening for notifications, you first have to open a new connection
|
||||
to the database by calling NewListener. This connection can not be used for
|
||||
anything other than LISTEN / NOTIFY. Calling Listen will open a "notification
|
||||
channel"; once a notification channel is open, a notification generated on that
|
||||
channel will effect a send on the Listener.Notify channel. A notification
|
||||
channel will remain open until Unlisten is called, though connection loss might
|
||||
result in some notifications being lost. To solve this problem, Listener sends
|
||||
a nil pointer over the Notify channel any time the connection is re-established
|
||||
following a connection loss. The application can get information about the
|
||||
state of the underlying connection by setting an event callback in the call to
|
||||
NewListener.
|
||||
|
||||
A single Listener can safely be used from concurrent goroutines, which means
|
||||
that there is often no need to create more than one Listener in your
|
||||
application. However, a Listener is always connected to a single database, so
|
||||
you will need to create a new Listener instance for every database you want to
|
||||
receive notifications in.
|
||||
|
||||
The channel name in both Listen and Unlisten is case sensitive, and can contain
|
||||
any characters legal in an identifier (see
|
||||
http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
|
||||
for more information). Note that the channel name will be truncated to 63
|
||||
bytes by the PostgreSQL server.
|
||||
|
||||
You can find a complete, working example of Listener usage at
|
||||
http://godoc.org/github.com/lib/pq/listen_example.
|
||||
|
||||
*/
|
||||
package pq
|
|
@ -0,0 +1,595 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq/oid"
|
||||
)
|
||||
|
||||
func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
|
||||
switch v := x.(type) {
|
||||
case []byte:
|
||||
return v
|
||||
default:
|
||||
return encode(parameterStatus, x, oid.T_unknown)
|
||||
}
|
||||
}
|
||||
|
||||
func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
|
||||
switch v := x.(type) {
|
||||
case int64:
|
||||
return strconv.AppendInt(nil, v, 10)
|
||||
case float64:
|
||||
return strconv.AppendFloat(nil, v, 'f', -1, 64)
|
||||
case []byte:
|
||||
if pgtypOid == oid.T_bytea {
|
||||
return encodeBytea(parameterStatus.serverVersion, v)
|
||||
}
|
||||
|
||||
return v
|
||||
case string:
|
||||
if pgtypOid == oid.T_bytea {
|
||||
return encodeBytea(parameterStatus.serverVersion, []byte(v))
|
||||
}
|
||||
|
||||
return []byte(v)
|
||||
case bool:
|
||||
return strconv.AppendBool(nil, v)
|
||||
case time.Time:
|
||||
return formatTs(v)
|
||||
|
||||
default:
|
||||
errorf("encode: unknown type for %T", v)
|
||||
}
|
||||
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} {
|
||||
switch f {
|
||||
case formatBinary:
|
||||
return binaryDecode(parameterStatus, s, typ)
|
||||
case formatText:
|
||||
return textDecode(parameterStatus, s, typ)
|
||||
default:
|
||||
panic("not reached")
|
||||
}
|
||||
}
|
||||
|
||||
func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
|
||||
switch typ {
|
||||
case oid.T_bytea:
|
||||
return s
|
||||
case oid.T_int8:
|
||||
return int64(binary.BigEndian.Uint64(s))
|
||||
case oid.T_int4:
|
||||
return int64(int32(binary.BigEndian.Uint32(s)))
|
||||
case oid.T_int2:
|
||||
return int64(int16(binary.BigEndian.Uint16(s)))
|
||||
case oid.T_uuid:
|
||||
b, err := decodeUUIDBinary(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
|
||||
default:
|
||||
errorf("don't know how to decode binary parameter of type %d", uint32(typ))
|
||||
}
|
||||
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
|
||||
switch typ {
|
||||
case oid.T_char, oid.T_varchar, oid.T_text:
|
||||
return string(s)
|
||||
case oid.T_bytea:
|
||||
b, err := parseBytea(s)
|
||||
if err != nil {
|
||||
errorf("%s", err)
|
||||
}
|
||||
return b
|
||||
case oid.T_timestamptz:
|
||||
return parseTs(parameterStatus.currentLocation, string(s))
|
||||
case oid.T_timestamp, oid.T_date:
|
||||
return parseTs(nil, string(s))
|
||||
case oid.T_time:
|
||||
return mustParse("15:04:05", typ, s)
|
||||
case oid.T_timetz:
|
||||
return mustParse("15:04:05-07", typ, s)
|
||||
case oid.T_bool:
|
||||
return s[0] == 't'
|
||||
case oid.T_int8, oid.T_int4, oid.T_int2:
|
||||
i, err := strconv.ParseInt(string(s), 10, 64)
|
||||
if err != nil {
|
||||
errorf("%s", err)
|
||||
}
|
||||
return i
|
||||
case oid.T_float4, oid.T_float8:
|
||||
bits := 64
|
||||
if typ == oid.T_float4 {
|
||||
bits = 32
|
||||
}
|
||||
f, err := strconv.ParseFloat(string(s), bits)
|
||||
if err != nil {
|
||||
errorf("%s", err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// appendEncodedText encodes item in text format as required by COPY
|
||||
// and appends to buf
|
||||
func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface{}) []byte {
|
||||
switch v := x.(type) {
|
||||
case int64:
|
||||
return strconv.AppendInt(buf, v, 10)
|
||||
case float64:
|
||||
return strconv.AppendFloat(buf, v, 'f', -1, 64)
|
||||
case []byte:
|
||||
encodedBytea := encodeBytea(parameterStatus.serverVersion, v)
|
||||
return appendEscapedText(buf, string(encodedBytea))
|
||||
case string:
|
||||
return appendEscapedText(buf, v)
|
||||
case bool:
|
||||
return strconv.AppendBool(buf, v)
|
||||
case time.Time:
|
||||
return append(buf, formatTs(v)...)
|
||||
case nil:
|
||||
return append(buf, "\\N"...)
|
||||
default:
|
||||
errorf("encode: unknown type for %T", v)
|
||||
}
|
||||
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func appendEscapedText(buf []byte, text string) []byte {
|
||||
escapeNeeded := false
|
||||
startPos := 0
|
||||
var c byte
|
||||
|
||||
// check if we need to escape
|
||||
for i := 0; i < len(text); i++ {
|
||||
c = text[i]
|
||||
if c == '\\' || c == '\n' || c == '\r' || c == '\t' {
|
||||
escapeNeeded = true
|
||||
startPos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if !escapeNeeded {
|
||||
return append(buf, text...)
|
||||
}
|
||||
|
||||
// copy till first char to escape, iterate the rest
|
||||
result := append(buf, text[:startPos]...)
|
||||
for i := startPos; i < len(text); i++ {
|
||||
c = text[i]
|
||||
switch c {
|
||||
case '\\':
|
||||
result = append(result, '\\', '\\')
|
||||
case '\n':
|
||||
result = append(result, '\\', 'n')
|
||||
case '\r':
|
||||
result = append(result, '\\', 'r')
|
||||
case '\t':
|
||||
result = append(result, '\\', 't')
|
||||
default:
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mustParse(f string, typ oid.Oid, s []byte) time.Time {
|
||||
str := string(s)
|
||||
|
||||
// check for a 30-minute-offset timezone
|
||||
if (typ == oid.T_timestamptz || typ == oid.T_timetz) &&
|
||||
str[len(str)-3] == ':' {
|
||||
f += ":00"
|
||||
}
|
||||
t, err := time.Parse(f, str)
|
||||
if err != nil {
|
||||
errorf("decode: %s", err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
var errInvalidTimestamp = errors.New("invalid timestamp")
|
||||
|
||||
type timestampParser struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *timestampParser) expect(str string, char byte, pos int) {
|
||||
if p.err != nil {
|
||||
return
|
||||
}
|
||||
if pos+1 > len(str) {
|
||||
p.err = errInvalidTimestamp
|
||||
return
|
||||
}
|
||||
if c := str[pos]; c != char && p.err == nil {
|
||||
p.err = fmt.Errorf("expected '%v' at position %v; got '%v'", char, pos, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *timestampParser) mustAtoi(str string, begin int, end int) int {
|
||||
if p.err != nil {
|
||||
return 0
|
||||
}
|
||||
if begin < 0 || end < 0 || begin > end || end > len(str) {
|
||||
p.err = errInvalidTimestamp
|
||||
return 0
|
||||
}
|
||||
result, err := strconv.Atoi(str[begin:end])
|
||||
if err != nil {
|
||||
if p.err == nil {
|
||||
p.err = fmt.Errorf("expected number; got '%v'", str)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// The location cache caches the time zones typically used by the client.
|
||||
type locationCache struct {
|
||||
cache map[int]*time.Location
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// All connections share the same list of timezones. Benchmarking shows that
|
||||
// about 5% speed could be gained by putting the cache in the connection and
|
||||
// losing the mutex, at the cost of a small amount of memory and a somewhat
|
||||
// significant increase in code complexity.
|
||||
var globalLocationCache = newLocationCache()
|
||||
|
||||
func newLocationCache() *locationCache {
|
||||
return &locationCache{cache: make(map[int]*time.Location)}
|
||||
}
|
||||
|
||||
// Returns the cached timezone for the specified offset, creating and caching
|
||||
// it if necessary.
|
||||
func (c *locationCache) getLocation(offset int) *time.Location {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
location, ok := c.cache[offset]
|
||||
if !ok {
|
||||
location = time.FixedZone("", offset)
|
||||
c.cache[offset] = location
|
||||
}
|
||||
|
||||
return location
|
||||
}
|
||||
|
||||
var infinityTsEnabled = false
|
||||
var infinityTsNegative time.Time
|
||||
var infinityTsPositive time.Time
|
||||
|
||||
const (
|
||||
infinityTsEnabledAlready = "pq: infinity timestamp enabled already"
|
||||
infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive"
|
||||
)
|
||||
|
||||
// EnableInfinityTs controls the handling of Postgres' "-infinity" and
|
||||
// "infinity" "timestamp"s.
|
||||
//
|
||||
// If EnableInfinityTs is not called, "-infinity" and "infinity" will return
|
||||
// []byte("-infinity") and []byte("infinity") respectively, and potentially
|
||||
// cause error "sql: Scan error on column index 0: unsupported driver -> Scan
|
||||
// pair: []uint8 -> *time.Time", when scanning into a time.Time value.
|
||||
//
|
||||
// Once EnableInfinityTs has been called, all connections created using this
|
||||
// driver will decode Postgres' "-infinity" and "infinity" for "timestamp",
|
||||
// "timestamp with time zone" and "date" types to the predefined minimum and
|
||||
// maximum times, respectively. When encoding time.Time values, any time which
|
||||
// equals or precedes the predefined minimum time will be encoded to
|
||||
// "-infinity". Any values at or past the maximum time will similarly be
|
||||
// encoded to "infinity".
|
||||
//
|
||||
// If EnableInfinityTs is called with negative >= positive, it will panic.
|
||||
// Calling EnableInfinityTs after a connection has been established results in
|
||||
// undefined behavior. If EnableInfinityTs is called more than once, it will
|
||||
// panic.
|
||||
func EnableInfinityTs(negative time.Time, positive time.Time) {
|
||||
if infinityTsEnabled {
|
||||
panic(infinityTsEnabledAlready)
|
||||
}
|
||||
if !negative.Before(positive) {
|
||||
panic(infinityTsNegativeMustBeSmaller)
|
||||
}
|
||||
infinityTsEnabled = true
|
||||
infinityTsNegative = negative
|
||||
infinityTsPositive = positive
|
||||
}
|
||||
|
||||
/*
|
||||
* Testing might want to toggle infinityTsEnabled
|
||||
*/
|
||||
func disableInfinityTs() {
|
||||
infinityTsEnabled = false
|
||||
}
|
||||
|
||||
// This is a time function specific to the Postgres default DateStyle
|
||||
// setting ("ISO, MDY"), the only one we currently support. This
|
||||
// accounts for the discrepancies between the parsing available with
|
||||
// time.Parse and the Postgres date formatting quirks.
|
||||
func parseTs(currentLocation *time.Location, str string) interface{} {
|
||||
switch str {
|
||||
case "-infinity":
|
||||
if infinityTsEnabled {
|
||||
return infinityTsNegative
|
||||
}
|
||||
return []byte(str)
|
||||
case "infinity":
|
||||
if infinityTsEnabled {
|
||||
return infinityTsPositive
|
||||
}
|
||||
return []byte(str)
|
||||
}
|
||||
t, err := ParseTimestamp(currentLocation, str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseTimestamp parses Postgres' text format. It returns a time.Time in
|
||||
// currentLocation iff that time's offset agrees with the offset sent from the
|
||||
// Postgres server. Otherwise, ParseTimestamp returns a time.Time with the
|
||||
// fixed offset offset provided by the Postgres server.
|
||||
func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, error) {
|
||||
p := timestampParser{}
|
||||
|
||||
monSep := strings.IndexRune(str, '-')
|
||||
// this is Gregorian year, not ISO Year
|
||||
// In Gregorian system, the year 1 BC is followed by AD 1
|
||||
year := p.mustAtoi(str, 0, monSep)
|
||||
daySep := monSep + 3
|
||||
month := p.mustAtoi(str, monSep+1, daySep)
|
||||
p.expect(str, '-', daySep)
|
||||
timeSep := daySep + 3
|
||||
day := p.mustAtoi(str, daySep+1, timeSep)
|
||||
|
||||
var hour, minute, second int
|
||||
if len(str) > monSep+len("01-01")+1 {
|
||||
p.expect(str, ' ', timeSep)
|
||||
minSep := timeSep + 3
|
||||
p.expect(str, ':', minSep)
|
||||
hour = p.mustAtoi(str, timeSep+1, minSep)
|
||||
secSep := minSep + 3
|
||||
p.expect(str, ':', secSep)
|
||||
minute = p.mustAtoi(str, minSep+1, secSep)
|
||||
secEnd := secSep + 3
|
||||
second = p.mustAtoi(str, secSep+1, secEnd)
|
||||
}
|
||||
remainderIdx := monSep + len("01-01 00:00:00") + 1
|
||||
// Three optional (but ordered) sections follow: the
|
||||
// fractional seconds, the time zone offset, and the BC
|
||||
// designation. We set them up here and adjust the other
|
||||
// offsets if the preceding sections exist.
|
||||
|
||||
nanoSec := 0
|
||||
tzOff := 0
|
||||
|
||||
if remainderIdx < len(str) && str[remainderIdx] == '.' {
|
||||
fracStart := remainderIdx + 1
|
||||
fracOff := strings.IndexAny(str[fracStart:], "-+ ")
|
||||
if fracOff < 0 {
|
||||
fracOff = len(str) - fracStart
|
||||
}
|
||||
fracSec := p.mustAtoi(str, fracStart, fracStart+fracOff)
|
||||
nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff))))
|
||||
|
||||
remainderIdx += fracOff + 1
|
||||
}
|
||||
if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') {
|
||||
// time zone separator is always '-' or '+' (UTC is +00)
|
||||
var tzSign int
|
||||
switch c := str[tzStart]; c {
|
||||
case '-':
|
||||
tzSign = -1
|
||||
case '+':
|
||||
tzSign = +1
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("expected '-' or '+' at position %v; got %v", tzStart, c)
|
||||
}
|
||||
tzHours := p.mustAtoi(str, tzStart+1, tzStart+3)
|
||||
remainderIdx += 3
|
||||
var tzMin, tzSec int
|
||||
if remainderIdx < len(str) && str[remainderIdx] == ':' {
|
||||
tzMin = p.mustAtoi(str, remainderIdx+1, remainderIdx+3)
|
||||
remainderIdx += 3
|
||||
}
|
||||
if remainderIdx < len(str) && str[remainderIdx] == ':' {
|
||||
tzSec = p.mustAtoi(str, remainderIdx+1, remainderIdx+3)
|
||||
remainderIdx += 3
|
||||
}
|
||||
tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec)
|
||||
}
|
||||
var isoYear int
|
||||
if remainderIdx+3 <= len(str) && str[remainderIdx:remainderIdx+3] == " BC" {
|
||||
isoYear = 1 - year
|
||||
remainderIdx += 3
|
||||
} else {
|
||||
isoYear = year
|
||||
}
|
||||
if remainderIdx < len(str) {
|
||||
return time.Time{}, fmt.Errorf("expected end of input, got %v", str[remainderIdx:])
|
||||
}
|
||||
t := time.Date(isoYear, time.Month(month), day,
|
||||
hour, minute, second, nanoSec,
|
||||
globalLocationCache.getLocation(tzOff))
|
||||
|
||||
if currentLocation != nil {
|
||||
// Set the location of the returned Time based on the session's
|
||||
// TimeZone value, but only if the local time zone database agrees with
|
||||
// the remote database on the offset.
|
||||
lt := t.In(currentLocation)
|
||||
_, newOff := lt.Zone()
|
||||
if newOff == tzOff {
|
||||
t = lt
|
||||
}
|
||||
}
|
||||
|
||||
return t, p.err
|
||||
}
|
||||
|
||||
// formatTs formats t into a format postgres understands.
|
||||
func formatTs(t time.Time) []byte {
|
||||
if infinityTsEnabled {
|
||||
// t <= -infinity : ! (t > -infinity)
|
||||
if !t.After(infinityTsNegative) {
|
||||
return []byte("-infinity")
|
||||
}
|
||||
// t >= infinity : ! (!t < infinity)
|
||||
if !t.Before(infinityTsPositive) {
|
||||
return []byte("infinity")
|
||||
}
|
||||
}
|
||||
return FormatTimestamp(t)
|
||||
}
|
||||
|
||||
// FormatTimestamp formats t into Postgres' text format for timestamps.
|
||||
func FormatTimestamp(t time.Time) []byte {
|
||||
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the
|
||||
// minus sign preferred by Go.
|
||||
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
|
||||
bc := false
|
||||
if t.Year() <= 0 {
|
||||
// flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11"
|
||||
t = t.AddDate((-t.Year())*2+1, 0, 0)
|
||||
bc = true
|
||||
}
|
||||
b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00"))
|
||||
|
||||
_, offset := t.Zone()
|
||||
offset = offset % 60
|
||||
if offset != 0 {
|
||||
// RFC3339Nano already printed the minus sign
|
||||
if offset < 0 {
|
||||
offset = -offset
|
||||
}
|
||||
|
||||
b = append(b, ':')
|
||||
if offset < 10 {
|
||||
b = append(b, '0')
|
||||
}
|
||||
b = strconv.AppendInt(b, int64(offset), 10)
|
||||
}
|
||||
|
||||
if bc {
|
||||
b = append(b, " BC"...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Parse a bytea value received from the server. Both "hex" and the legacy
|
||||
// "escape" format are supported.
|
||||
func parseBytea(s []byte) (result []byte, err error) {
|
||||
if len(s) >= 2 && bytes.Equal(s[:2], []byte("\\x")) {
|
||||
// bytea_output = hex
|
||||
s = s[2:] // trim off leading "\\x"
|
||||
result = make([]byte, hex.DecodedLen(len(s)))
|
||||
_, err := hex.Decode(result, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// bytea_output = escape
|
||||
for len(s) > 0 {
|
||||
if s[0] == '\\' {
|
||||
// escaped '\\'
|
||||
if len(s) >= 2 && s[1] == '\\' {
|
||||
result = append(result, '\\')
|
||||
s = s[2:]
|
||||
continue
|
||||
}
|
||||
|
||||
// '\\' followed by an octal number
|
||||
if len(s) < 4 {
|
||||
return nil, fmt.Errorf("invalid bytea sequence %v", s)
|
||||
}
|
||||
r, err := strconv.ParseInt(string(s[1:4]), 8, 9)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse bytea value: %s", err.Error())
|
||||
}
|
||||
result = append(result, byte(r))
|
||||
s = s[4:]
|
||||
} else {
|
||||
// We hit an unescaped, raw byte. Try to read in as many as
|
||||
// possible in one go.
|
||||
i := bytes.IndexByte(s, '\\')
|
||||
if i == -1 {
|
||||
result = append(result, s...)
|
||||
break
|
||||
}
|
||||
result = append(result, s[:i]...)
|
||||
s = s[i:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func encodeBytea(serverVersion int, v []byte) (result []byte) {
|
||||
if serverVersion >= 90000 {
|
||||
// Use the hex format if we know that the server supports it
|
||||
result = make([]byte, 2+hex.EncodedLen(len(v)))
|
||||
result[0] = '\\'
|
||||
result[1] = 'x'
|
||||
hex.Encode(result[2:], v)
|
||||
} else {
|
||||
// .. or resort to "escape"
|
||||
for _, b := range v {
|
||||
if b == '\\' {
|
||||
result = append(result, '\\', '\\')
|
||||
} else if b < 0x20 || b > 0x7e {
|
||||
result = append(result, []byte(fmt.Sprintf("\\%03o", b))...)
|
||||
} else {
|
||||
result = append(result, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NullTime represents a time.Time that may be null. NullTime implements the
|
||||
// sql.Scanner interface so it can be used as a scan destination, similar to
|
||||
// sql.NullString.
|
||||
type NullTime struct {
|
||||
Time time.Time
|
||||
Valid bool // Valid is true if Time is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (nt *NullTime) Scan(value interface{}) error {
|
||||
nt.Time, nt.Valid = value.(time.Time)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (nt NullTime) Value() (driver.Value, error) {
|
||||
if !nt.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return nt.Time, nil
|
||||
}
|
|
@ -0,0 +1,508 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Error severities
|
||||
const (
|
||||
Efatal = "FATAL"
|
||||
Epanic = "PANIC"
|
||||
Ewarning = "WARNING"
|
||||
Enotice = "NOTICE"
|
||||
Edebug = "DEBUG"
|
||||
Einfo = "INFO"
|
||||
Elog = "LOG"
|
||||
)
|
||||
|
||||
// Error represents an error communicating with the server.
|
||||
//
|
||||
// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields
|
||||
type Error struct {
|
||||
Severity string
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Detail string
|
||||
Hint string
|
||||
Position string
|
||||
InternalPosition string
|
||||
InternalQuery string
|
||||
Where string
|
||||
Schema string
|
||||
Table string
|
||||
Column string
|
||||
DataTypeName string
|
||||
Constraint string
|
||||
File string
|
||||
Line string
|
||||
Routine string
|
||||
}
|
||||
|
||||
// ErrorCode is a five-character error code.
|
||||
type ErrorCode string
|
||||
|
||||
// Name returns a more human friendly rendering of the error code, namely the
|
||||
// "condition name".
|
||||
//
|
||||
// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for
|
||||
// details.
|
||||
func (ec ErrorCode) Name() string {
|
||||
return errorCodeNames[ec]
|
||||
}
|
||||
|
||||
// ErrorClass is only the class part of an error code.
|
||||
type ErrorClass string
|
||||
|
||||
// Name returns the condition name of an error class. It is equivalent to the
|
||||
// condition name of the "standard" error code (i.e. the one having the last
|
||||
// three characters "000").
|
||||
func (ec ErrorClass) Name() string {
|
||||
return errorCodeNames[ErrorCode(ec+"000")]
|
||||
}
|
||||
|
||||
// Class returns the error class, e.g. "28".
|
||||
//
|
||||
// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for
|
||||
// details.
|
||||
func (ec ErrorCode) Class() ErrorClass {
|
||||
return ErrorClass(ec[0:2])
|
||||
}
|
||||
|
||||
// errorCodeNames is a mapping between the five-character error codes and the
|
||||
// human readable "condition names". It is derived from the list at
|
||||
// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html
|
||||
var errorCodeNames = map[ErrorCode]string{
|
||||
// Class 00 - Successful Completion
|
||||
"00000": "successful_completion",
|
||||
// Class 01 - Warning
|
||||
"01000": "warning",
|
||||
"0100C": "dynamic_result_sets_returned",
|
||||
"01008": "implicit_zero_bit_padding",
|
||||
"01003": "null_value_eliminated_in_set_function",
|
||||
"01007": "privilege_not_granted",
|
||||
"01006": "privilege_not_revoked",
|
||||
"01004": "string_data_right_truncation",
|
||||
"01P01": "deprecated_feature",
|
||||
// Class 02 - No Data (this is also a warning class per the SQL standard)
|
||||
"02000": "no_data",
|
||||
"02001": "no_additional_dynamic_result_sets_returned",
|
||||
// Class 03 - SQL Statement Not Yet Complete
|
||||
"03000": "sql_statement_not_yet_complete",
|
||||
// Class 08 - Connection Exception
|
||||
"08000": "connection_exception",
|
||||
"08003": "connection_does_not_exist",
|
||||
"08006": "connection_failure",
|
||||
"08001": "sqlclient_unable_to_establish_sqlconnection",
|
||||
"08004": "sqlserver_rejected_establishment_of_sqlconnection",
|
||||
"08007": "transaction_resolution_unknown",
|
||||
"08P01": "protocol_violation",
|
||||
// Class 09 - Triggered Action Exception
|
||||
"09000": "triggered_action_exception",
|
||||
// Class 0A - Feature Not Supported
|
||||
"0A000": "feature_not_supported",
|
||||
// Class 0B - Invalid Transaction Initiation
|
||||
"0B000": "invalid_transaction_initiation",
|
||||
// Class 0F - Locator Exception
|
||||
"0F000": "locator_exception",
|
||||
"0F001": "invalid_locator_specification",
|
||||
// Class 0L - Invalid Grantor
|
||||
"0L000": "invalid_grantor",
|
||||
"0LP01": "invalid_grant_operation",
|
||||
// Class 0P - Invalid Role Specification
|
||||
"0P000": "invalid_role_specification",
|
||||
// Class 0Z - Diagnostics Exception
|
||||
"0Z000": "diagnostics_exception",
|
||||
"0Z002": "stacked_diagnostics_accessed_without_active_handler",
|
||||
// Class 20 - Case Not Found
|
||||
"20000": "case_not_found",
|
||||
// Class 21 - Cardinality Violation
|
||||
"21000": "cardinality_violation",
|
||||
// Class 22 - Data Exception
|
||||
"22000": "data_exception",
|
||||
"2202E": "array_subscript_error",
|
||||
"22021": "character_not_in_repertoire",
|
||||
"22008": "datetime_field_overflow",
|
||||
"22012": "division_by_zero",
|
||||
"22005": "error_in_assignment",
|
||||
"2200B": "escape_character_conflict",
|
||||
"22022": "indicator_overflow",
|
||||
"22015": "interval_field_overflow",
|
||||
"2201E": "invalid_argument_for_logarithm",
|
||||
"22014": "invalid_argument_for_ntile_function",
|
||||
"22016": "invalid_argument_for_nth_value_function",
|
||||
"2201F": "invalid_argument_for_power_function",
|
||||
"2201G": "invalid_argument_for_width_bucket_function",
|
||||
"22018": "invalid_character_value_for_cast",
|
||||
"22007": "invalid_datetime_format",
|
||||
"22019": "invalid_escape_character",
|
||||
"2200D": "invalid_escape_octet",
|
||||
"22025": "invalid_escape_sequence",
|
||||
"22P06": "nonstandard_use_of_escape_character",
|
||||
"22010": "invalid_indicator_parameter_value",
|
||||
"22023": "invalid_parameter_value",
|
||||
"2201B": "invalid_regular_expression",
|
||||
"2201W": "invalid_row_count_in_limit_clause",
|
||||
"2201X": "invalid_row_count_in_result_offset_clause",
|
||||
"22009": "invalid_time_zone_displacement_value",
|
||||
"2200C": "invalid_use_of_escape_character",
|
||||
"2200G": "most_specific_type_mismatch",
|
||||
"22004": "null_value_not_allowed",
|
||||
"22002": "null_value_no_indicator_parameter",
|
||||
"22003": "numeric_value_out_of_range",
|
||||
"22026": "string_data_length_mismatch",
|
||||
"22001": "string_data_right_truncation",
|
||||
"22011": "substring_error",
|
||||
"22027": "trim_error",
|
||||
"22024": "unterminated_c_string",
|
||||
"2200F": "zero_length_character_string",
|
||||
"22P01": "floating_point_exception",
|
||||
"22P02": "invalid_text_representation",
|
||||
"22P03": "invalid_binary_representation",
|
||||
"22P04": "bad_copy_file_format",
|
||||
"22P05": "untranslatable_character",
|
||||
"2200L": "not_an_xml_document",
|
||||
"2200M": "invalid_xml_document",
|
||||
"2200N": "invalid_xml_content",
|
||||
"2200S": "invalid_xml_comment",
|
||||
"2200T": "invalid_xml_processing_instruction",
|
||||
// Class 23 - Integrity Constraint Violation
|
||||
"23000": "integrity_constraint_violation",
|
||||
"23001": "restrict_violation",
|
||||
"23502": "not_null_violation",
|
||||
"23503": "foreign_key_violation",
|
||||
"23505": "unique_violation",
|
||||
"23514": "check_violation",
|
||||
"23P01": "exclusion_violation",
|
||||
// Class 24 - Invalid Cursor State
|
||||
"24000": "invalid_cursor_state",
|
||||
// Class 25 - Invalid Transaction State
|
||||
"25000": "invalid_transaction_state",
|
||||
"25001": "active_sql_transaction",
|
||||
"25002": "branch_transaction_already_active",
|
||||
"25008": "held_cursor_requires_same_isolation_level",
|
||||
"25003": "inappropriate_access_mode_for_branch_transaction",
|
||||
"25004": "inappropriate_isolation_level_for_branch_transaction",
|
||||
"25005": "no_active_sql_transaction_for_branch_transaction",
|
||||
"25006": "read_only_sql_transaction",
|
||||
"25007": "schema_and_data_statement_mixing_not_supported",
|
||||
"25P01": "no_active_sql_transaction",
|
||||
"25P02": "in_failed_sql_transaction",
|
||||
// Class 26 - Invalid SQL Statement Name
|
||||
"26000": "invalid_sql_statement_name",
|
||||
// Class 27 - Triggered Data Change Violation
|
||||
"27000": "triggered_data_change_violation",
|
||||
// Class 28 - Invalid Authorization Specification
|
||||
"28000": "invalid_authorization_specification",
|
||||
"28P01": "invalid_password",
|
||||
// Class 2B - Dependent Privilege Descriptors Still Exist
|
||||
"2B000": "dependent_privilege_descriptors_still_exist",
|
||||
"2BP01": "dependent_objects_still_exist",
|
||||
// Class 2D - Invalid Transaction Termination
|
||||
"2D000": "invalid_transaction_termination",
|
||||
// Class 2F - SQL Routine Exception
|
||||
"2F000": "sql_routine_exception",
|
||||
"2F005": "function_executed_no_return_statement",
|
||||
"2F002": "modifying_sql_data_not_permitted",
|
||||
"2F003": "prohibited_sql_statement_attempted",
|
||||
"2F004": "reading_sql_data_not_permitted",
|
||||
// Class 34 - Invalid Cursor Name
|
||||
"34000": "invalid_cursor_name",
|
||||
// Class 38 - External Routine Exception
|
||||
"38000": "external_routine_exception",
|
||||
"38001": "containing_sql_not_permitted",
|
||||
"38002": "modifying_sql_data_not_permitted",
|
||||
"38003": "prohibited_sql_statement_attempted",
|
||||
"38004": "reading_sql_data_not_permitted",
|
||||
// Class 39 - External Routine Invocation Exception
|
||||
"39000": "external_routine_invocation_exception",
|
||||
"39001": "invalid_sqlstate_returned",
|
||||
"39004": "null_value_not_allowed",
|
||||
"39P01": "trigger_protocol_violated",
|
||||
"39P02": "srf_protocol_violated",
|
||||
// Class 3B - Savepoint Exception
|
||||
"3B000": "savepoint_exception",
|
||||
"3B001": "invalid_savepoint_specification",
|
||||
// Class 3D - Invalid Catalog Name
|
||||
"3D000": "invalid_catalog_name",
|
||||
// Class 3F - Invalid Schema Name
|
||||
"3F000": "invalid_schema_name",
|
||||
// Class 40 - Transaction Rollback
|
||||
"40000": "transaction_rollback",
|
||||
"40002": "transaction_integrity_constraint_violation",
|
||||
"40001": "serialization_failure",
|
||||
"40003": "statement_completion_unknown",
|
||||
"40P01": "deadlock_detected",
|
||||
// Class 42 - Syntax Error or Access Rule Violation
|
||||
"42000": "syntax_error_or_access_rule_violation",
|
||||
"42601": "syntax_error",
|
||||
"42501": "insufficient_privilege",
|
||||
"42846": "cannot_coerce",
|
||||
"42803": "grouping_error",
|
||||
"42P20": "windowing_error",
|
||||
"42P19": "invalid_recursion",
|
||||
"42830": "invalid_foreign_key",
|
||||
"42602": "invalid_name",
|
||||
"42622": "name_too_long",
|
||||
"42939": "reserved_name",
|
||||
"42804": "datatype_mismatch",
|
||||
"42P18": "indeterminate_datatype",
|
||||
"42P21": "collation_mismatch",
|
||||
"42P22": "indeterminate_collation",
|
||||
"42809": "wrong_object_type",
|
||||
"42703": "undefined_column",
|
||||
"42883": "undefined_function",
|
||||
"42P01": "undefined_table",
|
||||
"42P02": "undefined_parameter",
|
||||
"42704": "undefined_object",
|
||||
"42701": "duplicate_column",
|
||||
"42P03": "duplicate_cursor",
|
||||
"42P04": "duplicate_database",
|
||||
"42723": "duplicate_function",
|
||||
"42P05": "duplicate_prepared_statement",
|
||||
"42P06": "duplicate_schema",
|
||||
"42P07": "duplicate_table",
|
||||
"42712": "duplicate_alias",
|
||||
"42710": "duplicate_object",
|
||||
"42702": "ambiguous_column",
|
||||
"42725": "ambiguous_function",
|
||||
"42P08": "ambiguous_parameter",
|
||||
"42P09": "ambiguous_alias",
|
||||
"42P10": "invalid_column_reference",
|
||||
"42611": "invalid_column_definition",
|
||||
"42P11": "invalid_cursor_definition",
|
||||
"42P12": "invalid_database_definition",
|
||||
"42P13": "invalid_function_definition",
|
||||
"42P14": "invalid_prepared_statement_definition",
|
||||
"42P15": "invalid_schema_definition",
|
||||
"42P16": "invalid_table_definition",
|
||||
"42P17": "invalid_object_definition",
|
||||
// Class 44 - WITH CHECK OPTION Violation
|
||||
"44000": "with_check_option_violation",
|
||||
// Class 53 - Insufficient Resources
|
||||
"53000": "insufficient_resources",
|
||||
"53100": "disk_full",
|
||||
"53200": "out_of_memory",
|
||||
"53300": "too_many_connections",
|
||||
"53400": "configuration_limit_exceeded",
|
||||
// Class 54 - Program Limit Exceeded
|
||||
"54000": "program_limit_exceeded",
|
||||
"54001": "statement_too_complex",
|
||||
"54011": "too_many_columns",
|
||||
"54023": "too_many_arguments",
|
||||
// Class 55 - Object Not In Prerequisite State
|
||||
"55000": "object_not_in_prerequisite_state",
|
||||
"55006": "object_in_use",
|
||||
"55P02": "cant_change_runtime_param",
|
||||
"55P03": "lock_not_available",
|
||||
// Class 57 - Operator Intervention
|
||||
"57000": "operator_intervention",
|
||||
"57014": "query_canceled",
|
||||
"57P01": "admin_shutdown",
|
||||
"57P02": "crash_shutdown",
|
||||
"57P03": "cannot_connect_now",
|
||||
"57P04": "database_dropped",
|
||||
// Class 58 - System Error (errors external to PostgreSQL itself)
|
||||
"58000": "system_error",
|
||||
"58030": "io_error",
|
||||
"58P01": "undefined_file",
|
||||
"58P02": "duplicate_file",
|
||||
// Class F0 - Configuration File Error
|
||||
"F0000": "config_file_error",
|
||||
"F0001": "lock_file_exists",
|
||||
// Class HV - Foreign Data Wrapper Error (SQL/MED)
|
||||
"HV000": "fdw_error",
|
||||
"HV005": "fdw_column_name_not_found",
|
||||
"HV002": "fdw_dynamic_parameter_value_needed",
|
||||
"HV010": "fdw_function_sequence_error",
|
||||
"HV021": "fdw_inconsistent_descriptor_information",
|
||||
"HV024": "fdw_invalid_attribute_value",
|
||||
"HV007": "fdw_invalid_column_name",
|
||||
"HV008": "fdw_invalid_column_number",
|
||||
"HV004": "fdw_invalid_data_type",
|
||||
"HV006": "fdw_invalid_data_type_descriptors",
|
||||
"HV091": "fdw_invalid_descriptor_field_identifier",
|
||||
"HV00B": "fdw_invalid_handle",
|
||||
"HV00C": "fdw_invalid_option_index",
|
||||
"HV00D": "fdw_invalid_option_name",
|
||||
"HV090": "fdw_invalid_string_length_or_buffer_length",
|
||||
"HV00A": "fdw_invalid_string_format",
|
||||
"HV009": "fdw_invalid_use_of_null_pointer",
|
||||
"HV014": "fdw_too_many_handles",
|
||||
"HV001": "fdw_out_of_memory",
|
||||
"HV00P": "fdw_no_schemas",
|
||||
"HV00J": "fdw_option_name_not_found",
|
||||
"HV00K": "fdw_reply_handle",
|
||||
"HV00Q": "fdw_schema_not_found",
|
||||
"HV00R": "fdw_table_not_found",
|
||||
"HV00L": "fdw_unable_to_create_execution",
|
||||
"HV00M": "fdw_unable_to_create_reply",
|
||||
"HV00N": "fdw_unable_to_establish_connection",
|
||||
// Class P0 - PL/pgSQL Error
|
||||
"P0000": "plpgsql_error",
|
||||
"P0001": "raise_exception",
|
||||
"P0002": "no_data_found",
|
||||
"P0003": "too_many_rows",
|
||||
// Class XX - Internal Error
|
||||
"XX000": "internal_error",
|
||||
"XX001": "data_corrupted",
|
||||
"XX002": "index_corrupted",
|
||||
}
|
||||
|
||||
func parseError(r *readBuf) *Error {
|
||||
err := new(Error)
|
||||
for t := r.byte(); t != 0; t = r.byte() {
|
||||
msg := r.string()
|
||||
switch t {
|
||||
case 'S':
|
||||
err.Severity = msg
|
||||
case 'C':
|
||||
err.Code = ErrorCode(msg)
|
||||
case 'M':
|
||||
err.Message = msg
|
||||
case 'D':
|
||||
err.Detail = msg
|
||||
case 'H':
|
||||
err.Hint = msg
|
||||
case 'P':
|
||||
err.Position = msg
|
||||
case 'p':
|
||||
err.InternalPosition = msg
|
||||
case 'q':
|
||||
err.InternalQuery = msg
|
||||
case 'W':
|
||||
err.Where = msg
|
||||
case 's':
|
||||
err.Schema = msg
|
||||
case 't':
|
||||
err.Table = msg
|
||||
case 'c':
|
||||
err.Column = msg
|
||||
case 'd':
|
||||
err.DataTypeName = msg
|
||||
case 'n':
|
||||
err.Constraint = msg
|
||||
case 'F':
|
||||
err.File = msg
|
||||
case 'L':
|
||||
err.Line = msg
|
||||
case 'R':
|
||||
err.Routine = msg
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Fatal returns true if the Error Severity is fatal.
|
||||
func (err *Error) Fatal() bool {
|
||||
return err.Severity == Efatal
|
||||
}
|
||||
|
||||
// Get implements the legacy PGError interface. New code should use the fields
|
||||
// of the Error struct directly.
|
||||
func (err *Error) Get(k byte) (v string) {
|
||||
switch k {
|
||||
case 'S':
|
||||
return err.Severity
|
||||
case 'C':
|
||||
return string(err.Code)
|
||||
case 'M':
|
||||
return err.Message
|
||||
case 'D':
|
||||
return err.Detail
|
||||
case 'H':
|
||||
return err.Hint
|
||||
case 'P':
|
||||
return err.Position
|
||||
case 'p':
|
||||
return err.InternalPosition
|
||||
case 'q':
|
||||
return err.InternalQuery
|
||||
case 'W':
|
||||
return err.Where
|
||||
case 's':
|
||||
return err.Schema
|
||||
case 't':
|
||||
return err.Table
|
||||
case 'c':
|
||||
return err.Column
|
||||
case 'd':
|
||||
return err.DataTypeName
|
||||
case 'n':
|
||||
return err.Constraint
|
||||
case 'F':
|
||||
return err.File
|
||||
case 'L':
|
||||
return err.Line
|
||||
case 'R':
|
||||
return err.Routine
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err Error) Error() string {
|
||||
return "pq: " + err.Message
|
||||
}
|
||||
|
||||
// PGError is an interface used by previous versions of pq. It is provided
|
||||
// only to support legacy code. New code should use the Error type.
|
||||
type PGError interface {
|
||||
Error() string
|
||||
Fatal() bool
|
||||
Get(k byte) (v string)
|
||||
}
|
||||
|
||||
func errorf(s string, args ...interface{}) {
|
||||
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))
|
||||
}
|
||||
|
||||
func errRecoverNoErrBadConn(err *error) {
|
||||
e := recover()
|
||||
if e == nil {
|
||||
// Do nothing
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
*err, ok = e.(error)
|
||||
if !ok {
|
||||
*err = fmt.Errorf("pq: unexpected error: %#v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) errRecover(err *error) {
|
||||
e := recover()
|
||||
switch v := e.(type) {
|
||||
case nil:
|
||||
// Do nothing
|
||||
case runtime.Error:
|
||||
c.bad = true
|
||||
panic(v)
|
||||
case *Error:
|
||||
if v.Fatal() {
|
||||
*err = driver.ErrBadConn
|
||||
} else {
|
||||
*err = v
|
||||
}
|
||||
case *net.OpError:
|
||||
*err = driver.ErrBadConn
|
||||
case error:
|
||||
if v == io.EOF || v.(error).Error() == "remote error: handshake failure" {
|
||||
*err = driver.ErrBadConn
|
||||
} else {
|
||||
*err = v
|
||||
}
|
||||
|
||||
default:
|
||||
c.bad = true
|
||||
panic(fmt.Sprintf("unknown error: %#v", e))
|
||||
}
|
||||
|
||||
// Any time we return ErrBadConn, we need to remember it since *Tx doesn't
|
||||
// mark the connection bad in database/sql.
|
||||
if *err == driver.ErrBadConn {
|
||||
c.bad = true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,782 @@
|
|||
package pq
|
||||
|
||||
// Package pq is a pure Go Postgres driver for the database/sql package.
|
||||
// This module contains support for Postgres LISTEN/NOTIFY.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification represents a single notification from the database.
|
||||
type Notification struct {
|
||||
// Process ID (PID) of the notifying postgres backend.
|
||||
BePid int
|
||||
// Name of the channel the notification was sent on.
|
||||
Channel string
|
||||
// Payload, or the empty string if unspecified.
|
||||
Extra string
|
||||
}
|
||||
|
||||
func recvNotification(r *readBuf) *Notification {
|
||||
bePid := r.int32()
|
||||
channel := r.string()
|
||||
extra := r.string()
|
||||
|
||||
return &Notification{bePid, channel, extra}
|
||||
}
|
||||
|
||||
const (
|
||||
connStateIdle int32 = iota
|
||||
connStateExpectResponse
|
||||
connStateExpectReadyForQuery
|
||||
)
|
||||
|
||||
type message struct {
|
||||
typ byte
|
||||
err error
|
||||
}
|
||||
|
||||
var errListenerConnClosed = errors.New("pq: ListenerConn has been closed")
|
||||
|
||||
// ListenerConn is a low-level interface for waiting for notifications. You
|
||||
// should use Listener instead.
|
||||
type ListenerConn struct {
|
||||
// guards cn and err
|
||||
connectionLock sync.Mutex
|
||||
cn *conn
|
||||
err error
|
||||
|
||||
connState int32
|
||||
|
||||
// the sending goroutine will be holding this lock
|
||||
senderLock sync.Mutex
|
||||
|
||||
notificationChan chan<- *Notification
|
||||
|
||||
replyChan chan message
|
||||
}
|
||||
|
||||
// Creates a new ListenerConn. Use NewListener instead.
|
||||
func NewListenerConn(name string, notificationChan chan<- *Notification) (*ListenerConn, error) {
|
||||
return newDialListenerConn(defaultDialer{}, name, notificationChan)
|
||||
}
|
||||
|
||||
func newDialListenerConn(d Dialer, name string, c chan<- *Notification) (*ListenerConn, error) {
|
||||
cn, err := DialOpen(d, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &ListenerConn{
|
||||
cn: cn.(*conn),
|
||||
notificationChan: c,
|
||||
connState: connStateIdle,
|
||||
replyChan: make(chan message, 2),
|
||||
}
|
||||
|
||||
go l.listenerConnMain()
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// We can only allow one goroutine at a time to be running a query on the
|
||||
// connection for various reasons, so the goroutine sending on the connection
|
||||
// must be holding senderLock.
|
||||
//
|
||||
// Returns an error if an unrecoverable error has occurred and the ListenerConn
|
||||
// should be abandoned.
|
||||
func (l *ListenerConn) acquireSenderLock() error {
|
||||
// we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery
|
||||
l.senderLock.Lock()
|
||||
|
||||
l.connectionLock.Lock()
|
||||
err := l.err
|
||||
l.connectionLock.Unlock()
|
||||
if err != nil {
|
||||
l.senderLock.Unlock()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ListenerConn) releaseSenderLock() {
|
||||
l.senderLock.Unlock()
|
||||
}
|
||||
|
||||
// setState advances the protocol state to newState. Returns false if moving
|
||||
// to that state from the current state is not allowed.
|
||||
func (l *ListenerConn) setState(newState int32) bool {
|
||||
var expectedState int32
|
||||
|
||||
switch newState {
|
||||
case connStateIdle:
|
||||
expectedState = connStateExpectReadyForQuery
|
||||
case connStateExpectResponse:
|
||||
expectedState = connStateIdle
|
||||
case connStateExpectReadyForQuery:
|
||||
expectedState = connStateExpectResponse
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected listenerConnState %d", newState))
|
||||
}
|
||||
|
||||
return atomic.CompareAndSwapInt32(&l.connState, expectedState, newState)
|
||||
}
|
||||
|
||||
// Main logic is here: receive messages from the postgres backend, forward
|
||||
// notifications and query replies and keep the internal state in sync with the
|
||||
// protocol state. Returns when the connection has been lost, is about to go
|
||||
// away or should be discarded because we couldn't agree on the state with the
|
||||
// server backend.
|
||||
func (l *ListenerConn) listenerConnLoop() (err error) {
|
||||
defer errRecoverNoErrBadConn(&err)
|
||||
|
||||
r := &readBuf{}
|
||||
for {
|
||||
t, err := l.cn.recvMessage(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch t {
|
||||
case 'A':
|
||||
// recvNotification copies all the data so we don't need to worry
|
||||
// about the scratch buffer being overwritten.
|
||||
l.notificationChan <- recvNotification(r)
|
||||
|
||||
case 'T', 'D':
|
||||
// only used by tests; ignore
|
||||
|
||||
case 'E':
|
||||
// We might receive an ErrorResponse even when not in a query; it
|
||||
// is expected that the server will close the connection after
|
||||
// that, but we should make sure that the error we display is the
|
||||
// one from the stray ErrorResponse, not io.ErrUnexpectedEOF.
|
||||
if !l.setState(connStateExpectReadyForQuery) {
|
||||
return parseError(r)
|
||||
}
|
||||
l.replyChan <- message{t, parseError(r)}
|
||||
|
||||
case 'C', 'I':
|
||||
if !l.setState(connStateExpectReadyForQuery) {
|
||||
// protocol out of sync
|
||||
return fmt.Errorf("unexpected CommandComplete")
|
||||
}
|
||||
// ExecSimpleQuery doesn't need to know about this message
|
||||
|
||||
case 'Z':
|
||||
if !l.setState(connStateIdle) {
|
||||
// protocol out of sync
|
||||
return fmt.Errorf("unexpected ReadyForQuery")
|
||||
}
|
||||
l.replyChan <- message{t, nil}
|
||||
|
||||
case 'N', 'S':
|
||||
// ignore
|
||||
default:
|
||||
return fmt.Errorf("unexpected message %q from server in listenerConnLoop", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is the main routine for the goroutine receiving on the database
|
||||
// connection. Most of the main logic is in listenerConnLoop.
|
||||
func (l *ListenerConn) listenerConnMain() {
|
||||
err := l.listenerConnLoop()
|
||||
|
||||
// listenerConnLoop terminated; we're done, but we still have to clean up.
|
||||
// Make sure nobody tries to start any new queries by making sure the err
|
||||
// pointer is set. It is important that we do not overwrite its value; a
|
||||
// connection could be closed by either this goroutine or one sending on
|
||||
// the connection -- whoever closes the connection is assumed to have the
|
||||
// more meaningful error message (as the other one will probably get
|
||||
// net.errClosed), so that goroutine sets the error we expose while the
|
||||
// other error is discarded. If the connection is lost while two
|
||||
// goroutines are operating on the socket, it probably doesn't matter which
|
||||
// error we expose so we don't try to do anything more complex.
|
||||
l.connectionLock.Lock()
|
||||
if l.err == nil {
|
||||
l.err = err
|
||||
}
|
||||
l.cn.Close()
|
||||
l.connectionLock.Unlock()
|
||||
|
||||
// There might be a query in-flight; make sure nobody's waiting for a
|
||||
// response to it, since there's not going to be one.
|
||||
close(l.replyChan)
|
||||
|
||||
// let the listener know we're done
|
||||
close(l.notificationChan)
|
||||
|
||||
// this ListenerConn is done
|
||||
}
|
||||
|
||||
// Send a LISTEN query to the server. See ExecSimpleQuery.
|
||||
func (l *ListenerConn) Listen(channel string) (bool, error) {
|
||||
return l.ExecSimpleQuery("LISTEN " + QuoteIdentifier(channel))
|
||||
}
|
||||
|
||||
// Send an UNLISTEN query to the server. See ExecSimpleQuery.
|
||||
func (l *ListenerConn) Unlisten(channel string) (bool, error) {
|
||||
return l.ExecSimpleQuery("UNLISTEN " + QuoteIdentifier(channel))
|
||||
}
|
||||
|
||||
// Send `UNLISTEN *` to the server. See ExecSimpleQuery.
|
||||
func (l *ListenerConn) UnlistenAll() (bool, error) {
|
||||
return l.ExecSimpleQuery("UNLISTEN *")
|
||||
}
|
||||
|
||||
// Ping the remote server to make sure it's alive. Non-nil error means the
|
||||
// connection has failed and should be abandoned.
|
||||
func (l *ListenerConn) Ping() error {
|
||||
sent, err := l.ExecSimpleQuery("")
|
||||
if !sent {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
// shouldn't happen
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Attempt to send a query on the connection. Returns an error if sending the
|
||||
// query failed, and the caller should initiate closure of this connection.
|
||||
// The caller must be holding senderLock (see acquireSenderLock and
|
||||
// releaseSenderLock).
|
||||
func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
|
||||
defer errRecoverNoErrBadConn(&err)
|
||||
|
||||
// must set connection state before sending the query
|
||||
if !l.setState(connStateExpectResponse) {
|
||||
panic("two queries running at the same time")
|
||||
}
|
||||
|
||||
// Can't use l.cn.writeBuf here because it uses the scratch buffer which
|
||||
// might get overwritten by listenerConnLoop.
|
||||
b := &writeBuf{
|
||||
buf: []byte("Q\x00\x00\x00\x00"),
|
||||
pos: 1,
|
||||
}
|
||||
b.string(q)
|
||||
l.cn.send(b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute a "simple query" (i.e. one with no bindable parameters) on the
|
||||
// connection. The possible return values are:
|
||||
// 1) "executed" is true; the query was executed to completion on the
|
||||
// database server. If the query failed, err will be set to the error
|
||||
// returned by the database, otherwise err will be nil.
|
||||
// 2) If "executed" is false, the query could not be executed on the remote
|
||||
// server. err will be non-nil.
|
||||
//
|
||||
// After a call to ExecSimpleQuery has returned an executed=false value, the
|
||||
// connection has either been closed or will be closed shortly thereafter, and
|
||||
// all subsequently executed queries will return an error.
|
||||
func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
|
||||
if err = l.acquireSenderLock(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer l.releaseSenderLock()
|
||||
|
||||
err = l.sendSimpleQuery(q)
|
||||
if err != nil {
|
||||
// We can't know what state the protocol is in, so we need to abandon
|
||||
// this connection.
|
||||
l.connectionLock.Lock()
|
||||
// Set the error pointer if it hasn't been set already; see
|
||||
// listenerConnMain.
|
||||
if l.err == nil {
|
||||
l.err = err
|
||||
}
|
||||
l.connectionLock.Unlock()
|
||||
l.cn.c.Close()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// now we just wait for a reply..
|
||||
for {
|
||||
m, ok := <-l.replyChan
|
||||
if !ok {
|
||||
// We lost the connection to server, don't bother waiting for a
|
||||
// a response. err should have been set already.
|
||||
l.connectionLock.Lock()
|
||||
err := l.err
|
||||
l.connectionLock.Unlock()
|
||||
return false, err
|
||||
}
|
||||
switch m.typ {
|
||||
case 'Z':
|
||||
// sanity check
|
||||
if m.err != nil {
|
||||
panic("m.err != nil")
|
||||
}
|
||||
// done; err might or might not be set
|
||||
return true, err
|
||||
|
||||
case 'E':
|
||||
// sanity check
|
||||
if m.err == nil {
|
||||
panic("m.err == nil")
|
||||
}
|
||||
// server responded with an error; ReadyForQuery to follow
|
||||
err = m.err
|
||||
|
||||
default:
|
||||
return false, fmt.Errorf("unknown response for simple query: %q", m.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListenerConn) Close() error {
|
||||
l.connectionLock.Lock()
|
||||
if l.err != nil {
|
||||
l.connectionLock.Unlock()
|
||||
return errListenerConnClosed
|
||||
}
|
||||
l.err = errListenerConnClosed
|
||||
l.connectionLock.Unlock()
|
||||
// We can't send anything on the connection without holding senderLock.
|
||||
// Simply close the net.Conn to wake up everyone operating on it.
|
||||
return l.cn.c.Close()
|
||||
}
|
||||
|
||||
// Err() returns the reason the connection was closed. It is not safe to call
|
||||
// this function until l.Notify has been closed.
|
||||
func (l *ListenerConn) Err() error {
|
||||
return l.err
|
||||
}
|
||||
|
||||
var errListenerClosed = errors.New("pq: Listener has been closed")
|
||||
|
||||
var ErrChannelAlreadyOpen = errors.New("pq: channel is already open")
|
||||
var ErrChannelNotOpen = errors.New("pq: channel is not open")
|
||||
|
||||
type ListenerEventType int
|
||||
|
||||
const (
|
||||
// Emitted only when the database connection has been initially
|
||||
// initialized. err will always be nil.
|
||||
ListenerEventConnected ListenerEventType = iota
|
||||
|
||||
// Emitted after a database connection has been lost, either because of an
|
||||
// error or because Close has been called. err will be set to the reason
|
||||
// the database connection was lost.
|
||||
ListenerEventDisconnected
|
||||
|
||||
// Emitted after a database connection has been re-established after
|
||||
// connection loss. err will always be nil. After this event has been
|
||||
// emitted, a nil pq.Notification is sent on the Listener.Notify channel.
|
||||
ListenerEventReconnected
|
||||
|
||||
// Emitted after a connection to the database was attempted, but failed.
|
||||
// err will be set to an error describing why the connection attempt did
|
||||
// not succeed.
|
||||
ListenerEventConnectionAttemptFailed
|
||||
)
|
||||
|
||||
type EventCallbackType func(event ListenerEventType, err error)
|
||||
|
||||
// Listener provides an interface for listening to notifications from a
|
||||
// PostgreSQL database. For general usage information, see section
|
||||
// "Notifications".
|
||||
//
|
||||
// Listener can safely be used from concurrently running goroutines.
|
||||
type Listener struct {
|
||||
// Channel for receiving notifications from the database. In some cases a
|
||||
// nil value will be sent. See section "Notifications" above.
|
||||
Notify chan *Notification
|
||||
|
||||
name string
|
||||
minReconnectInterval time.Duration
|
||||
maxReconnectInterval time.Duration
|
||||
dialer Dialer
|
||||
eventCallback EventCallbackType
|
||||
|
||||
lock sync.Mutex
|
||||
isClosed bool
|
||||
reconnectCond *sync.Cond
|
||||
cn *ListenerConn
|
||||
connNotificationChan <-chan *Notification
|
||||
channels map[string]struct{}
|
||||
}
|
||||
|
||||
// NewListener creates a new database connection dedicated to LISTEN / NOTIFY.
|
||||
//
|
||||
// name should be set to a connection string to be used to establish the
|
||||
// database connection (see section "Connection String Parameters" above).
|
||||
//
|
||||
// minReconnectInterval controls the duration to wait before trying to
|
||||
// re-establish the database connection after connection loss. After each
|
||||
// consecutive failure this interval is doubled, until maxReconnectInterval is
|
||||
// reached. Successfully completing the connection establishment procedure
|
||||
// resets the interval back to minReconnectInterval.
|
||||
//
|
||||
// The last parameter eventCallback can be set to a function which will be
|
||||
// called by the Listener when the state of the underlying database connection
|
||||
// changes. This callback will be called by the goroutine which dispatches the
|
||||
// notifications over the Notify channel, so you should try to avoid doing
|
||||
// potentially time-consuming operations from the callback.
|
||||
func NewListener(name string,
|
||||
minReconnectInterval time.Duration,
|
||||
maxReconnectInterval time.Duration,
|
||||
eventCallback EventCallbackType) *Listener {
|
||||
return NewDialListener(defaultDialer{}, name, minReconnectInterval, maxReconnectInterval, eventCallback)
|
||||
}
|
||||
|
||||
// NewDialListener is like NewListener but it takes a Dialer.
|
||||
func NewDialListener(d Dialer,
|
||||
name string,
|
||||
minReconnectInterval time.Duration,
|
||||
maxReconnectInterval time.Duration,
|
||||
eventCallback EventCallbackType) *Listener {
|
||||
|
||||
l := &Listener{
|
||||
name: name,
|
||||
minReconnectInterval: minReconnectInterval,
|
||||
maxReconnectInterval: maxReconnectInterval,
|
||||
dialer: d,
|
||||
eventCallback: eventCallback,
|
||||
|
||||
channels: make(map[string]struct{}),
|
||||
|
||||
Notify: make(chan *Notification, 32),
|
||||
}
|
||||
l.reconnectCond = sync.NewCond(&l.lock)
|
||||
|
||||
go l.listenerMain()
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Returns the notification channel for this listener. This is the same
|
||||
// channel as Notify, and will not be recreated during the life time of the
|
||||
// Listener.
|
||||
func (l *Listener) NotificationChannel() <-chan *Notification {
|
||||
return l.Notify
|
||||
}
|
||||
|
||||
// Listen starts listening for notifications on a channel. Calls to this
|
||||
// function will block until an acknowledgement has been received from the
|
||||
// server. Note that Listener automatically re-establishes the connection
|
||||
// after connection loss, so this function may block indefinitely if the
|
||||
// connection can not be re-established.
|
||||
//
|
||||
// Listen will only fail in three conditions:
|
||||
// 1) The channel is already open. The returned error will be
|
||||
// ErrChannelAlreadyOpen.
|
||||
// 2) The query was executed on the remote server, but PostgreSQL returned an
|
||||
// error message in response to the query. The returned error will be a
|
||||
// pq.Error containing the information the server supplied.
|
||||
// 3) Close is called on the Listener before the request could be completed.
|
||||
//
|
||||
// The channel name is case-sensitive.
|
||||
func (l *Listener) Listen(channel string) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
|
||||
// The server allows you to issue a LISTEN on a channel which is already
|
||||
// open, but it seems useful to be able to detect this case to spot for
|
||||
// mistakes in application logic. If the application genuinely does't
|
||||
// care, it can check the exported error and ignore it.
|
||||
_, exists := l.channels[channel]
|
||||
if exists {
|
||||
return ErrChannelAlreadyOpen
|
||||
}
|
||||
|
||||
if l.cn != nil {
|
||||
// If gotResponse is true but error is set, the query was executed on
|
||||
// the remote server, but resulted in an error. This should be
|
||||
// relatively rare, so it's fine if we just pass the error to our
|
||||
// caller. However, if gotResponse is false, we could not complete the
|
||||
// query on the remote server and our underlying connection is about
|
||||
// to go away, so we only add relname to l.channels, and wait for
|
||||
// resync() to take care of the rest.
|
||||
gotResponse, err := l.cn.Listen(channel)
|
||||
if gotResponse && err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
l.channels[channel] = struct{}{}
|
||||
for l.cn == nil {
|
||||
l.reconnectCond.Wait()
|
||||
// we let go of the mutex for a while
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlisten removes a channel from the Listener's channel list. Returns
|
||||
// ErrChannelNotOpen if the Listener is not listening on the specified channel.
|
||||
// Returns immediately with no error if there is no connection. Note that you
|
||||
// might still get notifications for this channel even after Unlisten has
|
||||
// returned.
|
||||
//
|
||||
// The channel name is case-sensitive.
|
||||
func (l *Listener) Unlisten(channel string) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
|
||||
// Similarly to LISTEN, this is not an error in Postgres, but it seems
|
||||
// useful to distinguish from the normal conditions.
|
||||
_, exists := l.channels[channel]
|
||||
if !exists {
|
||||
return ErrChannelNotOpen
|
||||
}
|
||||
|
||||
if l.cn != nil {
|
||||
// Similarly to Listen (see comment in that function), the caller
|
||||
// should only be bothered with an error if it came from the backend as
|
||||
// a response to our query.
|
||||
gotResponse, err := l.cn.Unlisten(channel)
|
||||
if gotResponse && err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Don't bother waiting for resync if there's no connection.
|
||||
delete(l.channels, channel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlistenAll removes all channels from the Listener's channel list. Returns
|
||||
// immediately with no error if there is no connection. Note that you might
|
||||
// still get notifications for any of the deleted channels even after
|
||||
// UnlistenAll has returned.
|
||||
func (l *Listener) UnlistenAll() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
|
||||
if l.cn != nil {
|
||||
// Similarly to Listen (see comment in that function), the caller
|
||||
// should only be bothered with an error if it came from the backend as
|
||||
// a response to our query.
|
||||
gotResponse, err := l.cn.UnlistenAll()
|
||||
if gotResponse && err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Don't bother waiting for resync if there's no connection.
|
||||
l.channels = make(map[string]struct{})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping the remote server to make sure it's alive. Non-nil return value means
|
||||
// that there is no active connection.
|
||||
func (l *Listener) Ping() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
if l.cn == nil {
|
||||
return errors.New("no connection")
|
||||
}
|
||||
|
||||
return l.cn.Ping()
|
||||
}
|
||||
|
||||
// Clean up after losing the server connection. Returns l.cn.Err(), which
|
||||
// should have the reason the connection was lost.
|
||||
func (l *Listener) disconnectCleanup() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
// sanity check; can't look at Err() until the channel has been closed
|
||||
select {
|
||||
case _, ok := <-l.connNotificationChan:
|
||||
if ok {
|
||||
panic("connNotificationChan not closed")
|
||||
}
|
||||
default:
|
||||
panic("connNotificationChan not closed")
|
||||
}
|
||||
|
||||
err := l.cn.Err()
|
||||
l.cn.Close()
|
||||
l.cn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// Synchronize the list of channels we want to be listening on with the server
|
||||
// after the connection has been established.
|
||||
func (l *Listener) resync(cn *ListenerConn, notificationChan <-chan *Notification) error {
|
||||
doneChan := make(chan error)
|
||||
go func() {
|
||||
for channel := range l.channels {
|
||||
// If we got a response, return that error to our caller as it's
|
||||
// going to be more descriptive than cn.Err().
|
||||
gotResponse, err := cn.Listen(channel)
|
||||
if gotResponse && err != nil {
|
||||
doneChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
// If we couldn't reach the server, wait for notificationChan to
|
||||
// close and then return the error message from the connection, as
|
||||
// per ListenerConn's interface.
|
||||
if err != nil {
|
||||
for _ = range notificationChan {
|
||||
}
|
||||
doneChan <- cn.Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
doneChan <- nil
|
||||
}()
|
||||
|
||||
// Ignore notifications while synchronization is going on to avoid
|
||||
// deadlocks. We have to send a nil notification over Notify anyway as
|
||||
// we can't possibly know which notifications (if any) were lost while
|
||||
// the connection was down, so there's no reason to try and process
|
||||
// these messages at all.
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-notificationChan:
|
||||
if !ok {
|
||||
notificationChan = nil
|
||||
}
|
||||
|
||||
case err := <-doneChan:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// caller should NOT be holding l.lock
|
||||
func (l *Listener) closed() bool {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
return l.isClosed
|
||||
}
|
||||
|
||||
func (l *Listener) connect() error {
|
||||
notificationChan := make(chan *Notification, 32)
|
||||
cn, err := newDialListenerConn(l.dialer, l.name, notificationChan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
err = l.resync(cn, notificationChan)
|
||||
if err != nil {
|
||||
cn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
l.cn = cn
|
||||
l.connNotificationChan = notificationChan
|
||||
l.reconnectCond.Broadcast()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close disconnects the Listener from the database and shuts it down.
|
||||
// Subsequent calls to its methods will return an error. Close returns an
|
||||
// error if the connection has already been closed.
|
||||
func (l *Listener) Close() error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
if l.isClosed {
|
||||
return errListenerClosed
|
||||
}
|
||||
|
||||
if l.cn != nil {
|
||||
l.cn.Close()
|
||||
}
|
||||
l.isClosed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) emitEvent(event ListenerEventType, err error) {
|
||||
if l.eventCallback != nil {
|
||||
l.eventCallback(event, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Main logic here: maintain a connection to the server when possible, wait
|
||||
// for notifications and emit events.
|
||||
func (l *Listener) listenerConnLoop() {
|
||||
var nextReconnect time.Time
|
||||
|
||||
reconnectInterval := l.minReconnectInterval
|
||||
for {
|
||||
for {
|
||||
err := l.connect()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if l.closed() {
|
||||
return
|
||||
}
|
||||
l.emitEvent(ListenerEventConnectionAttemptFailed, err)
|
||||
|
||||
time.Sleep(reconnectInterval)
|
||||
reconnectInterval *= 2
|
||||
if reconnectInterval > l.maxReconnectInterval {
|
||||
reconnectInterval = l.maxReconnectInterval
|
||||
}
|
||||
}
|
||||
|
||||
if nextReconnect.IsZero() {
|
||||
l.emitEvent(ListenerEventConnected, nil)
|
||||
} else {
|
||||
l.emitEvent(ListenerEventReconnected, nil)
|
||||
l.Notify <- nil
|
||||
}
|
||||
|
||||
reconnectInterval = l.minReconnectInterval
|
||||
nextReconnect = time.Now().Add(reconnectInterval)
|
||||
|
||||
for {
|
||||
notification, ok := <-l.connNotificationChan
|
||||
if !ok {
|
||||
// lost connection, loop again
|
||||
break
|
||||
}
|
||||
l.Notify <- notification
|
||||
}
|
||||
|
||||
err := l.disconnectCleanup()
|
||||
if l.closed() {
|
||||
return
|
||||
}
|
||||
l.emitEvent(ListenerEventDisconnected, err)
|
||||
|
||||
time.Sleep(nextReconnect.Sub(time.Now()))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) listenerMain() {
|
||||
l.listenerConnLoop()
|
||||
close(l.Notify)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// Package oid contains OID constants
|
||||
// as defined by the Postgres server.
|
||||
package oid
|
||||
|
||||
// Oid is a Postgres Object ID.
|
||||
type Oid uint32
|
|
@ -0,0 +1,74 @@
|
|||
// +build ignore
|
||||
|
||||
// Generate the table of OID values
|
||||
// Run with 'go run gen.go'.
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func main() {
|
||||
datname := os.Getenv("PGDATABASE")
|
||||
sslmode := os.Getenv("PGSSLMODE")
|
||||
|
||||
if datname == "" {
|
||||
os.Setenv("PGDATABASE", "pqgotest")
|
||||
}
|
||||
|
||||
if sslmode == "" {
|
||||
os.Setenv("PGSSLMODE", "disable")
|
||||
}
|
||||
|
||||
db, err := sql.Open("postgres", "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command("gofmt")
|
||||
cmd.Stderr = os.Stderr
|
||||
w, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
f, err := os.Create("types.go")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cmd.Stdout = f
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintln(w, "// generated by 'go run gen.go'; do not edit")
|
||||
fmt.Fprintln(w, "\npackage oid")
|
||||
fmt.Fprintln(w, "const (")
|
||||
rows, err := db.Query(`
|
||||
SELECT typname, oid
|
||||
FROM pg_type WHERE oid < 10000
|
||||
ORDER BY oid;
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var name string
|
||||
var oid int
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&name, &oid)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintf(w, "T_%s Oid = %d\n", name, oid)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprintln(w, ")")
|
||||
w.Close()
|
||||
cmd.Wait()
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
// generated by 'go run gen.go'; do not edit
|
||||
|
||||
package oid
|
||||
|
||||
const (
|
||||
T_bool Oid = 16
|
||||
T_bytea Oid = 17
|
||||
T_char Oid = 18
|
||||
T_name Oid = 19
|
||||
T_int8 Oid = 20
|
||||
T_int2 Oid = 21
|
||||
T_int2vector Oid = 22
|
||||
T_int4 Oid = 23
|
||||
T_regproc Oid = 24
|
||||
T_text Oid = 25
|
||||
T_oid Oid = 26
|
||||
T_tid Oid = 27
|
||||
T_xid Oid = 28
|
||||
T_cid Oid = 29
|
||||
T_oidvector Oid = 30
|
||||
T_pg_type Oid = 71
|
||||
T_pg_attribute Oid = 75
|
||||
T_pg_proc Oid = 81
|
||||
T_pg_class Oid = 83
|
||||
T_json Oid = 114
|
||||
T_xml Oid = 142
|
||||
T__xml Oid = 143
|
||||
T_pg_node_tree Oid = 194
|
||||
T__json Oid = 199
|
||||
T_smgr Oid = 210
|
||||
T_point Oid = 600
|
||||
T_lseg Oid = 601
|
||||
T_path Oid = 602
|
||||
T_box Oid = 603
|
||||
T_polygon Oid = 604
|
||||
T_line Oid = 628
|
||||
T__line Oid = 629
|
||||
T_cidr Oid = 650
|
||||
T__cidr Oid = 651
|
||||
T_float4 Oid = 700
|
||||
T_float8 Oid = 701
|
||||
T_abstime Oid = 702
|
||||
T_reltime Oid = 703
|
||||
T_tinterval Oid = 704
|
||||
T_unknown Oid = 705
|
||||
T_circle Oid = 718
|
||||
T__circle Oid = 719
|
||||
T_money Oid = 790
|
||||
T__money Oid = 791
|
||||
T_macaddr Oid = 829
|
||||
T_inet Oid = 869
|
||||
T__bool Oid = 1000
|
||||
T__bytea Oid = 1001
|
||||
T__char Oid = 1002
|
||||
T__name Oid = 1003
|
||||
T__int2 Oid = 1005
|
||||
T__int2vector Oid = 1006
|
||||
T__int4 Oid = 1007
|
||||
T__regproc Oid = 1008
|
||||
T__text Oid = 1009
|
||||
T__tid Oid = 1010
|
||||
T__xid Oid = 1011
|
||||
T__cid Oid = 1012
|
||||
T__oidvector Oid = 1013
|
||||
T__bpchar Oid = 1014
|
||||
T__varchar Oid = 1015
|
||||
T__int8 Oid = 1016
|
||||
T__point Oid = 1017
|
||||
T__lseg Oid = 1018
|
||||
T__path Oid = 1019
|
||||
T__box Oid = 1020
|
||||
T__float4 Oid = 1021
|
||||
T__float8 Oid = 1022
|
||||
T__abstime Oid = 1023
|
||||
T__reltime Oid = 1024
|
||||
T__tinterval Oid = 1025
|
||||
T__polygon Oid = 1027
|
||||
T__oid Oid = 1028
|
||||
T_aclitem Oid = 1033
|
||||
T__aclitem Oid = 1034
|
||||
T__macaddr Oid = 1040
|
||||
T__inet Oid = 1041
|
||||
T_bpchar Oid = 1042
|
||||
T_varchar Oid = 1043
|
||||
T_date Oid = 1082
|
||||
T_time Oid = 1083
|
||||
T_timestamp Oid = 1114
|
||||
T__timestamp Oid = 1115
|
||||
T__date Oid = 1182
|
||||
T__time Oid = 1183
|
||||
T_timestamptz Oid = 1184
|
||||
T__timestamptz Oid = 1185
|
||||
T_interval Oid = 1186
|
||||
T__interval Oid = 1187
|
||||
T__numeric Oid = 1231
|
||||
T_pg_database Oid = 1248
|
||||
T__cstring Oid = 1263
|
||||
T_timetz Oid = 1266
|
||||
T__timetz Oid = 1270
|
||||
T_bit Oid = 1560
|
||||
T__bit Oid = 1561
|
||||
T_varbit Oid = 1562
|
||||
T__varbit Oid = 1563
|
||||
T_numeric Oid = 1700
|
||||
T_refcursor Oid = 1790
|
||||
T__refcursor Oid = 2201
|
||||
T_regprocedure Oid = 2202
|
||||
T_regoper Oid = 2203
|
||||
T_regoperator Oid = 2204
|
||||
T_regclass Oid = 2205
|
||||
T_regtype Oid = 2206
|
||||
T__regprocedure Oid = 2207
|
||||
T__regoper Oid = 2208
|
||||
T__regoperator Oid = 2209
|
||||
T__regclass Oid = 2210
|
||||
T__regtype Oid = 2211
|
||||
T_record Oid = 2249
|
||||
T_cstring Oid = 2275
|
||||
T_any Oid = 2276
|
||||
T_anyarray Oid = 2277
|
||||
T_void Oid = 2278
|
||||
T_trigger Oid = 2279
|
||||
T_language_handler Oid = 2280
|
||||
T_internal Oid = 2281
|
||||
T_opaque Oid = 2282
|
||||
T_anyelement Oid = 2283
|
||||
T__record Oid = 2287
|
||||
T_anynonarray Oid = 2776
|
||||
T_pg_authid Oid = 2842
|
||||
T_pg_auth_members Oid = 2843
|
||||
T__txid_snapshot Oid = 2949
|
||||
T_uuid Oid = 2950
|
||||
T__uuid Oid = 2951
|
||||
T_txid_snapshot Oid = 2970
|
||||
T_fdw_handler Oid = 3115
|
||||
T_anyenum Oid = 3500
|
||||
T_tsvector Oid = 3614
|
||||
T_tsquery Oid = 3615
|
||||
T_gtsvector Oid = 3642
|
||||
T__tsvector Oid = 3643
|
||||
T__gtsvector Oid = 3644
|
||||
T__tsquery Oid = 3645
|
||||
T_regconfig Oid = 3734
|
||||
T__regconfig Oid = 3735
|
||||
T_regdictionary Oid = 3769
|
||||
T__regdictionary Oid = 3770
|
||||
T_anyrange Oid = 3831
|
||||
T_event_trigger Oid = 3838
|
||||
T_int4range Oid = 3904
|
||||
T__int4range Oid = 3905
|
||||
T_numrange Oid = 3906
|
||||
T__numrange Oid = 3907
|
||||
T_tsrange Oid = 3908
|
||||
T__tsrange Oid = 3909
|
||||
T_tstzrange Oid = 3910
|
||||
T__tstzrange Oid = 3911
|
||||
T_daterange Oid = 3912
|
||||
T__daterange Oid = 3913
|
||||
T_int8range Oid = 3926
|
||||
T__int8range Oid = 3927
|
||||
)
|
|
@ -0,0 +1,175 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ssl generates a function to upgrade a net.Conn based on the "sslmode" and
|
||||
// related settings. The function is nil when no upgrade should take place.
|
||||
func ssl(o values) func(net.Conn) net.Conn {
|
||||
verifyCaOnly := false
|
||||
tlsConf := tls.Config{}
|
||||
switch mode := o.Get("sslmode"); mode {
|
||||
// "require" is the default.
|
||||
case "", "require":
|
||||
// We must skip TLS's own verification since it requires full
|
||||
// verification since Go 1.3.
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
|
||||
// From http://www.postgresql.org/docs/current/static/libpq-ssl.html:
|
||||
// Note: For backwards compatibility with earlier versions of PostgreSQL, if a
|
||||
// root CA file exists, the behavior of sslmode=require will be the same as
|
||||
// that of verify-ca, meaning the server certificate is validated against the
|
||||
// CA. Relying on this behavior is discouraged, and applications that need
|
||||
// certificate validation should always use verify-ca or verify-full.
|
||||
if _, err := os.Stat(o.Get("sslrootcert")); err == nil {
|
||||
verifyCaOnly = true
|
||||
} else {
|
||||
o.Set("sslrootcert", "")
|
||||
}
|
||||
case "verify-ca":
|
||||
// We must skip TLS's own verification since it requires full
|
||||
// verification since Go 1.3.
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
verifyCaOnly = true
|
||||
case "verify-full":
|
||||
tlsConf.ServerName = o.Get("host")
|
||||
case "disable":
|
||||
return nil
|
||||
default:
|
||||
errorf(`unsupported sslmode %q; only "require" (default), "verify-full", "verify-ca", and "disable" supported`, mode)
|
||||
}
|
||||
|
||||
sslClientCertificates(&tlsConf, o)
|
||||
sslCertificateAuthority(&tlsConf, o)
|
||||
sslRenegotiation(&tlsConf)
|
||||
|
||||
return func(conn net.Conn) net.Conn {
|
||||
client := tls.Client(conn, &tlsConf)
|
||||
if verifyCaOnly {
|
||||
sslVerifyCertificateAuthority(client, &tlsConf)
|
||||
}
|
||||
return client
|
||||
}
|
||||
}
|
||||
|
||||
// sslClientCertificates adds the certificate specified in the "sslcert" and
|
||||
// "sslkey" settings, or if they aren't set, from the .postgresql directory
|
||||
// in the user's home directory. The configured files must exist and have
|
||||
// the correct permissions.
|
||||
func sslClientCertificates(tlsConf *tls.Config, o values) {
|
||||
sslkey := o.Get("sslkey")
|
||||
sslcert := o.Get("sslcert")
|
||||
|
||||
var cinfo, kinfo os.FileInfo
|
||||
var err error
|
||||
|
||||
if sslcert != "" && sslkey != "" {
|
||||
// Check that both files exist. Note that we don't do any more extensive
|
||||
// checks than this (such as checking that the paths aren't directories);
|
||||
// LoadX509KeyPair() will take care of the rest.
|
||||
cinfo, err = os.Stat(sslcert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
kinfo, err = os.Stat(sslkey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
// Automatically find certificates from ~/.postgresql
|
||||
sslcert, sslkey, cinfo, kinfo = sslHomeCertificates()
|
||||
|
||||
if cinfo == nil || kinfo == nil {
|
||||
// No certificates to load
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The files must also have the correct permissions
|
||||
sslCertificatePermissions(cinfo, kinfo)
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(sslcert, sslkey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting.
|
||||
func sslCertificateAuthority(tlsConf *tls.Config, o values) {
|
||||
if sslrootcert := o.Get("sslrootcert"); sslrootcert != "" {
|
||||
tlsConf.RootCAs = x509.NewCertPool()
|
||||
|
||||
cert, err := ioutil.ReadFile(sslrootcert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ok := tlsConf.RootCAs.AppendCertsFromPEM(cert)
|
||||
if !ok {
|
||||
errorf("couldn't parse pem in sslrootcert")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sslHomeCertificates returns the path and stats of certificates in the current
|
||||
// user's home directory.
|
||||
func sslHomeCertificates() (cert, key string, cinfo, kinfo os.FileInfo) {
|
||||
user, err := user.Current()
|
||||
|
||||
if err != nil {
|
||||
// user.Current() might fail when cross-compiling. We have to ignore the
|
||||
// error and continue without client certificates, since we wouldn't know
|
||||
// from where to load them.
|
||||
return
|
||||
}
|
||||
|
||||
cert = filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt")
|
||||
key = filepath.Join(user.HomeDir, ".postgresql", "postgresql.key")
|
||||
|
||||
cinfo, err = os.Stat(cert)
|
||||
if err != nil {
|
||||
cinfo = nil
|
||||
}
|
||||
|
||||
kinfo, err = os.Stat(key)
|
||||
if err != nil {
|
||||
kinfo = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// sslVerifyCertificateAuthority carries out a TLS handshake to the server and
|
||||
// verifies the presented certificate against the CA, i.e. the one specified in
|
||||
// sslrootcert or the system CA if sslrootcert was not specified.
|
||||
func sslVerifyCertificateAuthority(client *tls.Conn, tlsConf *tls.Config) {
|
||||
err := client.Handshake()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
certs := client.ConnectionState().PeerCertificates
|
||||
opts := x509.VerifyOptions{
|
||||
DNSName: client.ConnectionState().ServerName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
Roots: tlsConf.RootCAs,
|
||||
}
|
||||
for i, cert := range certs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
_, err = certs[0].Verify(opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// +build go1.7
|
||||
|
||||
package pq
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// Accept renegotiation requests initiated by the backend.
|
||||
//
|
||||
// Renegotiation was deprecated then removed from PostgreSQL 9.5, but
|
||||
// the default configuration of older versions has it enabled. Redshift
|
||||
// also initiates renegotiations and cannot be reconfigured.
|
||||
func sslRenegotiation(conf *tls.Config) {
|
||||
conf.Renegotiation = tls.RenegotiateFreelyAsClient
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// +build !windows
|
||||
|
||||
package pq
|
||||
|
||||
import "os"
|
||||
|
||||
// sslCertificatePermissions checks the permissions on user-supplied certificate
|
||||
// files. The key file should have very little access.
|
||||
//
|
||||
// libpq does not check key file permissions on Windows.
|
||||
func sslCertificatePermissions(cert, key os.FileInfo) {
|
||||
kmode := key.Mode()
|
||||
if kmode != kmode&0600 {
|
||||
panic(ErrSSLKeyHasWorldPermissions)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// +build !go1.7
|
||||
|
||||
package pq
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
// Renegotiation is not supported by crypto/tls until Go 1.7.
|
||||
func sslRenegotiation(*tls.Config) {}
|
|
@ -0,0 +1,9 @@
|
|||
// +build windows
|
||||
|
||||
package pq
|
||||
|
||||
import "os"
|
||||
|
||||
// sslCertificatePermissions checks the permissions on user-supplied certificate
|
||||
// files. In libpq, this is a no-op on Windows.
|
||||
func sslCertificatePermissions(cert, key os.FileInfo) {}
|
|
@ -0,0 +1,76 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
nurl "net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseURL no longer needs to be used by clients of this library since supplying a URL as a
|
||||
// connection string to sql.Open() is now supported:
|
||||
//
|
||||
// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full")
|
||||
//
|
||||
// It remains exported here for backwards-compatibility.
|
||||
//
|
||||
// ParseURL converts a url to a connection string for driver.Open.
|
||||
// Example:
|
||||
//
|
||||
// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full"
|
||||
//
|
||||
// converts to:
|
||||
//
|
||||
// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full"
|
||||
//
|
||||
// A minimal example:
|
||||
//
|
||||
// "postgres://"
|
||||
//
|
||||
// This will be blank, causing driver.Open to use all of the defaults
|
||||
func ParseURL(url string) (string, error) {
|
||||
u, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
|
||||
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
|
||||
}
|
||||
|
||||
var kvs []string
|
||||
escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`)
|
||||
accrue := func(k, v string) {
|
||||
if v != "" {
|
||||
kvs = append(kvs, k+"="+escaper.Replace(v))
|
||||
}
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
v := u.User.Username()
|
||||
accrue("user", v)
|
||||
|
||||
v, _ = u.User.Password()
|
||||
accrue("password", v)
|
||||
}
|
||||
|
||||
if host, port, err := net.SplitHostPort(u.Host); err != nil {
|
||||
accrue("host", u.Host)
|
||||
} else {
|
||||
accrue("host", host)
|
||||
accrue("port", port)
|
||||
}
|
||||
|
||||
if u.Path != "" {
|
||||
accrue("dbname", u.Path[1:])
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
for k := range q {
|
||||
accrue(k, q.Get(k))
|
||||
}
|
||||
|
||||
sort.Strings(kvs) // Makes testing easier (not a performance concern)
|
||||
return strings.Join(kvs, " "), nil
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Package pq is a pure Go Postgres driver for the database/sql package.
|
||||
|
||||
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris rumprun
|
||||
|
||||
package pq
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
func userCurrent() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
return u.Username, nil
|
||||
}
|
||||
|
||||
name := os.Getenv("USER")
|
||||
if name != "" {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return "", ErrCouldNotDetectUsername
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// Package pq is a pure Go Postgres driver for the database/sql package.
|
||||
package pq
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Perform Windows user name lookup identically to libpq.
|
||||
//
|
||||
// The PostgreSQL code makes use of the legacy Win32 function
|
||||
// GetUserName, and that function has not been imported into stock Go.
|
||||
// GetUserNameEx is available though, the difference being that a
|
||||
// wider range of names are available. To get the output to be the
|
||||
// same as GetUserName, only the base (or last) component of the
|
||||
// result is returned.
|
||||
func userCurrent() (string, error) {
|
||||
pw_name := make([]uint16, 128)
|
||||
pwname_size := uint32(len(pw_name)) - 1
|
||||
err := syscall.GetUserNameEx(syscall.NameSamCompatible, &pw_name[0], &pwname_size)
|
||||
if err != nil {
|
||||
return "", ErrCouldNotDetectUsername
|
||||
}
|
||||
s := syscall.UTF16ToString(pw_name)
|
||||
u := filepath.Base(s)
|
||||
return u, nil
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package pq
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// decodeUUIDBinary interprets the binary format of a uuid, returning it in text format.
|
||||
func decodeUUIDBinary(src []byte) ([]byte, error) {
|
||||
if len(src) != 16 {
|
||||
return nil, fmt.Errorf("pq: unable to decode uuid; bad length: %d", len(src))
|
||||
}
|
||||
|
||||
dst := make([]byte, 36)
|
||||
dst[8], dst[13], dst[18], dst[23] = '-', '-', '-', '-'
|
||||
hex.Encode(dst[0:], src[0:4])
|
||||
hex.Encode(dst[9:], src[4:6])
|
||||
hex.Encode(dst[14:], src[6:8])
|
||||
hex.Encode(dst[19:], src[8:10])
|
||||
hex.Encode(dst[24:], src[10:16])
|
||||
|
||||
return dst, nil
|
||||
}
|
|
@ -0,0 +1,352 @@
|
|||
Meddler [![Build Status](https://travis-ci.org/russross/meddler.svg?branch=master)](https://travis-ci.org/russross/meddler)
|
||||
=======
|
||||
|
||||
Meddler is a small toolkit to take some of the tedium out of moving data
|
||||
back and forth between sql queries and structs.
|
||||
|
||||
It is not a complete ORM. It is intended to be lightweight way to add some
|
||||
of the convenience of an ORM while leaving more control in the hands of the
|
||||
programmer.
|
||||
|
||||
Package docs are available at:
|
||||
|
||||
* http://godoc.org/github.com/russross/meddler
|
||||
|
||||
The package is housed on github, and the README there has more info:
|
||||
|
||||
* http://github.com/russross/meddler
|
||||
|
||||
This is currently configured for SQLite, MySQL, and PostgreSQL, but it
|
||||
can be configured for use with other databases. If you use it
|
||||
successfully with a different database, please contact me and I will
|
||||
add it to the list of pre-configured databases.
|
||||
|
||||
### DANGER
|
||||
|
||||
Meddler is still a work in progress, and additional
|
||||
backward-incompatible changes to the API are likely. The most recent
|
||||
change added support for multiple database types and made it easier
|
||||
to switch between them. This is most likely to affect the way you
|
||||
initialize the library to work with your database (see the install
|
||||
section below).
|
||||
|
||||
Another recent update is the change to int64 for primary keys. This
|
||||
matches the convention used in database/sql, and is more portable,
|
||||
but it may require some minor changes to existing code.
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
The usual `go get` command will put it in your `$GOPATH`:
|
||||
|
||||
go get github.com/russross/meddler
|
||||
|
||||
If you are only using one type of database, you should set Default
|
||||
to match your database type, e.g.:
|
||||
|
||||
meddler.Default = meddler.PostgreSQL
|
||||
|
||||
The default database is MySQL, so you should change it for anything
|
||||
else. To use multiple databases within a single project, or to use a
|
||||
database other than MySQL, PostgreSQL, or SQLite, see below.
|
||||
|
||||
Note: If you are using MySQL with the `github.com/go-sql-driver/mysql`
|
||||
driver, you must set "parseTime=true" in the sql.Open call or the
|
||||
time conversion meddlers will not work.
|
||||
|
||||
|
||||
Why?
|
||||
----
|
||||
|
||||
These are the features that set meddler apart from similar
|
||||
libraries:
|
||||
|
||||
* It uses standard database/sql types, and does not require
|
||||
special fields in your structs. This lets you use meddler
|
||||
selectively, without having to alter other database code already
|
||||
in your project. After creating meddler, I incorporated it into
|
||||
an existing project, and I was able to convert the code one
|
||||
struct and one query at a time.
|
||||
* It leaves query writing to you. It has convenience functions for
|
||||
simple INSERT/UPDATE/SELECT queries by integer primary key, but
|
||||
beyond that it stays out of query writing.
|
||||
* It supports on-the-fly data transformations. If you have a map
|
||||
or a slice in your struct, you can instruct meddler to
|
||||
encode/decode using JSON or Gob automatically. If you have time
|
||||
fields, you can have meddler automatically write them into the
|
||||
database as UTC, and convert them to the local time zone on
|
||||
reads. These processors are called “meddlers”, because they
|
||||
meddle with the data instead of passing it through directly.
|
||||
* NULL fields in the database can be read as zero values in the
|
||||
struct, and zero values in the struct can be written as NULL
|
||||
values. This is not always the right thing to do, but it is
|
||||
often good enough and is much simpler than most alternatives.
|
||||
* It exposes low-level hooks for more complex situations. If you
|
||||
are writing a query that does not map well to the main helper
|
||||
functions, you can still get some help by using the lower-level
|
||||
functions to build your own helpers.
|
||||
|
||||
|
||||
High-level functions
|
||||
--------------------
|
||||
|
||||
Meddler does not create or alter tables. It just provides a little
|
||||
glue to make it easier to read and write structs as SQL rows. Start
|
||||
by annotating a struct:
|
||||
|
||||
``` go
|
||||
type Person struct {
|
||||
ID int `meddler:"id,pk"`
|
||||
Name string `meddler:"name"`
|
||||
Age int
|
||||
salary int
|
||||
Created time.Time `meddler:"created,localtime"`
|
||||
Closed time.Time `meddler:",localtimez"`
|
||||
}
|
||||
```
|
||||
|
||||
Notes about this example:
|
||||
|
||||
* If the optional tag is provided, the first field is the database
|
||||
column name. Note that "Closed" does not provide a column name,
|
||||
so it will default to "Closed". Likewise, if there is no tag,
|
||||
the field name will be used.
|
||||
* ID is marked as the primary key. Currently only integer primary
|
||||
keys are supported. This is only relevant to Load, Save, Insert,
|
||||
and Update, a few of the higher-level functions that need to
|
||||
understand primary keys. Meddler assumes that pk fields have an
|
||||
autoincrement mechanism set in the database.
|
||||
* Age has a column name of "Age". A tag is only necessary when the
|
||||
column name is not the same as the field name, or when you need
|
||||
to select other options.
|
||||
* salary is not an exported field, so meddler does not see it. It
|
||||
will be ignored.
|
||||
* Created is marked with "localtime". This means that it will be
|
||||
converted to UTC when being saved, and back to the local time
|
||||
zone when being loaded.
|
||||
* Closed has a column name of "Closed", since the tag did not
|
||||
specify anything different. Closed is marked as "localtimez".
|
||||
This has the same properties as "localtime", except that the
|
||||
zero time will be saved in the database as a null column (and
|
||||
null values will be loaded as the zero time value).
|
||||
|
||||
Meddler provides a few high-level functions (note: DB is an
|
||||
interface that works with a *sql.DB or a *sql.Tx):
|
||||
|
||||
* Load(db DB, table string, dst interface{}, pk int64) error
|
||||
|
||||
This loads a single record by its primary key. For example:
|
||||
|
||||
elt := new(Person)
|
||||
err := meddler.Load(db, "person", elt, 15)
|
||||
|
||||
db can be a *sql.DB or a *sql.Tx. The table is the name of the
|
||||
table, pk is the primary key value, and dst is a pointer to the
|
||||
struct where it should be stored.
|
||||
|
||||
Note: this call requires that the struct have an integer primary
|
||||
key field marked.
|
||||
|
||||
* Insert(db DB, table string, src interface{}) error
|
||||
|
||||
This inserts a new row into the database. If the struct value
|
||||
has a primary key field, it must be zero (and will be omitted
|
||||
from the insert statement, prompting a default autoincrement
|
||||
value).
|
||||
|
||||
elt := &Person{
|
||||
Name: "Alice",
|
||||
Age: 22,
|
||||
// ...
|
||||
}
|
||||
err := meddler.Insert(db, "person", elt)
|
||||
// elt.ID is updated to the value assigned by the database
|
||||
|
||||
* Update(db DB, table string, src interface{}) error
|
||||
|
||||
This updates an existing row. It must have a primary key, which
|
||||
must be non-zero.
|
||||
|
||||
Note: this call requires that the struct have an integer primary
|
||||
key field marked.
|
||||
|
||||
* Save(db DB, table string, src interface{}) error
|
||||
|
||||
Pick Insert or Update automatically. If there is a non-zero
|
||||
primary key present, it uses Update, otherwise it uses Insert.
|
||||
|
||||
Note: this call requires that the struct have an integer primary
|
||||
key field marked.
|
||||
|
||||
* QueryRow(db DB, dst interface{}, query string, args ...interface) error
|
||||
|
||||
Perform the given query, and scan the single-row result into
|
||||
dst, which must be a pointer to a struct.
|
||||
|
||||
For example:
|
||||
|
||||
elt := new(Person)
|
||||
err := meddler.QueryRow(db, elt, "select * from person where name = ?", "bob")
|
||||
|
||||
* QueryAll(db DB, dst interface{}, query string, args ...interface) error
|
||||
|
||||
Perform the given query, and scan the results into dst, which
|
||||
must be a pointer to a slice of struct pointers.
|
||||
|
||||
For example:
|
||||
|
||||
var people []*Person
|
||||
err := meddler.QueryAll(db, &people, "select * from person")
|
||||
|
||||
* Scan(rows *sql.Rows, dst interface{}) error
|
||||
|
||||
Scans a single row of data into a struct, complete with
|
||||
meddling. Can be called repeatedly to walk through all of the
|
||||
rows in a result set. Returns sql.ErrNoRows when there is no
|
||||
more data.
|
||||
|
||||
* ScanRow(rows *sql.Rows, dst interface{}) error
|
||||
|
||||
Similar to Scan, but guarantees that the rows object
|
||||
is closed when it returns. Also returns sql.ErrNoRows if there
|
||||
was no row.
|
||||
|
||||
* ScanAll(rows *sql.Rows, dst interface{}) error
|
||||
|
||||
Expects a pointer to a slice of structs/pointers to structs, and
|
||||
appends as many elements as it finds in the row set. Closes the
|
||||
row set when it is finished. Does not return sql.ErrNoRows on an
|
||||
empty set; instead it just does not add anything to the slice.
|
||||
|
||||
Note: all of these functions can also be used as methods on Database
|
||||
objects. When used as package functions, they use the Default
|
||||
Database object, which is MySQL unless you change it.
|
||||
|
||||
|
||||
Meddlers
|
||||
--------
|
||||
|
||||
A meddler is a handler that gets to meddle with a field before it is
|
||||
saved, or when it is loaded. "localtime" and "localtimez" are
|
||||
examples of built-in meddlers. The full list of built-in meddlers
|
||||
includes:
|
||||
|
||||
* identity: the default meddler, which does not do anything
|
||||
|
||||
* localtime: for time.Time and *time.Time fields. Converts the
|
||||
value to UTC on save, and back to the local time zone on loads.
|
||||
To set your local time zone, make sure the TZ environment
|
||||
variable is set when your program is launched, or use something
|
||||
like:
|
||||
|
||||
os.Setenv("TZ", "America/Denver")
|
||||
|
||||
in your initial setup, before you start using time functions.
|
||||
|
||||
* localtimez: same, but only for time.Time, and treats the zero
|
||||
time as a null field (converts both ways)
|
||||
|
||||
* utctime: similar to localtime, but keeps the value in UTC on
|
||||
loads. This ensures that the time is always coverted to UTC on
|
||||
save, which is the sane way to save time values in a database.
|
||||
|
||||
* utctimez: same, but with zero time means null.
|
||||
|
||||
* zeroisnull: for other types where a zero value should be
|
||||
inserted as null, and null values should be read as zero values.
|
||||
Works for integer, unsigned integer, float, complex number, and
|
||||
string types. Note: not for pointer types.
|
||||
|
||||
* json: marshals the field value into JSON when saving, and
|
||||
unmarshals on load.
|
||||
|
||||
* jsongzip: same, but compresses using gzip on save, and
|
||||
uncompresses on load
|
||||
|
||||
* gob: encodes the field value using Gob when saving, and
|
||||
decodes on load.
|
||||
|
||||
* gobgzip: same, but compresses using gzip on save, and
|
||||
uncompresses on load
|
||||
|
||||
You can implement custom meddlers as well by implementing the
|
||||
Meddler interface. See the existing implementations in medder.go for
|
||||
examples.
|
||||
|
||||
|
||||
Working with different database types
|
||||
-------------------------------------
|
||||
|
||||
Meddler can work with multiple database types simultaneously.
|
||||
Database-specific parameters are stored in a Database struct, and
|
||||
structs are pre-defined for MySQL, PostgreSQL, and SQLite.
|
||||
|
||||
Instead of relying on the package-level functions, use the method
|
||||
form on the appropriate database type, e.g.:
|
||||
|
||||
err = meddler.PostgreSQL.Load(...)
|
||||
|
||||
instead of
|
||||
|
||||
err = meddler.Load(...)
|
||||
|
||||
Or to save typing, define your own abbreviated name for each
|
||||
database:
|
||||
|
||||
ms := meddler.MySQL
|
||||
pg := meddler.PostgreSQL
|
||||
err = ms.Load(...)
|
||||
err = pg.QueryAll(...)
|
||||
|
||||
If you need a different database, create your own Database instance
|
||||
with the appropriate parameters set. If everything works okay,
|
||||
please contact me with the parameters you used so I can add the new
|
||||
database to the pre-defined list.
|
||||
|
||||
|
||||
Lower-level functions
|
||||
---------------------
|
||||
|
||||
If you are using more complex queries and just want to reduce the
|
||||
tedium of reading and writing values, there are some lower-level
|
||||
helper functions as well. See the package docs for details, and
|
||||
see the implementations of the higher-level functions to see how
|
||||
they are used.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Meddler is distributed under the BSD 2-Clause License. If this
|
||||
license prevents you from using Meddler in your project, please
|
||||
contact me and I will consider adding an additional license that is
|
||||
better suited to your needs.
|
||||
|
||||
> Copyright © 2013 Russ Ross.
|
||||
> All rights reserved.
|
||||
>
|
||||
> Redistribution and use in source and binary forms, with or without
|
||||
> modification, are permitted provided that the following conditions
|
||||
> are met:
|
||||
>
|
||||
> 1. Redistributions of source code must retain the above copyright
|
||||
> notice, this list of conditions and the following disclaimer.
|
||||
>
|
||||
> 2. Redistributions in binary form must reproduce the above
|
||||
> copyright notice, this list of conditions and the following
|
||||
> disclaimer in the documentation and/or other materials provided with
|
||||
> the distribution.
|
||||
>
|
||||
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
> POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Meddler is a small toolkit to take some of the tedium out of moving data
|
||||
back and forth between sql queries and structs.
|
||||
|
||||
It is not a complete ORM. It is intended to be lightweight way to add some
|
||||
of the convenience of an ORM while leaving more control in the hands of the
|
||||
programmer.
|
||||
|
||||
Package docs are available at:
|
||||
|
||||
http://godoc.org/github.com/russross/meddler
|
||||
|
||||
The package is housed on github, and the README there has more info:
|
||||
|
||||
http://github.com/russross/meddler
|
||||
|
||||
*/
|
||||
package meddler
|
|
@ -0,0 +1,247 @@
|
|||
package meddler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type dbErr struct {
|
||||
msg string
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *dbErr) Error() string {
|
||||
return fmt.Sprintf("%s: %v", err.msg, err.err)
|
||||
}
|
||||
|
||||
// DriverErr returns the original error as returned by the database driver
|
||||
// if the error comes from the driver, with the second value set to true.
|
||||
// Otherwise, it returns err itself with false as second value.
|
||||
func DriverErr(err error) (error, bool) {
|
||||
if dbe, ok := err.(*dbErr); ok {
|
||||
return dbe.err, true
|
||||
}
|
||||
return err, false
|
||||
}
|
||||
|
||||
// DB is a generic database interface, matching both *sql.Db and *sql.Tx
|
||||
type DB interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
// Load loads a record using a query for the primary key field.
|
||||
// Returns sql.ErrNoRows if not found.
|
||||
func (d *Database) Load(db DB, table string, dst interface{}, pk int64) error {
|
||||
columns, err := d.ColumnsQuoted(dst, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make sure we have a primary key field
|
||||
pkName, _, err := d.PrimaryKey(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pkName == "" {
|
||||
return fmt.Errorf("meddler.Load: no primary key field found")
|
||||
}
|
||||
|
||||
// run the query
|
||||
q := fmt.Sprintf("SELECT %s FROM %s WHERE %s = %s", columns, d.quoted(table), d.quoted(pkName), d.Placeholder)
|
||||
|
||||
rows, err := db.Query(q, pk)
|
||||
if err != nil {
|
||||
return &dbErr{msg: "meddler.Load: DB error in Query", err: err}
|
||||
}
|
||||
|
||||
// scan the row
|
||||
return d.ScanRow(rows, dst)
|
||||
}
|
||||
|
||||
// Load using the Default Database type
|
||||
func Load(db DB, table string, dst interface{}, pk int64) error {
|
||||
return Default.Load(db, table, dst, pk)
|
||||
}
|
||||
|
||||
// Insert performs an INSERT query for the given record.
|
||||
// If the record has a primary key flagged, it must be zero, and it
|
||||
// will be set to the newly-allocated primary key value from the database
|
||||
// as returned by LastInsertId.
|
||||
func (d *Database) Insert(db DB, table string, src interface{}) error {
|
||||
pkName, pkValue, err := d.PrimaryKey(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pkName != "" && pkValue != 0 {
|
||||
return fmt.Errorf("meddler.Insert: primary key must be zero")
|
||||
}
|
||||
|
||||
// gather the query parts
|
||||
namesPart, err := d.ColumnsQuoted(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valuesPart, err := d.PlaceholdersString(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values, err := d.Values(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the query
|
||||
q := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", d.quoted(table), namesPart, valuesPart)
|
||||
if d.UseReturningToGetID && pkName != "" {
|
||||
q += " RETURNING " + d.quoted(pkName)
|
||||
var newPk int64
|
||||
err := db.QueryRow(q, values...).Scan(&newPk)
|
||||
if err != nil {
|
||||
return &dbErr{msg: "meddler.Insert: DB error in QueryRow", err: err}
|
||||
}
|
||||
if err = d.SetPrimaryKey(src, newPk); err != nil {
|
||||
return fmt.Errorf("meddler.Insert: Error saving updated pk: %v", err)
|
||||
}
|
||||
} else if pkName != "" {
|
||||
result, err := db.Exec(q, values...)
|
||||
if err != nil {
|
||||
return &dbErr{msg: "meddler.Insert: DB error in Exec", err: err}
|
||||
}
|
||||
|
||||
// save the new primary key
|
||||
newPk, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return &dbErr{msg: "meddler.Insert: DB error getting new primary key value", err: err}
|
||||
}
|
||||
if err = d.SetPrimaryKey(src, newPk); err != nil {
|
||||
return fmt.Errorf("meddler.Insert: Error saving updated pk: %v", err)
|
||||
}
|
||||
} else {
|
||||
// no primary key, so no need to lookup new value
|
||||
_, err := db.Exec(q, values...)
|
||||
if err != nil {
|
||||
return &dbErr{msg: "meddler.Insert: DB error in Exec", err: err}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert using the Default Database type
|
||||
func Insert(db DB, table string, src interface{}) error {
|
||||
return Default.Insert(db, table, src)
|
||||
}
|
||||
|
||||
// Update performs and UPDATE query for the given record.
|
||||
// The record must have an integer primary key field that is non-zero,
|
||||
// and it will be used to select the database row that gets updated.
|
||||
func (d *Database) Update(db DB, table string, src interface{}) error {
|
||||
// gather the query parts
|
||||
names, err := d.Columns(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
placeholders, err := d.Placeholders(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values, err := d.Values(src, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// form the column=placeholder pairs
|
||||
var pairs []string
|
||||
for i := 0; i < len(names) && i < len(placeholders); i++ {
|
||||
pair := fmt.Sprintf("%s=%s", d.quoted(names[i]), placeholders[i])
|
||||
pairs = append(pairs, pair)
|
||||
}
|
||||
|
||||
pkName, pkValue, err := d.PrimaryKey(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pkName == "" {
|
||||
return fmt.Errorf("meddler.Update: no primary key field")
|
||||
}
|
||||
if pkValue < 1 {
|
||||
return fmt.Errorf("meddler.Update: primary key must be an integer > 0")
|
||||
}
|
||||
ph := d.placeholder(len(placeholders) + 1)
|
||||
|
||||
// run the query
|
||||
q := fmt.Sprintf("UPDATE %s SET %s WHERE %s=%s", d.quoted(table),
|
||||
strings.Join(pairs, ","),
|
||||
d.quoted(pkName), ph)
|
||||
values = append(values, pkValue)
|
||||
|
||||
if _, err := db.Exec(q, values...); err != nil {
|
||||
return &dbErr{msg: "meddler.Update: DB error in Exec", err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update using the Default Database type
|
||||
func Update(db DB, table string, src interface{}) error {
|
||||
return Default.Update(db, table, src)
|
||||
}
|
||||
|
||||
// Save performs an INSERT or an UPDATE, depending on whether or not
|
||||
// a primary keys exists and is non-zero.
|
||||
func (d *Database) Save(db DB, table string, src interface{}) error {
|
||||
pkName, pkValue, err := d.PrimaryKey(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pkName != "" && pkValue != 0 {
|
||||
return d.Update(db, table, src)
|
||||
} else {
|
||||
return d.Insert(db, table, src)
|
||||
}
|
||||
}
|
||||
|
||||
// Save using the Default Database type
|
||||
func Save(db DB, table string, src interface{}) error {
|
||||
return Default.Save(db, table, src)
|
||||
}
|
||||
|
||||
// QueryOne performs the given query with the given arguments, scanning a
|
||||
// single row of results into dst. Returns sql.ErrNoRows if there was no
|
||||
// result row.
|
||||
func (d *Database) QueryRow(db DB, dst interface{}, query string, args ...interface{}) error {
|
||||
// perform the query
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// gather the result
|
||||
return d.ScanRow(rows, dst)
|
||||
}
|
||||
|
||||
// QueryRow using the Default Database type
|
||||
func QueryRow(db DB, dst interface{}, query string, args ...interface{}) error {
|
||||
return Default.QueryRow(db, dst, query, args...)
|
||||
}
|
||||
|
||||
// QueryAll performs the given query with the given arguments, scanning
|
||||
// all results rows into dst.
|
||||
func (d *Database) QueryAll(db DB, dst interface{}, query string, args ...interface{}) error {
|
||||
// perform the query
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// gather the results
|
||||
return d.ScanAll(rows, dst)
|
||||
}
|
||||
|
||||
// QueryAll using the Default Database type
|
||||
func QueryAll(db DB, dst interface{}, query string, args ...interface{}) error {
|
||||
return Default.QueryAll(db, dst, query, args...)
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
package meddler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Meddler is the interface for a field meddler. Implementations can be
|
||||
// registered to convert struct fields being loaded and saved in the database.
|
||||
type Meddler interface {
|
||||
// PreRead is called before a Scan operation. It is given a pointer to
|
||||
// the raw struct field, and returns the value that will be given to
|
||||
// the database driver.
|
||||
PreRead(fieldAddr interface{}) (scanTarget interface{}, err error)
|
||||
|
||||
// PostRead is called after a Scan operation. It is given the value returned
|
||||
// by PreRead and a pointer to the raw struct field. It is expected to fill
|
||||
// in the struct field if the two are different.
|
||||
PostRead(fieldAddr interface{}, scanTarget interface{}) error
|
||||
|
||||
// PreWrite is called before an Insert or Update operation. It is given
|
||||
// a pointer to the raw struct field, and returns the value that will be
|
||||
// given to the database driver.
|
||||
PreWrite(field interface{}) (saveValue interface{}, err error)
|
||||
}
|
||||
|
||||
// Register sets up a meddler type. Meddlers get a chance to meddle with the
|
||||
// data being loaded or saved when a field is annotated with the name of the meddler.
|
||||
// The registry is global.
|
||||
func Register(name string, m Meddler) {
|
||||
if name == "pk" {
|
||||
panic("meddler.Register: pk cannot be used as a meddler name")
|
||||
}
|
||||
registry[name] = m
|
||||
}
|
||||
|
||||
var registry = make(map[string]Meddler)
|
||||
|
||||
func init() {
|
||||
Register("identity", IdentityMeddler(false))
|
||||
Register("localtime", TimeMeddler{ZeroIsNull: false, Local: true})
|
||||
Register("localtimez", TimeMeddler{ZeroIsNull: true, Local: true})
|
||||
Register("utctime", TimeMeddler{ZeroIsNull: false, Local: false})
|
||||
Register("utctimez", TimeMeddler{ZeroIsNull: true, Local: false})
|
||||
Register("zeroisnull", ZeroIsNullMeddler(false))
|
||||
Register("json", JSONMeddler(false))
|
||||
Register("jsongzip", JSONMeddler(true))
|
||||
Register("gob", GobMeddler(false))
|
||||
Register("gobgzip", GobMeddler(true))
|
||||
}
|
||||
|
||||
// IdentityMeddler is the default meddler, and it passes the original value through with
|
||||
// no changes.
|
||||
type IdentityMeddler bool
|
||||
|
||||
func (elt IdentityMeddler) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
|
||||
return fieldAddr, nil
|
||||
}
|
||||
|
||||
func (elt IdentityMeddler) PostRead(fieldAddr, scanTarget interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (elt IdentityMeddler) PreWrite(field interface{}) (saveValue interface{}, err error) {
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// TimeMeddler provides useful operations on time.Time fields. It can convert the zero time
|
||||
// to and from a null column, and it can convert the time zone to UTC on save and to Local on load.
|
||||
type TimeMeddler struct {
|
||||
ZeroIsNull bool
|
||||
Local bool
|
||||
}
|
||||
|
||||
func (elt TimeMeddler) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
|
||||
switch tgt := fieldAddr.(type) {
|
||||
case *time.Time:
|
||||
if elt.ZeroIsNull {
|
||||
return &tgt, nil
|
||||
}
|
||||
return fieldAddr, nil
|
||||
case **time.Time:
|
||||
if elt.ZeroIsNull {
|
||||
return nil, fmt.Errorf("meddler.TimeMeddler cannot be used on a *time.Time field, only time.Time")
|
||||
}
|
||||
return fieldAddr, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("meddler.TimeMeddler.PreRead: unknown struct field type: %T", fieldAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (elt TimeMeddler) PostRead(fieldAddr, scanTarget interface{}) error {
|
||||
switch tgt := fieldAddr.(type) {
|
||||
case *time.Time:
|
||||
if elt.ZeroIsNull {
|
||||
src := scanTarget.(**time.Time)
|
||||
if *src == nil {
|
||||
*tgt = time.Time{}
|
||||
} else if elt.Local {
|
||||
*tgt = (*src).Local()
|
||||
} else {
|
||||
*tgt = (*src).UTC()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
src := scanTarget.(*time.Time)
|
||||
if elt.Local {
|
||||
*tgt = src.Local()
|
||||
} else {
|
||||
*tgt = src.UTC()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case **time.Time:
|
||||
if elt.ZeroIsNull {
|
||||
return fmt.Errorf("meddler TimeMeddler cannot be used on a *time.Time field, only time.Time")
|
||||
}
|
||||
src := scanTarget.(**time.Time)
|
||||
if *src == nil {
|
||||
*tgt = nil
|
||||
} else if elt.Local {
|
||||
**src = (*src).Local()
|
||||
*tgt = *src
|
||||
} else {
|
||||
**src = (*src).UTC()
|
||||
*tgt = *src
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("meddler.TimeMeddler.PostRead: unknown struct field type: %T", fieldAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (elt TimeMeddler) PreWrite(field interface{}) (saveValue interface{}, err error) {
|
||||
switch tgt := field.(type) {
|
||||
case time.Time:
|
||||
if elt.ZeroIsNull && tgt.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
return tgt.UTC(), nil
|
||||
|
||||
case *time.Time:
|
||||
if tgt == nil || elt.ZeroIsNull && tgt.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
return tgt.UTC(), nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("meddler.TimeMeddler.PreWrite: unknown struct field type: %T", field)
|
||||
}
|
||||
}
|
||||
|
||||
// ZeroIsNullMeddler converts zero value fields (integers both signed and unsigned, floats, complex numbers,
|
||||
// and strings) to and from null database columns.
|
||||
type ZeroIsNullMeddler bool
|
||||
|
||||
func (elt ZeroIsNullMeddler) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
|
||||
// create a pointer to this element
|
||||
// the database driver will set it to nil if the column value is null
|
||||
return reflect.New(reflect.TypeOf(fieldAddr)).Interface(), nil
|
||||
}
|
||||
|
||||
func (elt ZeroIsNullMeddler) PostRead(fieldAddr, scanTarget interface{}) error {
|
||||
sv := reflect.ValueOf(scanTarget)
|
||||
fv := reflect.ValueOf(fieldAddr)
|
||||
if sv.Elem().IsNil() {
|
||||
// null column, so set target to be zero value
|
||||
fv.Elem().Set(reflect.Zero(fv.Elem().Type()))
|
||||
} else {
|
||||
// copy the value that scan found
|
||||
fv.Elem().Set(sv.Elem().Elem())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (elt ZeroIsNullMeddler) PreWrite(field interface{}) (saveValue interface{}, err error) {
|
||||
val := reflect.ValueOf(field)
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if val.Int() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if val.Uint() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if val.Float() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
if val.Complex() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.String:
|
||||
if val.String() == "" {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Bool:
|
||||
if !val.Bool() {
|
||||
return nil, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("ZeroIsNullMeddler.PreWrite: unknown struct field type: %T", field)
|
||||
}
|
||||
|
||||
return field, nil
|
||||
}
|
||||
|
||||
type JSONMeddler bool
|
||||
|
||||
func (zip JSONMeddler) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
|
||||
// give a pointer to a byte buffer to grab the raw data
|
||||
return new([]byte), nil
|
||||
}
|
||||
|
||||
func (zip JSONMeddler) PostRead(fieldAddr, scanTarget interface{}) error {
|
||||
ptr := scanTarget.(*[]byte)
|
||||
if ptr == nil {
|
||||
return fmt.Errorf("JSONMeddler.PostRead: nil pointer")
|
||||
}
|
||||
raw := *ptr
|
||||
|
||||
if zip {
|
||||
// un-gzip and decode json
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating gzip Reader: %v", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
jsonDecoder := json.NewDecoder(gzipReader)
|
||||
if err := jsonDecoder.Decode(fieldAddr); err != nil {
|
||||
return fmt.Errorf("JSON decoder/gzip error: %v", err)
|
||||
}
|
||||
if err := gzipReader.Close(); err != nil {
|
||||
return fmt.Errorf("Closing gzip reader: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decode json
|
||||
jsonDecoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
if err := jsonDecoder.Decode(fieldAddr); err != nil {
|
||||
return fmt.Errorf("JSON decode error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (zip JSONMeddler) PreWrite(field interface{}) (saveValue interface{}, err error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
if zip {
|
||||
// json encode and gzip
|
||||
gzipWriter := gzip.NewWriter(buffer)
|
||||
defer gzipWriter.Close()
|
||||
jsonEncoder := json.NewEncoder(gzipWriter)
|
||||
if err := jsonEncoder.Encode(field); err != nil {
|
||||
return nil, fmt.Errorf("JSON encoding/gzip error: %v", err)
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return nil, fmt.Errorf("Closing gzip writer: %v", err)
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// json encode
|
||||
jsonEncoder := json.NewEncoder(buffer)
|
||||
if err := jsonEncoder.Encode(field); err != nil {
|
||||
return nil, fmt.Errorf("JSON encoding error: %v", err)
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
type GobMeddler bool
|
||||
|
||||
func (zip GobMeddler) PreRead(fieldAddr interface{}) (scanTarget interface{}, err error) {
|
||||
// give a pointer to a byte buffer to grab the raw data
|
||||
return new([]byte), nil
|
||||
}
|
||||
|
||||
func (zip GobMeddler) PostRead(fieldAddr, scanTarget interface{}) error {
|
||||
ptr := scanTarget.(*[]byte)
|
||||
if ptr == nil {
|
||||
return fmt.Errorf("GobMeddler.PostRead: nil pointer")
|
||||
}
|
||||
raw := *ptr
|
||||
|
||||
if zip {
|
||||
// un-gzip and decode gob
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating gzip Reader: %v", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
gobDecoder := gob.NewDecoder(gzipReader)
|
||||
if err := gobDecoder.Decode(fieldAddr); err != nil {
|
||||
return fmt.Errorf("Gob decoder/gzip error: %v", err)
|
||||
}
|
||||
if err := gzipReader.Close(); err != nil {
|
||||
return fmt.Errorf("Closing gzip reader: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decode gob
|
||||
gobDecoder := gob.NewDecoder(bytes.NewReader(raw))
|
||||
if err := gobDecoder.Decode(fieldAddr); err != nil {
|
||||
return fmt.Errorf("Gob decode error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (zip GobMeddler) PreWrite(field interface{}) (saveValue interface{}, err error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
|
||||
if zip {
|
||||
// gob encode and gzip
|
||||
gzipWriter := gzip.NewWriter(buffer)
|
||||
defer gzipWriter.Close()
|
||||
gobEncoder := gob.NewEncoder(gzipWriter)
|
||||
if err := gobEncoder.Encode(field); err != nil {
|
||||
return nil, fmt.Errorf("Gob encoding/gzip error: %v", err)
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return nil, fmt.Errorf("Closing gzip writer: %v", err)
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// gob encode
|
||||
gobEncoder := gob.NewEncoder(buffer)
|
||||
if err := gobEncoder.Encode(field); err != nil {
|
||||
return nil, fmt.Errorf("Gob encoding error: %v", err)
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,576 @@
|
|||
package meddler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// the name of our struct tag
|
||||
const tagName = "meddler"
|
||||
|
||||
// Database contains database-specific options.
|
||||
// MySQL, PostgreSQL, and SQLite are provided for convenience.
|
||||
// Setting Default to any of these lets you use the package-level convenience functions.
|
||||
type Database struct {
|
||||
Quote string // the quote character for table and column names
|
||||
Placeholder string // the placeholder style to use in generated queries
|
||||
UseReturningToGetID bool // use PostgreSQL-style RETURNING "ID" instead of calling sql.Result.LastInsertID
|
||||
}
|
||||
|
||||
var MySQL = &Database{
|
||||
Quote: "`",
|
||||
Placeholder: "?",
|
||||
UseReturningToGetID: false,
|
||||
}
|
||||
|
||||
var PostgreSQL = &Database{
|
||||
Quote: `"`,
|
||||
Placeholder: "$1",
|
||||
UseReturningToGetID: true,
|
||||
}
|
||||
|
||||
var SQLite = &Database{
|
||||
Quote: `"`,
|
||||
Placeholder: "?",
|
||||
UseReturningToGetID: false,
|
||||
}
|
||||
|
||||
var Default = MySQL
|
||||
|
||||
func (d *Database) quoted(s string) string {
|
||||
return d.Quote + s + d.Quote
|
||||
}
|
||||
|
||||
func (d *Database) placeholder(n int) string {
|
||||
return strings.Replace(d.Placeholder, "1", strconv.FormatInt(int64(n), 10), 1)
|
||||
}
|
||||
|
||||
// Debug enables debug mode, where unused columns and struct fields will be logged
|
||||
var Debug = true
|
||||
|
||||
type structField struct {
|
||||
column string
|
||||
index int
|
||||
primaryKey bool
|
||||
meddler Meddler
|
||||
}
|
||||
|
||||
type structData struct {
|
||||
columns []string
|
||||
fields map[string]*structField
|
||||
pk string
|
||||
}
|
||||
|
||||
// cache reflection data
|
||||
var fieldsCache = make(map[reflect.Type]*structData)
|
||||
var fieldsCacheMutex sync.Mutex
|
||||
|
||||
// getFields gathers the list of columns from a struct using reflection.
|
||||
func getFields(dstType reflect.Type) (*structData, error) {
|
||||
fieldsCacheMutex.Lock()
|
||||
defer fieldsCacheMutex.Unlock()
|
||||
|
||||
if result, present := fieldsCache[dstType]; present {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// make sure dst is a non-nil pointer to a struct
|
||||
if dstType.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("meddler called with non-pointer destination %v", dstType)
|
||||
}
|
||||
structType := dstType.Elem()
|
||||
if structType.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("meddler called with pointer to non-struct %v", dstType)
|
||||
}
|
||||
|
||||
// gather the list of fields in the struct
|
||||
data := new(structData)
|
||||
data.fields = make(map[string]*structField)
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
f := structType.Field(i)
|
||||
|
||||
// skip non-exported fields
|
||||
if f.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// examine the tag for metadata
|
||||
tag := strings.Split(f.Tag.Get(tagName), ",")
|
||||
|
||||
// was this field marked for skipping?
|
||||
if len(tag) > 0 && tag[0] == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// default to the field name
|
||||
name := f.Name
|
||||
|
||||
// the tag can override the field name
|
||||
if len(tag) > 0 && tag[0] != "" {
|
||||
name = tag[0]
|
||||
}
|
||||
|
||||
// check for a meddler
|
||||
var meddler Meddler = registry["identity"]
|
||||
for j := 1; j < len(tag); j++ {
|
||||
if tag[j] == "pk" {
|
||||
if f.Type.Kind() == reflect.Ptr {
|
||||
return nil, fmt.Errorf("meddler found field %s which is marked as the primary key but is a pointer", f.Name)
|
||||
}
|
||||
|
||||
// make sure it is an int of some kind
|
||||
switch f.Type.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
default:
|
||||
return nil, fmt.Errorf("meddler found field %s which is marked as the primary key, but is not an integer type", f.Name)
|
||||
}
|
||||
|
||||
if data.pk != "" {
|
||||
return nil, fmt.Errorf("meddler found field %s which is marked as the primary key, but a primary key field was already found", f.Name)
|
||||
}
|
||||
data.pk = name
|
||||
} else if m, present := registry[tag[j]]; present {
|
||||
meddler = m
|
||||
} else {
|
||||
return nil, fmt.Errorf("meddler found field %s with meddler %s, but that meddler is not registered", f.Name, tag[j])
|
||||
}
|
||||
}
|
||||
|
||||
if _, present := data.fields[name]; present {
|
||||
return nil, fmt.Errorf("meddler found multiple fields for column %s", name)
|
||||
}
|
||||
data.fields[name] = &structField{
|
||||
column: name,
|
||||
primaryKey: name == data.pk,
|
||||
index: i,
|
||||
meddler: meddler,
|
||||
}
|
||||
data.columns = append(data.columns, name)
|
||||
}
|
||||
|
||||
fieldsCache[dstType] = data
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Columns returns a list of column names for its input struct.
|
||||
func (d *Database) Columns(src interface{}, includePk bool) ([]string, error) {
|
||||
data, err := getFields(reflect.TypeOf(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, elt := range data.columns {
|
||||
if !includePk && elt == data.pk {
|
||||
continue
|
||||
}
|
||||
names = append(names, elt)
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Columns using the Default Database type
|
||||
func Columns(src interface{}, includePk bool) ([]string, error) {
|
||||
return Default.Columns(src, includePk)
|
||||
}
|
||||
|
||||
// ColumnsQuoted is similar to Columns, but it return the list of columns in the form:
|
||||
// `column1`,`column2`,...
|
||||
// using Quote as the quote character.
|
||||
func (d *Database) ColumnsQuoted(src interface{}, includePk bool) (string, error) {
|
||||
unquoted, err := Columns(src, includePk)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, elt := range unquoted {
|
||||
parts = append(parts, d.quoted(elt))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ","), nil
|
||||
}
|
||||
|
||||
// ColumnsQuoted using the Default Database type
|
||||
func ColumnsQuoted(src interface{}, includePk bool) (string, error) {
|
||||
return Default.ColumnsQuoted(src, includePk)
|
||||
}
|
||||
|
||||
// PrimaryKey returns the name and value of the primary key field. The name
|
||||
// is the empty string if there is not primary key field marked.
|
||||
func (d *Database) PrimaryKey(src interface{}) (name string, pk int64, err error) {
|
||||
data, err := getFields(reflect.TypeOf(src))
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
if data.pk == "" {
|
||||
return "", 0, nil
|
||||
}
|
||||
|
||||
name = data.pk
|
||||
field := reflect.ValueOf(src).Elem().Field(data.fields[name].index)
|
||||
switch field.Type().Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
pk = field.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
pk = int64(field.Uint())
|
||||
default:
|
||||
return "", 0, fmt.Errorf("meddler found field %s which is marked as the primary key, but is not an integer type", name)
|
||||
}
|
||||
|
||||
return name, pk, nil
|
||||
}
|
||||
|
||||
// PrimaryKey using the Default Database type
|
||||
func PrimaryKey(src interface{}) (name string, pk int64, err error) {
|
||||
return Default.PrimaryKey(src)
|
||||
}
|
||||
|
||||
// SetPrimaryKey sets the primary key field to the given int value.
|
||||
func (d *Database) SetPrimaryKey(src interface{}, pk int64) error {
|
||||
data, err := getFields(reflect.TypeOf(src))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.pk == "" {
|
||||
return fmt.Errorf("meddler.SetPrimaryKey: no primary key field found")
|
||||
}
|
||||
|
||||
field := reflect.ValueOf(src).Elem().Field(data.fields[data.pk].index)
|
||||
switch field.Type().Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
field.SetInt(pk)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
field.SetUint(uint64(pk))
|
||||
default:
|
||||
return fmt.Errorf("meddler found field %s which is marked as the primary key, but is not an integer type", data.pk)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPrimaryKey using the Default Database type
|
||||
func SetPrimaryKey(src interface{}, pk int64) error {
|
||||
return Default.SetPrimaryKey(src, pk)
|
||||
}
|
||||
|
||||
// Values returns a list of PreWrite processed values suitable for
|
||||
// use in an INSERT or UPDATE query. If includePk is false, the primary
|
||||
// key field is omitted. The columns used are the same ones (in the same
|
||||
// order) as returned by Columns.
|
||||
func (d *Database) Values(src interface{}, includePk bool) ([]interface{}, error) {
|
||||
columns, err := d.Columns(src, includePk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.SomeValues(src, columns)
|
||||
}
|
||||
|
||||
// Values using the Default Database type
|
||||
func Values(src interface{}, includePk bool) ([]interface{}, error) {
|
||||
return Default.Values(src, includePk)
|
||||
}
|
||||
|
||||
// SomeValues returns a list of PreWrite processed values suitable for
|
||||
// use in an INSERT or UPDATE query. The columns used are the same ones (in
|
||||
// the same order) as specified in the columns argument.
|
||||
func (d *Database) SomeValues(src interface{}, columns []string) ([]interface{}, error) {
|
||||
data, err := getFields(reflect.TypeOf(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
structVal := reflect.ValueOf(src).Elem()
|
||||
|
||||
var values []interface{}
|
||||
for _, name := range columns {
|
||||
field, present := data.fields[name]
|
||||
if !present {
|
||||
// write null to the database
|
||||
values = append(values, nil)
|
||||
|
||||
if Debug {
|
||||
log.Printf("meddler.SomeValues: column [%s] not found in struct", name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
saveVal, err := field.meddler.PreWrite(structVal.Field(field.index).Interface())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meddler.SomeValues: PreWrite error on column [%s]: %v", name, err)
|
||||
}
|
||||
values = append(values, saveVal)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// SomeValues using the Default Database type
|
||||
func SomeValues(src interface{}, columns []string) ([]interface{}, error) {
|
||||
return Default.SomeValues(src, columns)
|
||||
}
|
||||
|
||||
// Placeholders returns a list of placeholders suitable for an INSERT or UPDATE query.
|
||||
// If includePk is false, the primary key field is omitted.
|
||||
func (d *Database) Placeholders(src interface{}, includePk bool) ([]string, error) {
|
||||
data, err := getFields(reflect.TypeOf(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var placeholders []string
|
||||
for _, name := range data.columns {
|
||||
if !includePk && name == data.pk {
|
||||
continue
|
||||
}
|
||||
ph := d.placeholder(len(placeholders) + 1)
|
||||
placeholders = append(placeholders, ph)
|
||||
}
|
||||
|
||||
return placeholders, nil
|
||||
}
|
||||
|
||||
// Placeholders using the Default Database type
|
||||
func Placeholders(src interface{}, includePk bool) ([]string, error) {
|
||||
return Default.Placeholders(src, includePk)
|
||||
}
|
||||
|
||||
// PlaceholdersString returns a list of placeholders suitable for an INSERT
|
||||
// or UPDATE query in string form, e.g.:
|
||||
// ?,?,?,?
|
||||
// if includePk is false, the primary key field is omitted.
|
||||
func (d *Database) PlaceholdersString(src interface{}, includePk bool) (string, error) {
|
||||
lst, err := d.Placeholders(src, includePk)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Join(lst, ","), nil
|
||||
}
|
||||
|
||||
// PlaceholdersString using the Default Database type
|
||||
func PlaceholdersString(src interface{}, includePk bool) (string, error) {
|
||||
return Default.PlaceholdersString(src, includePk)
|
||||
}
|
||||
|
||||
// scan a single row of data into a struct.
|
||||
func (d *Database) scanRow(data *structData, rows *sql.Rows, dst interface{}, columns []string) error {
|
||||
// check if there is data waiting
|
||||
if !rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
// get a list of targets
|
||||
targets, err := d.Targets(dst, columns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// perform the scan
|
||||
if err := rows.Scan(targets...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// post-process and copy the target values into the struct
|
||||
if err := d.WriteTargets(dst, columns, targets); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// Targets returns a list of values suitable for handing to a
|
||||
// Scan function in the sql package, complete with meddling. After
|
||||
// the Scan is performed, the same values should be handed to
|
||||
// WriteTargets to finalize the values and record them in the struct.
|
||||
func (d *Database) Targets(dst interface{}, columns []string) ([]interface{}, error) {
|
||||
data, err := getFields(reflect.TypeOf(dst))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
structVal := reflect.ValueOf(dst).Elem()
|
||||
|
||||
var targets []interface{}
|
||||
for _, name := range columns {
|
||||
if field, present := data.fields[name]; present {
|
||||
fieldAddr := structVal.Field(field.index).Addr().Interface()
|
||||
scanTarget, err := field.meddler.PreRead(fieldAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meddler.Targets: PreRead error on column %s: %v", name, err)
|
||||
}
|
||||
targets = append(targets, scanTarget)
|
||||
} else {
|
||||
// no destination, so throw this away
|
||||
targets = append(targets, new(interface{}))
|
||||
|
||||
if Debug {
|
||||
log.Printf("meddler.Targets: column [%s] not found in struct", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// Targets using the Default Database type
|
||||
func Targets(dst interface{}, columns []string) ([]interface{}, error) {
|
||||
return Default.Targets(dst, columns)
|
||||
}
|
||||
|
||||
// WriteTargets post-processes values with meddlers after a Scan from the
|
||||
// sql package has been performed. The list of targets is normally produced
|
||||
// by Targets.
|
||||
func (d *Database) WriteTargets(dst interface{}, columns []string, targets []interface{}) error {
|
||||
if len(columns) != len(targets) {
|
||||
return fmt.Errorf("meddler.WriteTargets: mismatch in number of columns (%d) and targets (%s)",
|
||||
len(columns), len(targets))
|
||||
}
|
||||
|
||||
data, err := getFields(reflect.TypeOf(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
structVal := reflect.ValueOf(dst).Elem()
|
||||
|
||||
for i, name := range columns {
|
||||
if field, present := data.fields[name]; present {
|
||||
fieldAddr := structVal.Field(field.index).Addr().Interface()
|
||||
err := field.meddler.PostRead(fieldAddr, targets[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("meddler.WriteTargets: PostRead error on column [%s]: %v", name, err)
|
||||
}
|
||||
} else {
|
||||
// not destination, so throw this away
|
||||
if Debug {
|
||||
log.Printf("meddler.WriteTargets: column [%s] not found in struct", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTargets using the Default Database type
|
||||
func WriteTargets(dst interface{}, columns []string, targets []interface{}) error {
|
||||
return Default.WriteTargets(dst, columns, targets)
|
||||
}
|
||||
|
||||
// Scan scans a single sql result row into a struct.
|
||||
// It leaves rows ready to be scanned again for the next row.
|
||||
// Returns sql.ErrNoRows if there is no data to read.
|
||||
func (d *Database) Scan(rows *sql.Rows, dst interface{}) error {
|
||||
// get the list of struct fields
|
||||
data, err := getFields(reflect.TypeOf(dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the sql columns
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.scanRow(data, rows, dst, columns)
|
||||
}
|
||||
|
||||
// Scan using the Default Database type
|
||||
func Scan(rows *sql.Rows, dst interface{}) error {
|
||||
return Default.Scan(rows, dst)
|
||||
}
|
||||
|
||||
// ScanRow scans a single sql result row into a struct.
|
||||
// It reads exactly one result row and closes rows when finished.
|
||||
// Returns sql.ErrNoRows if there is no result row.
|
||||
func (d *Database) ScanRow(rows *sql.Rows, dst interface{}) error {
|
||||
// make sure we always close rows
|
||||
defer rows.Close()
|
||||
|
||||
if err := d.Scan(rows, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScanRow using the Default Database type
|
||||
func ScanRow(rows *sql.Rows, dst interface{}) error {
|
||||
return Default.ScanRow(rows, dst)
|
||||
}
|
||||
|
||||
// ScanAll scans all sql result rows into a slice of structs.
|
||||
// It reads all rows and closes rows when finished.
|
||||
// dst should be a pointer to a slice of the appropriate type.
|
||||
// The new results will be appended to any existing data in dst.
|
||||
func (d *Database) ScanAll(rows *sql.Rows, dst interface{}) error {
|
||||
// make sure we always close rows
|
||||
defer rows.Close()
|
||||
|
||||
// make sure dst is an appropriate type
|
||||
dstVal := reflect.ValueOf(dst)
|
||||
if dstVal.Kind() != reflect.Ptr || dstVal.IsNil() {
|
||||
return fmt.Errorf("ScanAll called with non-pointer destination: %T", dst)
|
||||
}
|
||||
sliceVal := dstVal.Elem()
|
||||
if sliceVal.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("ScanAll called with pointer to non-slice: %T", dst)
|
||||
}
|
||||
ptrType := sliceVal.Type().Elem()
|
||||
if ptrType.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("ScanAll expects element to be pointers, found %T", dst)
|
||||
}
|
||||
eltType := ptrType.Elem()
|
||||
if eltType.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("ScanAll expects element to be pointers to structs, found %T", dst)
|
||||
}
|
||||
|
||||
// get the list of struct fields
|
||||
data, err := getFields(ptrType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the sql columns
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// gather the results
|
||||
for {
|
||||
// create a new element
|
||||
eltVal := reflect.New(eltType)
|
||||
elt := eltVal.Interface()
|
||||
|
||||
// scan it
|
||||
if err := d.scanRow(data, rows, elt, columns); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// add to the result slice
|
||||
sliceVal.Set(reflect.Append(sliceVal, eltVal))
|
||||
}
|
||||
}
|
||||
|
||||
// ScanAll using the Default Database type
|
||||
func ScanAll(rows *sql.Rows, dst interface{}) error {
|
||||
return Default.ScanAll(rows, dst)
|
||||
}
|
|
@ -50,12 +50,30 @@
|
|||
"revision": "726cc8b906e3d31c70a9671c90a13716a8d3f50d",
|
||||
"revisionTime": "2016-12-16T23:05:37Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "tvimR1R9mxvCDSUxlnKgIXjJBus=",
|
||||
"path": "github.com/lib/pq",
|
||||
"revision": "67c3f2a8884c9b1aac5503c8d42ae4f73a93511c",
|
||||
"revisionTime": "2017-01-17T20:56:33Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "xppHi82MLqVx1eyQmbhTesAEjx8=",
|
||||
"path": "github.com/lib/pq/oid",
|
||||
"revision": "67c3f2a8884c9b1aac5503c8d42ae4f73a93511c",
|
||||
"revisionTime": "2017-01-17T20:56:33Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "nP2A51J4e45E5HuXMdfQT2hBMfc=",
|
||||
"path": "github.com/mssola/user_agent",
|
||||
"revision": "6ef8cd49fcd9eb0ca7c76a581419c966ed2cb26e",
|
||||
"revisionTime": "2016-12-08T17:59:04Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "8T6S2BULujaIanLkxD1iw5rklBY=",
|
||||
"path": "github.com/russross/meddler",
|
||||
"revision": "f742b0f9424f3bfaffe189e921d734608929e84f",
|
||||
"revisionTime": "2015-05-13T15:34:18Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "vE43s37+4CJ2CDU6TlOUOYE0K9c=",
|
||||
"path": "golang.org/x/crypto/bcrypt",
|
||||
|
|
Loading…
Reference in New Issue