;;; emacsql-psql.el --- EmacSQL back-end for PostgreSQL via psql  -*- lexical-binding:t -*-

;; This is free and unencumbered software released into the public domain.

;; Author: Christopher Wellons <wellons@nullprogram.com>
;; Maintainer: Jonas Bernoulli <emacs.emacsql@jonas.bernoulli.dev>

;; SPDX-License-Identifier: Unlicense

;;; Commentary:

;; This library provides an EmacSQL back-end for PostgreSQL, which
;; uses the standard `psql' command line program.

;; (For an alternative back-end for PostgreSQL, see `emacsql-pg'.)

;;; Code:

(require 'emacsql)

(defvar emacsql-psql-executable "psql"
  "Path to the psql (PostgreSQL client) executable.")

(defun emacsql-psql-unavailable-p ()
  "Return a reason if the psql executable is not available.
:no-executable -- cannot find the executable
:cannot-execute -- cannot run the executable
:old-version -- sqlite3 version is too old"
  (let ((psql emacsql-psql-executable))
    (if (null (executable-find psql))
        :no-executable
      (condition-case _
          (with-temp-buffer
            (call-process psql nil (current-buffer) nil "--version")
            (let ((version (cl-third (split-string (buffer-string)))))
              (if (version< version "1.0.0")
                  :old-version
                nil)))
        (error :cannot-execute)))))

(defconst emacsql-psql-reserved
  (emacsql-register-reserved
   '( ALL ANALYSE ANALYZE AND ANY AS ASC AUTHORIZATION BETWEEN BINARY
      BOTH CASE CAST CHECK COLLATE COLUMN CONSTRAINT CREATE CROSS
      CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER DEFAULT
      DEFERRABLE DESC DISTINCT DO ELSE END EXCEPT FALSE FOR FOREIGN
      FREEZE FROM FULL GRANT GROUP HAVING ILIKE IN INITIALLY INNER
      INTERSECT INTO IS ISNULL JOIN LEADING LEFT LIKE LIMIT LOCALTIME
      LOCALTIMESTAMP NATURAL NEW NOT NOTNULL NULL OFF OFFSET OLD ON
      ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY REFERENCES RIGHT
      SELECT SESSION_USER SIMILAR SOME TABLE THEN TO TRAILING TRUE
      UNION UNIQUE USER USING VERBOSE WHEN WHERE))
  "List of all of PostgreSQL's reserved words.
http://www.postgresql.org/docs/7.3/static/sql-keywords-appendix.html")

(defclass emacsql-psql-connection (emacsql-connection)
  ((dbname :reader emacsql-psql-dbname :initarg :dbname)
   (types :allocation :class
          :reader emacsql-types
          :initform '((integer "BIGINT")
                      (float "DOUBLE PRECISION")
                      (object "TEXT")
                      (nil "TEXT"))))
  "A connection to a PostgreSQL database via psql.")

(cl-defun emacsql-psql (dbname &key username hostname port debug)
  "Connect to a PostgreSQL server using the psql command line program."
  (let ((args (list dbname)))
    (when username
      (push username args))
    (push "-n" args)
    (when port
      (push "-p" args)
      (push port args))
    (when hostname
      (push "-h" args)
      (push hostname args))
    (setq args (nreverse args))
    (let* ((buffer (generate-new-buffer " *emacsql-psql*"))
           (psql emacsql-psql-executable)
           (command (mapconcat #'shell-quote-argument (cons psql args) " "))
           (process (start-process-shell-command
                     "emacsql-psql" buffer (concat "stty raw && " command)))
           (connection (make-instance 'emacsql-psql-connection
                                      :handle process
                                      :dbname dbname)))
      (setf (process-sentinel process)
            (lambda (proc _) (kill-buffer (process-buffer proc))))
      (set-process-query-on-exit-flag (oref connection handle) nil)
      (when debug (emacsql-enable-debugging connection))
      (mapc (apply-partially #'emacsql-send-message connection)
            '("\\pset pager off"
              "\\pset null nil"
              "\\a"
              "\\t"
              "\\f ' '"
              "SET client_min_messages TO ERROR;"
              "\\set PROMPT1 ]"
              "EMACSQL;")) ; error message flush
      (emacsql-wait connection)
      (emacsql connection
               [:set (= default-transaction-isolation 'SERIALIZABLE)])
      (emacsql-register connection))))

(cl-defmethod emacsql-close ((connection emacsql-psql-connection))
  (let ((process (oref connection handle)))
    (when (process-live-p process)
      (process-send-string process "\\q\n"))))

(cl-defmethod emacsql-send-message ((connection emacsql-psql-connection) message)
  (let ((process (oref connection handle)))
    (process-send-string process message)
    (process-send-string process "\n")))

(cl-defmethod emacsql-waiting-p ((connection emacsql-psql-connection))
  (with-current-buffer (emacsql-buffer connection)
    (cond ((= (buffer-size) 1) (string= "]" (buffer-string)))
          ((> (buffer-size) 1) (string= "\n]" (buffer-substring
                                               (- (point-max) 2)
                                               (point-max)))))))

(cl-defmethod emacsql-check-error ((connection emacsql-psql-connection))
  (with-current-buffer (emacsql-buffer connection)
    (let ((case-fold-search t))
      (goto-char (point-min))
      (when (looking-at "error:")
        (let* ((beg (line-beginning-position))
               (end (line-end-position)))
          (signal 'emacsql-error (list (buffer-substring beg end))))))))

(cl-defmethod emacsql-parse ((connection emacsql-psql-connection))
  (emacsql-check-error connection)
  (with-current-buffer (emacsql-buffer connection)
    (let ((standard-input (current-buffer)))
      (goto-char (point-min))
      (cl-loop until (looking-at "]")
               collect (read) into row
               when (looking-at "\n")
               collect row into rows
               and do (progn (forward-char 1) (setq row ()))
               finally (cl-return rows)))))

(provide 'emacsql-psql)

;;; emacsql-psql.el ends here
