Add support for SSL modes in the PG connection string, fix #137.

In passing, refactor the *pgconn- dynamic bindings in favor of directly
using the connection property list straight from the connection string
parser, processing it when necessary. That allows to make it simple to
add an internal :use-ssl property.
This commit is contained in:
Dimitri Fontaine 2014-12-16 18:45:43 +01:00
parent 1996256f8f
commit 073f012d1a
7 changed files with 135 additions and 80 deletions

View File

@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "PGLOADER" "1" "October 2014" "ff" ""
.TH "PGLOADER" "1" "December 2014" "ff" ""
.
.SH "NAME"
\fBpgloader\fR \- PostgreSQL data loader
@ -278,7 +278,7 @@ The \fB<postgresql\-url>\fR parameter is expected to be given as a \fIConnection
.
.nf
postgresql://[user[:password]@][netloc][:port][/dbname][?schema\.table]
postgresql://[user[:password]@][netloc][:port][/dbname][?option=value&\.\.\.]
.
.fi
.
@ -337,7 +337,19 @@ Should be a proper identifier (letter followed by a mix of letters, digits and t
When omitted, the \fIdbname\fR defaults to the value of the environment variable \fBPGDATABASE\fR, and if that is unset, to the \fIuser\fR value as determined above\.
.
.IP "\(bu" 4
The only optional parameter should be a possibly qualified table name\.
\fIoptions\fR
.
.IP
The optional parameters must be supplied with the form \fBname=value\fR, and you may use several parameters by separating them away using an ampersand (\fB&\fR) character\.
.
.IP
Only two options are supported here, \fItablename\fR (which might be qualified with a schema name) and \fIsslmode\fR\.
.
.IP
The \fIsslmode\fR parameter values can be one of \fBdisable\fR, \fBallow\fR, \fBprefer\fR or \fBrequire\fR\.
.
.IP
For backward compatibility reasons, it\'s possible to specify the \fItablename\fR option directly, without spelling out the \fBtablename=\fR parts\.
.
.IP "" 0
.

View File

@ -257,7 +257,7 @@ The `<postgresql-url>` parameter is expected to be given as a *Connection URI*
as documented in the PostgreSQL documentation at
http://www.postgresql.org/docs/9.3/static/libpq-connect.html#LIBPQ-CONNSTRING.
postgresql://[user[:password]@][netloc][:port][/dbname][?schema.table]
postgresql://[user[:password]@][netloc][:port][/dbname][?option=value&...]
Where:
@ -307,7 +307,21 @@ Where:
variable `PGDATABASE`, and if that is unset, to the *user* value as
determined above.
- The only optional parameter should be a possibly qualified table name.
- *options*
The optional parameters must be supplied with the form `name=value`, and
you may use several parameters by separating them away using an
ampersand (`&`) character.
Only two options are supported here, *tablename* (which might be
qualified with a schema name) and *sslmode*.
The *sslmode* parameter values can be one of `disable`, `allow`,
`prefer` or `require`.
For backward compatibility reasons, it's possible to specify the
*tablename* option directly, without spelling out the `tablename=`
parts.
### Regular Expressions

View File

@ -16,11 +16,7 @@
#:*copy-batch-rows*
#:*copy-batch-size*
#:*concurrent-batches*
#:*pgconn-host*
#:*pgconn-port*
#:*pgconn-user*
#:*pgconn-pass*
#:*pg-dbname*
#:*pgconn*
#:*pg-settings*
#:*myconn-host*
#:*myconn-port*
@ -121,11 +117,16 @@
;;;
;;; PostgreSQL Connection Credentials and Session Settings
;;;
(defparameter *pgconn-host* "localhost")
(defparameter *pgconn-port* (parse-integer (getenv-default "PGPORT" "5432")))
(defparameter *pgconn-user* (uiop:getenv "USER"))
(defparameter *pgconn-pass* "pgpass")
(defparameter *pg-dbname* nil)
(defparameter *pgconn*
'(:type :postgresql
:host "localhost"
:port (parse-integer (getenv-default "PGPORT" "5432"))
:user (uiop:getenv "USER")
:pass "pgpass"
:dbname nil
:table-name nil
:use-ssl nil)
"Default PostgreSQL connection string.")
(defparameter *pg-settings* nil "An alist of GUC names and values.")
;;;

View File

