branch: elpa/emacsql commit 1116a0873e65c7e55a8b8414526e25f81dbe95f0 Author: Christopher Wellons <well...@nullprogram.com> Commit: Christopher Wellons <well...@nullprogram.com>
Turn everything into generic functions. This opens up the path for multiple backends. --- Makefile | 4 +- emacsql-pkg.el | 3 + emacsql-sqlite.el | 165 +++++++++++++++++++++++++++ emacsql.el | 327 ++++++++++++++++++------------------------------------ 4 files changed, 278 insertions(+), 221 deletions(-) diff --git a/Makefile b/Makefile index 8f4579f75b..b6ea2b187b 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BATCH := $(EMACS) -batch -Q -L . COMPILE := $(BATCH) -f batch-byte-compile TEST := $(BATCH) -l $(PACKAGE)-tests.elc -f ert-run-tests-batch -EL = $(PACKAGE).el $(PACKAGE)-tests.el +EL = emacsql.el emacsql-sqlite.el $(PACKAGE)-tests.el ELC = $(EL:.el=.elc) @@ -22,4 +22,4 @@ clean: $(RM) *.elc %.elc: %.el - @$(COMPILE) $< + $(COMPILE) $< diff --git a/emacsql-pkg.el b/emacsql-pkg.el new file mode 100644 index 0000000000..89314bddbb --- /dev/null +++ b/emacsql-pkg.el @@ -0,0 +1,3 @@ +(define-package "emacsql" "1.0.0" + "high-level SQL database front-end" + '((emacs "24.1") (cl-lib "0.3"))) diff --git a/emacsql-sqlite.el b/emacsql-sqlite.el new file mode 100644 index 0000000000..243cb73113 --- /dev/null +++ b/emacsql-sqlite.el @@ -0,0 +1,165 @@ +;; emacsql-sqlite.el --- SQLite backend for Emacsql -*- lexical-binding: t; -*- + +;;; Code: + +(require 'cl-lib) +(require 'eieio) +(require 'emacsql) + +(defvar emacsql-sqlite3-executable "sqlite3" + "Path to the sqlite3 executable.") + +(defun emacsql-sqlite3-unavailable-p () + "Return a reason if the sqlite3 executable is not available. + +:no-executable -- cannot find the executable +:cannot-execute -- cannot run the executable +:old-version -- sqlite3 version is too old" + (let ((sqlite3 emacsql-sqlite3-executable)) + (if (null (executable-find sqlite3)) + :no-executable + (condition-case _ + (with-temp-buffer + (call-process sqlite3 nil (current-buffer) nil "--version") + (let ((version (car (split-string (buffer-string))))) + (if (version< version "3.0.0") + :old-version + nil))) + (error :cannot-execute))))) + +(defclass emacsql-sqlite-connection (emacsql-connection) + ((file :initarg :file + :type (or null string) + :documentation "Database file name.")) + (:documentation "A connection to a SQLite database.")) + +;;;###autoload +(cl-defun emacsql-sqlite (file &key debug) + "Open a connected to database stored in FILE. +If FILE is nil use an in-memory database. + +:debug LOG -- When non-nil, log all SQLite commands to a log +buffer. This is for debugging purposes." + (let* ((buffer (generate-new-buffer "*emacsql-connection*")) + (fullfile (if file (expand-file-name file) ":memory:")) + (process (start-process "emacsql" buffer emacsql-sqlite3-executable + "-interactive" fullfile))) + (setf (process-sentinel process) (lambda (_proc _) (kill-buffer buffer))) + (process-send-string process ".mode list\n") + (process-send-string process ".separator ' '\n") + (process-send-string process ".nullvalue nil\n") + (process-send-string process ".prompt ]\n") + (process-send-string process "EMACSQL;\n") ;; error message flush + (let ((connection (make-instance + 'emacsql-sqlite-connection + :process process + :file (when file fullfile) + :log-buffer ))) + (prog1 connection + (when debug + (setf (emacsql-log-buffer connection) + (generate-new-buffer "*emacsql-log*"))) + (emacsql-wait connection) + (emacsql-add-connection connection))))) + +;;;###autoload +(defalias 'emacsql-connect 'emacsql-sqlite) + +(defmethod emacsql-close ((connection emacsql-sqlite-connection)) + "Gracefully exits the SQLite subprocess." + (let ((process (emacsql-process connection))) + (when (and process (process-live-p process)) + (process-send-string process ".exit\n")))) + +(defmethod emacsql-waiting-p ((connection emacsql-sqlite-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))))))) + +(defun emacsql-sqlite--parse (connection) + "Parse SQLite output into an s-expression." + (with-current-buffer (emacsql-buffer connection) + (let ((standard-input (current-buffer))) + (setf (point) (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) (setf row ())) + finally (cl-return rows))))) + +(defvar emacsql-sqlite-condition-alist + '(("unable to open" emacsql-access) + ("cannot open" emacsql-access) + ("source database is busy" emacsql-access) + ("unknown database" emacsql-access) + ("writable" emacsql-access) + ("no such table" emacsql-table) + ("table [^ ]+ already exists" emacsql-table) + ("no such column" emacsql-table) + ("already another table" emacsql-table) + ("Cannot add" emacsql-table) + ("table name" emacsql-table) + ("already an index" emacsql-table) + ("constraint cannot be drop" emacsql-table) + ("database is locked" emacsql-lock) + ("no transaction is active" emacsql-transaction) + ("cannot start a transaction" emacsql-transaction) + ("out of memory" emacsql-fatal) + ("corrupt database" emacsql-fatal) + ("interrupt" emacsql-fatal) + ("values were supplied" emacsql-syntax) + ("mismatch" emacsql-syntax) + ("no such" emacsql-syntax) + ("does not match" emacsql-syntax) + ("circularly defined" emacsql-syntax) + ("parameters are not allowed" emacsql-syntax) + ("missing" emacsql-syntax) + ("is only allowed on" emacsql-syntax) + ("more than one primary key" emacsql-syntax) + ("not constant" emacsql-syntax) + ("duplicate" emacsql-syntax) + ("name reserved" emacsql-syntax) + ("cannot use variables" emacsql-syntax) + ("no tables specified" emacsql-syntax) + ("syntax error" emacsql-syntax) + ("no such function" emacsql-syntax) + ("unknown function" emacsql-syntax) + ("wrong number of arguments" emacsql-syntax) + ("term does not match" emacsql-syntax) + ("clause" emacsql-syntax) + ("tree is too large" emacsql-syntax) + ("too many" emacsql-syntax)) + "List of regexp's mapping sqlite3 output to conditions.") + +(defun emacsql-sqlite-get-condition (message) + "Get condition for MESSAGE provided from SQLite." + (or (cadr (cl-assoc message emacsql-sqlite-condition-alist + :test (lambda (a b) (string-match-p b a)))) + 'emacsql-error)) + +(defun emacsql-sqlite--check-error (conn) + "Return non-nil or throw an appropriate error." + (with-current-buffer (emacsql-buffer conn) + (emacsql-wait conn) + (setf (point) (point-min)) + (prog1 t + (when (looking-at "Error:") + (let* ((message (buffer-substring (line-beginning-position) + (line-end-position))) + (condition (emacsql-sqlite-get-condition message))) + (signal condition (list message))))))) + +(defmethod emacsql ((connection emacsql-sqlite-connection) sql &rest args) + "Send structured SQL expression to CONNECTION with ARGS." + (let ((sql-string (apply #'emacsql-compile sql args))) + (emacsql-clear connection) + (emacsql-send-string connection sql-string) + (emacsql-sqlite--check-error connection) + (emacsql-sqlite--parse connection))) + +(provide 'emacsql-sqlite) + +;;; emacsql-sqlite.el ends here diff --git a/emacsql.el b/emacsql.el index 52b7ea87a6..c9097c6113 100644 --- a/emacsql.el +++ b/emacsql.el @@ -1,29 +1,30 @@ -;;; emacsql.el --- SQL database built on SQLite -*- lexical-binding: t; -*- +;;; emacsql.el --- high-level SQL database front-end -*- lexical-binding: t; -*- ;; This is free and unencumbered software released into the public domain. ;; Author: Christopher Wellons <well...@nullprogram.com> ;; URL: https://github.com/skeeto/emacsql ;; Version: 1.0.0 -;; Package-Requires: ((cl-lib "0.3")) ;;; Commentary: ;; The purpose of this package is to provide a high-level Elisp -;; interface to a high-performance database backend. Not every feature -;; of SQLite will be exposed at the high-level, but most of it should -;; be. +;; interface to a high-performance database back-end. Not every feature +;; of SQL will be exposed, but the important parts should be. -;; Every emacsql function operates on a database connection -;; established with `emacsql-connect', connecting to a SQLite database -;; file. For each connection a sqlite3 inferior process is kept alive. -;; Connections are closed with `elfeed-close'. +;; Most emacsql functions operate on a database connection. A +;; connection to SQLite is established with `emacsql-connect'. For +;; each such connection a sqlite3 inferior process is kept alive in +;; the background. Connections are closed with `emacsql-close'. ;; (defvar db (emacsql-connect "company.db")) -;; Identifiers for tables and columns are symbols. SQL keywords are -;; lisp keywords. Use `emacsql' for sending structured statements to -;; the database. +;; Other types of database connections are available (PostgreSQL via +;; `emacsql-psql'). + +;; Use `emacsql' to send an s-expression SQL statements to a connected +;; database. Identifiers for tables and columns are symbols. SQL +;; keywords are lisp keywords. Anything else is data. ;; (emacsql db [:create-table people [name id salary]]) @@ -57,29 +58,38 @@ ;;; Code: (require 'cl-lib) +(require 'eieio) + +(defclass emacsql-connection () + ((process :type process + :initarg :process + :accessor emacsql-process) + (log-buffer :type (or null buffer) + :initarg :log-buffer + :accessor emacsql-log-buffer + :documentation "Output log (debug).")) + (:documentation "A connection to a SQL database.")) + +(defgeneric emacsql (connection sql &rest args) + "Send SQL s-expression to CONNECTION and return the results.") + +(defgeneric emacsql-close (connection) + "Close CONNECTION and free all resources.") + +(defmethod emacsql-buffer ((connection emacsql-connection)) + "Get proccess buffer for CONNECTION." + (process-buffer (emacsql-process connection))) + +(defmethod emacsql-log ((connection emacsql-connection) message) + "Log MESSAGE into CONNECTION's log. +MESSAGE should not have a newline on the end." + (let ((log (emacsql-log-buffer connection))) + (when log + (with-current-buffer log + (setf (point) (point-max)) + (princ (concat message "\n") log))))) -(defvar emacsql-sqlite3-executable "sqlite3" - "Path to the sqlite3 executable.") - -(defun emacsql-sqlite3-unavailable-p () - "Return a reason if the sqlite3 executable is not available. - -:no-executable -- cannot find the executable -:cannot-execute -- cannot run the executable -:old-version -- sqlite3 version is too old" - (let ((sqlite3 emacsql-sqlite3-executable)) - (if (null (executable-find sqlite3)) - :no-executable - (condition-case _ - (with-temp-buffer - (call-process sqlite3 nil (current-buffer) nil "--version") - (let ((version (car (split-string (buffer-string))))) - (if (version< version "3.0.0") - :old-version - nil))) - (error :cannot-execute))))) - -;;; Error definitions +;; Standard Emacsql errors: (defmacro emacsql-deferror (symbol parents message) "Defines a new error symbol for Emacsql." @@ -94,74 +104,44 @@ "Emacsql had an unhandled condition") (emacsql-deferror emacsql-syntax () "Invalid SQL statement") -(emacsql-deferror emacsql-table () "SQL table error") +(emacsql-deferror emacsql-table () "Table error") (emacsql-deferror emacsql-lock () "Database locked") (emacsql-deferror emacsql-transaction () "Invalid transaction") (emacsql-deferror emacsql-fatal () "Fatal error") -(emacsql-deferror emacsql-file () "Filesystem access error") - -(defvar emacsql-condition-alist - '(("unable to open" emacsql-file) - ("cannot open" emacsql-file) - ("source database is busy" emacsql-file) - ("unknown database" emacsql-file) - ("writable" emacsql-file) - ("no such table" emacsql-table) - ("table [^ ]+ already exists" emacsql-table) - ("no such column" emacsql-table) - ("already another table" emacsql-table) - ("Cannot add" emacsql-table) - ("table name" emacsql-table) - ("already an index" emacsql-table) - ("constraint cannot be drop" emacsql-table) - ("database is locked" emacsql-lock) - ("no transaction is active" emacsql-transaction) - ("cannot start a transaction" emacsql-transaction) - ("out of memory" emacsql-fatal) - ("corrupt database" emacsql-fatal) - ("interrupt" emacsql-fatal) - ("values were supplied" emacsql-syntax) - ("mismatch" emacsql-syntax) - ("no such" emacsql-syntax) - ("does not match" emacsql-syntax) - ("circularly defined" emacsql-syntax) - ("parameters are not allowed" emacsql-syntax) - ("missing" emacsql-syntax) - ("is only allowed on" emacsql-syntax) - ("more than one primary key" emacsql-syntax) - ("not constant" emacsql-syntax) - ("duplicate" emacsql-syntax) - ("name reserved" emacsql-syntax) - ("cannot use variables" emacsql-syntax) - ("no tables specified" emacsql-syntax) - ("syntax error" emacsql-syntax) - ("no such function" emacsql-syntax) - ("unknown function" emacsql-syntax) - ("wrong number of arguments" emacsql-syntax) - ("term does not match" emacsql-syntax) - ("clause" emacsql-syntax) - ("tree is too large" emacsql-syntax) - ("too many" emacsql-syntax)) - "List of regexp's mapping sqlite3 output to conditions.") - -(defun emacsql-get-condition (message) - (or (cadr (cl-assoc message emacsql-condition-alist - :test (lambda (a b) (string-match-p b a)))) - 'emacsql-error)) +(emacsql-deferror emacsql-access () "Database access error") (defun emacsql-error (format &rest args) "Like `error', but signal an emacsql-syntax condition." (signal 'emacsql-syntax (list (apply #'format format args)))) -;;; Connection handling: +;;; Sending and receiving: -(cl-defstruct (emacsql (:constructor emacsql--create)) - "A connection to a SQLite database." - process file log) +(defmethod emacsql-send-string + ((connection emacsql-connection) string &optional no-log) + "Send STRING to CONNECTION, automatically appending newline." + (let ((process (emacsql-process connection))) + (unless no-log (emacsql-log connection string)) + (process-send-string process string) + (process-send-string process "\n"))) + +(defmethod emacsql-clear ((connection emacsql-connection)) + "Clear the process buffer for CONNECTION-SPEC." + (with-current-buffer (emacsql-buffer connection) + (erase-buffer))) -(defun emacsql-buffer (conn) - "Get proccess buffer for CONN." - (process-buffer (emacsql-process conn))) +(defgeneric emacsql-waiting-p (connection) + "Return non-nil if CONNECTION is ready for more input.") + +(defmethod emacsql-wait ((connection emacsql-connection) &optional timeout) + "Block until CONNECTION is waiting for further input." + (let ((end (when timeout (+ (float-time) timeout)))) + (while (and (or (null timeout) (< (float-time) end)) + (not (emacsql-waiting-p connection))) + (accept-process-output (emacsql-process connection) timeout)))) + +(provide 'emacsql) ; end of generic function declarations + +;; Automatic connection cleanup: (defvar emacsql-connections () "Collection of all known emacsql connections. @@ -170,6 +150,12 @@ This collection exists for cleanup purposes.") (defvar emacsql-reap-timer nil "Timer used to check for dead emacsql connections.") +(defun emacsql-add-connection (connection) + "Add CONNECTION to the global connection list." + (emacsql-start-reap-timer) + (push (cons (copy-sequence connection) (emacsql--ref connection)) + emacsql-connections)) + (defun emacsql--ref (thing) "Create a weak reference to THING." (let ((ref (make-hash-table :test 'eq :size 1 :weakness 'value))) @@ -180,64 +166,6 @@ This collection exists for cleanup purposes.") "Retrieve value from REF." (gethash t ref)) -(cl-defun emacsql-connect (file &key debug) - "Open a connected to database stored in FILE. -If FILE is nil use an in-memory database. - -:debug LOG -- When non-nil, log all SQLite commands to a log -buffer. This is for debugging purposes." - (emacsql-start-reap-timer) - (let* ((buffer (generate-new-buffer "*emacsql-connection*")) - (fullfile (if file (expand-file-name file) ":memory:")) - (process (start-process "emacsql" buffer emacsql-sqlite3-executable - "-interactive" fullfile))) - (setf (process-sentinel process) (lambda (_proc _) (kill-buffer buffer))) - (process-send-string process ".mode list\n") - (process-send-string process ".separator ' '\n") - (process-send-string process ".nullvalue nil\n") - (process-send-string process ".prompt ]\n") - (process-send-string process "EMACSQL;\n") ;; force error message - (let ((conn (emacsql--create - :process process - :file (when file fullfile) - :log (when debug (generate-new-buffer "*emacsql-log*"))))) - (prog1 conn - (emacsql--wait conn) - (push (cons (copy-sequence conn) (emacsql--ref conn)) - emacsql-connections))))) - -(defun emacsql-close (conn) - "Close connection to CONN database." - (let ((process (emacsql-process conn))) - (when (and process (process-live-p process)) - (process-send-string process ".exit\n")))) - -(defmacro emacsql-with-connection (conn-spec &rest body) - "Open an Emacsql connection, evaluate BODY, and close the connection. -CONN-SPEC is a connection specification like the call to -`emacsql-connect', establishing a single binding. - - (emacsql-with-connection (db \"company.db\") - (emacsql db [:create-table foo [x]]) - (emacsql db [:insert :into foo :values ([1] [2] [3])]) - (emacsql db [:select * :from foo]))" - (declare (indent 1)) - `(let ((,(car conn-spec) (emacsql-connect ,@(cdr conn-spec)))) - (unwind-protect - (progn ,@body) - (emacsql-close ,(car conn-spec))))) - -(defmacro emacsql-thread (conn &rest statements) - "Thread CONN through STATEMENTS. -A statement can be a list, containing a statement with its arguments." - (declare (indent 1)) - `(let ((emacsql--conn ,conn)) - ,@(cl-loop for statement in statements - when (vectorp statement) - collect (list 'emacsql 'emacsql--conn statement) - else - collect (append (list 'emacsql 'emacsql--conn) statement)))) - (defun emacsql-reap () "Clean up after lost connections." (cl-loop for (conn-copy . ref) in emacsql-connections @@ -260,66 +188,35 @@ A statement can be a list, containing a statement with its arguments." (cancel-timer emacsql-reap-timer) (setf emacsql-reap-timer nil))) -;;; Sending and receiving: +;; Useful macros: -(defun emacsql--log (conn &rest messages) - "Log MESSAGES into CONN's log." - (let ((log (emacsql-log conn))) - (when log - (with-current-buffer log - (setf (point) (point-max)) - (mapc (lambda (s) (princ s log)) messages))))) +(require 'emacsql-sqlite) ; for `emacsql-connect' -(defun emacsql--send (conn string) - "Send STRING to CONN, automatically appending newline." - (let ((process (emacsql-process conn))) - (emacsql--log conn string "\n") - (process-send-string process string) - (process-send-string process "\n"))) +(defmacro emacsql-with-connection (connection-spec &rest body) + "Open an Emacsql connection, evaluate BODY, and close the connection. +CONNECTION-SPEC is a connection specification like the call to +`emacsql-connect', establishing a single binding. -(defun emacsql--clear (conn) - "Clear the process buffer for CONN." - (with-current-buffer (emacsql-buffer conn) - (erase-buffer))) + (emacsql-with-connection (db \"company.db\") + (emacsql db [:create-table foo [x]]) + (emacsql db [:insert :into foo :values ([1] [2] [3])]) + (emacsql db [:select * :from foo]))" + (declare (indent 1)) + `(let ((,(car connection-spec) (emacsql-connect ,@(cdr connection-spec)))) + (unwind-protect + (progn ,@body) + (emacsql-close ,(car connection-spec))))) -(defun emacsql--complete-p (conn) - "Return non-nil if receive buffer has finished filling." - (with-current-buffer (emacsql-buffer conn) - (cond ((= (buffer-size) 1) (string= "]" (buffer-string))) - ((> (buffer-size) 1) (string= "\n]" - (buffer-substring - (- (point-max) 2) (point-max))))))) - -(defun emacsql--parse (conn) - "Parse a query result into an s-expression." - (with-current-buffer (emacsql-buffer conn) - (let ((standard-input (current-buffer))) - (setf (point) (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) (setf row ())) - finally (cl-return rows))))) - -(defun emacsql--check-error (conn) - "Return non-nil or throw an appropriate error." - (with-current-buffer (emacsql-buffer conn) - (emacsql--wait conn) - (setf (point) (point-min)) - (prog1 t - (when (looking-at "Error:") - (let* ((message (buffer-substring (line-beginning-position) - (line-end-position))) - (condition (emacsql-get-condition message))) - (signal condition (list message))))))) - -(defun emacsql--wait (conn &optional timeout) - "Block Emacs until CONN has finished sending output." - (let ((end (when timeout (+ (float-time) timeout)))) - (while (and (or (null timeout) (< (float-time) end)) - (not (emacsql--complete-p conn))) - (accept-process-output (emacsql-process conn) timeout)))) +(defmacro emacsql-thread (connection &rest statements) + "Thread CONNECTION through STATEMENTS. +A statement can be a list, containing a statement with its arguments." + (declare (indent 1)) + `(let ((emacsql--conn ,connection)) + ,@(cl-loop for statement in statements + when (vectorp statement) + collect (list 'emacsql 'emacsql--conn statement) + else + collect (append (list 'emacsql 'emacsql--conn) statement)))) ;;; Escaping: @@ -355,7 +252,7 @@ A statement can be a list, containing a statement with its arguments." (vector (concat "(" (mapconcat #'emacsql-escape-value vector ", ") ")")) (otherwise (emacsql-error "Invalid vector %S" vector)))) -;; Structured SQL compilation: +;; S-expression SQL compilation: (defvar emacsql-expanders () "Alist of all expansion functions.") @@ -430,16 +327,9 @@ a list of (<string> [arg-pos] ...)." (emacsql-error "Invalid var type %S" kind)))))))) (defun emacsql-compile (sql &rest args) - "Compile structured SQL expression into a string." + "Compile s-expression SQL expression into a string." (apply #'emacsql-format (emacsql-expand sql) args)) -(defun emacsql (conn sql &rest args) - "Send structured SQL expression to CONN with ARGS." - (emacsql--clear conn) - (emacsql--send conn (apply #'emacsql-format (emacsql-expand sql) args)) - (emacsql--check-error conn) - (emacsql--parse conn)) - (defun emacsql-var (var) "Return the index number of VAR, or nil if VAR is not a variable. A variable is a symbol that looks like $1, $2, $3, etc. A $ means @@ -846,16 +736,15 @@ definitions for return from a `emacsql-defexpander'." (pop-to-buffer (current-buffer)))))) (defun emacsql-flatten-sql (sql) - "Convert a structured SQL into a flat string for display." + "Convert a s-expression SQL into a flat string for display." (cl-destructuring-bind (string . vars) (emacsql-expand sql) (apply #'format string (cl-loop for i from 1 to (length vars) collect (intern (format "$%d" i)))))) +;;;###autoload (defun emacsql-show-last-sql () - "Display the compiled SQL of the structured SQL expression before point." + "Display the compiled SQL of the s-expression SQL expression before point." (interactive) (emacsql-show-sql (emacsql-flatten-sql (preceding-sexp)))) -(provide 'emacsql) - ;;; emacsql.el ends here