@ -82,15 +82,46 @@
(declare (ignore slash))
(list :dbname dbname)))
(defrule dsn-option-ssl-disable "disable" (:constant :no))
(defrule dsn-option-ssl-allow "allow" (:constant :try))
(defrule dsn-option-ssl-prefer "prefer" (:constant :try))
(defrule dsn-option-ssl-require "require" (:constant :yes))
(defrule dsn-option-ssl (and "sslmode" "=" (or dsn-option-ssl-disable
dsn-option-ssl-allow
dsn-option-ssl-prefer
dsn-option-ssl-require))
(:lambda (ssl)
(destructuring-bind (key e val) ssl
(declare (ignore key e))
(cons :use-ssl val))))
(defrule qualified-table-name (and namestring "." namestring)
(:destructure (schema dot table)
(declare (ignore dot))
(format nil "~a.~a" (text schema) (text table))))
(defrule dsn-table-name (and "?" (or qualified-table-name namestring))
(:destructure (qm name)
(defrule dsn-table-name (or qualified-table-name namestring)
(:lambda (name)
(cons :table-name name)))
(defrule dsn-option-table-name (and (? (and "tablename" "="))
dsn-table-name)
(:lambda (opt-tn)
(bind (((_ table-name) opt-tn))
table-name)))
(defrule dsn-option (or dsn-option-ssl dsn-option-table-name))
(defrule another-dsn-option (and "&" dsn-option)
(:lambda (source)
(bind (((_ option) source)) option)))
(defrule dsn-options (and "?" dsn-option (* another-dsn-option))
(:lambda (options)
(destructuring-bind (qm opt1 opts) options
(declare (ignore qm))
(list :table-name name)))
(alexandria:alist-plist `(,opt1 ,@opts)))))
(defrule pgsql-prefix (and (or "postgresql" "postgres" "pgsql") "://")
(:constant (list :type :postgresql)))
@ -99,7 +130,7 @@
(? dsn-user-password)
(? dsn-hostname)
dsn-dbname
(? dsn-table-name))
(? dsn-options))
(:lambda (uri)
(destructuring-bind (&key type
user
@ -107,7 +138,8 @@
host
port
dbname
table-name)
table-name
use-ssl)
(apply #'append uri)
;; Default to environment variables as described in
;; http://www.postgresql.org/docs/9.3/static/app-psql.html
@ -122,6 +154,7 @@
#-unix "localhost"))
:port (or port (parse-integer
(getenv-default "PGPORT" "5432")))
:use-ssl use-ssl
:dbname (or dbname (getenv-default "PGDATABASE" user))
:table-name table-name))))
@ -142,18 +175,8 @@
(defun pgsql-connection-bindings (pg-db-uri gucs)
"Generate the code needed to set PostgreSQL connection bindings."
(destructuring-bind (&key ((:host pghost))
((:port pgport))
((:user pguser))
((:password pgpass))
((:dbname pgdb))
&allow-other-keys)
pg-db-uri
`((*pgconn-host* ',pghost)
(*pgconn-port* ,pgport)
(*pgconn-user* ,pguser)
(*pgconn-pass* ,pgpass)
(*pg-dbname* ,pgdb)
(destructuring-bind (&key ((:dbname pgdb)) &allow-other-keys) pg-db-uri
`((*pgconn* ',pg-db-uri)
(*pg-settings* ',gucs)
(pgloader.pgsql::*pgsql-reserved-keywords*
(pgloader.pgsql:list-reserved-keywords ,pgdb)))))

View File

@ -20,7 +20,7 @@
(muffle-warning))))
(progn ,@forms)))
(defmacro with-pgsql-transaction ((&key (dbname *pg-dbname*) database) &body forms)
(defmacro with-pgsql-transaction ((&key dbname username database) &body forms)
"Run FORMS within a PostgreSQL transaction to DBNAME, reusing DATABASE if
given. To get the connection spec from the DBNAME, use `get-connection-spec'."
(if database
@ -31,11 +31,9 @@
(set-session-gucs *pg-settings* :transaction t)
,@forms)))
;; no database given, create a new database connection
`(let (#+unix (cl-postgres::*unix-socket-dir* (get-unix-socket-dir))
;; if no dbname is given at macro-expansion time, we want to
;; use the current value of *pg-dbname* at run time
(*pg-dbname* (or ,dbname *pg-dbname*)))
(pomo:with-connection (get-connection-spec *pg-dbname*)
`(let (#+unix (cl-postgres::*unix-socket-dir* (get-unix-socket-dir)))
(pomo:with-connection (get-connection-spec :dbname ,dbname
:username ,username)
(log-message :debug "CONNECT")
(set-session-gucs *pg-settings*)
(handling-pgsql-notices ()
@ -47,32 +45,41 @@
"Run FROMS within a PostgreSQL connection to DBNAME. To get the connection
spec from the DBNAME, use `get-connection-spec'."
`(let (#+unix (cl-postgres::*unix-socket-dir* (get-unix-socket-dir)))
(pomo:with-connection (get-connection-spec ,dbname)
(log-message :debug "CONNECT ~s" (get-connection-spec ,dbname))
(pomo:with-connection (get-connection-spec :dbname ,dbname)
(log-message :debug "CONNECT ~s" (get-connection-spec :dbname ,dbname))
(set-session-gucs *pg-settings*)
(handling-pgsql-notices ()
,@forms))))
(defun get-unix-socket-dir ()
"When *pgcon-host* is a (cons :unix path) value, return the right value
"When *pgconn* host is a (cons :unix path) value, return the right value
for cl-postgres::*unix-socket-dir*."
(if (and (consp *pgconn-host*)
(eq :unix (car *pgconn-host*)))
;; set to *pgconn-host* value
(directory-namestring (fad:pathname-as-directory (cdr *pgconn-host*)))
(destructuring-bind (&key host &allow-other-keys) *pgconn*
(if (and (consp host) (eq :unix (car host)))
;; set to *pgconn* host value
(directory-namestring (fad:pathname-as-directory (cdr host)))
;; keep as is.
cl-postgres::*unix-socket-dir*))
cl-postgres::*unix-socket-dir*)))
(defun get-connection-spec (dbname &key (with-port t))
(defun get-connection-spec (&key dbname username (with-port t))
"pomo:with-connection and cl-postgres:open-database and open-db-writer are
not using the same connection spec format..."
(let* ((host (if (and (consp *pgconn-host*) (eq :unix (car *pgconn-host*)))
:unix
*pgconn-host*))
(conspec (list dbname *pgconn-user* *pgconn-pass* host)))
(destructuring-bind (&key type host port user password
((:dbname pgconn-dbname))
use-ssl
table-name)
*pgconn*
(declare (ignore type table-name))
(let* ((host (if (and (consp host) (eq :unix (car host))) :unix host))
(conspec (list (or dbname pgconn-dbname)
(or username user)
password
host)))
(if with-port
(append conspec (list :port *pgconn-port*))
(append conspec (list *pgconn-port*)))))
(setf conspec (append conspec (list :port port)))
(setf conspec (append conspec (list port))))
(if use-ssl (append conspec (list :use-ssl use-ssl)) conspec))))
(defun set-session-gucs (alist &key transaction database)
"Set given GUCs to given values for the current session."
@ -113,15 +120,14 @@
(defun list-databases (&optional (username "postgres"))
"Connect to a local database and get the database list"
(let* ((*pgconn-user* username))
(with-pgsql-transaction (:dbname "postgres")
(with-pgsql-transaction (:dbname "postgres" :username username)
(loop for (dbname) in (pomo:query
"select datname
from pg_database
where datname !~ 'postgres|template'")
collect dbname))))
collect dbname)))
(defun list-tables (&optional (dbname *pg-dbname*))
(defun list-tables (&optional dbname)
"Return an alist of tables names and list of columns to pay attention to."
(with-pgsql-transaction (:dbname dbname)
(loop for (relname colarray) in (pomo:query "

View File

@ -65,7 +65,7 @@
(defmacro with-stats-collection ((table-name
&key
(dbname *pg-dbname*)
dbname
summary
use-result-as-read
use-result-as-rows
@ -73,11 +73,13 @@
&body forms)
"Measure time spent in running BODY into STATE, accounting the seconds to
given DBNAME and TABLE-NAME"
(destructuring-bind (&key ((:dbname pgconn-dbname)) &allow-other-keys)
*pgconn*
(let ((result (gensym "result"))
(secs (gensym "secs")))
`(let ((*pg-dbname* (or ,dbname *pg-dbname*)))
(prog2
(pgstate-add-table ,pgstate *pg-dbname* ,table-name)
(secs (gensym "secs"))
(dbname (or dbname pgconn-dbname)))
`(prog2
(pgstate-add-table ,pgstate ,dbname ,table-name)
(multiple-value-bind (,result ,secs)
(timing ,@forms)
(cond ((and ,use-result-as-read ,use-result-as-rows)

View File

@ -11,10 +11,7 @@
(*copy-batch-rows* . ,*copy-batch-rows*)
(*copy-batch-size* . ,*copy-batch-size*)
(*concurrent-batches* . ,*concurrent-batches*)
(*pgconn-host* . ',*pgconn-host*)
(*pgconn-port* . ,*pgconn-port*)
(*pgconn-user* . ,*pgconn-user*)
(*pgconn-pass* . ,*pgconn-pass*)
(*pgconn* . ',*pgconn*)
(*pg-settings* . ',*pg-settings*)
(*myconn-host* . ',*myconn-host*)
(*myconn-port* . ,*myconn-port*)