Suggestion: if database.c is redone would it be possible to name the
functions `database-get' etc rather than `get-database' ?
Here is some proof-of-concept code for supporting multiple
database-like backends with a generic interface: it supports dbm,
pg.el and in-memory/filesystem-persistent backends. The trickiest bit
is handling searches uniformly ; billp's syntax could work with SQL and
ldap, but for dbm & in-memory would require mapping over the entire
database.
EIEIO is at <
URL:http://www.ultranet.com/~zappo/eieio.shtml> (with
stuff like this I occasionally manage to forget I'm programming in a
crummy imitation of a Lisp :-).
;;; db.el -- various types of databases for emacs
;;
;; Copyright: (C) 1999 Eric Marsden
;; Author: Eric Marsden <emarsden(a)mail.dotcom.fr>
;; Time-stamp: <1999-11-16 ecm>
;;
;;
;; quasi-uniform access to different types of databases from emacs:
;; PostgreSQL tables using pg.el, XEmacs dbm/berkeley_db database API,
;; and file+hashtable-based in-memory tables.
(require 'cl)
(require 'eieio)
(defclass db:database ()
()
"The class of objects accessible via the database API")
(defclass db:dbm-database (db:database)
((file
:initarg :file
:documentation "Path to file which contains the data for this database")
(access
:initarg :access
:initform "rw+"
:documentation "Access rights to the database (any combination of
`r', `w' and `+' for read, write and creation flags)")
(mode
:initarg :mode
:initform 420
:documentation "Permissions on the database")
(db
:initarg :db
:initform nil))
"A dbm database")
(defclass db:pg-database (db:database)
((dbname
:initform "template1"
:documentation "Name of the database")
(user
:documentation "Username under which to connect to the database")
(password
:initform "")
(port
:initform 5432
:documentation "Port where postmaster is listening")
(host
:initform "localhost")
(connection
:initform nil))
"A PostgreSQL database, accessed via pg.el")
(defclass db:file-database (db:database)
((file
:initarg :file
:documentation "Path to file which contains the data for this database")
(access
:initarg :access
:initform "rw+"
:documentation "Access rights to the database (any combination of
`r', `w' and `+' for read, write and creation flags)")
(mode
:initarg :mode
:initform 420
:documentation "Permissions on the database")
(db
:initarg :db
:initform nil))
"A hashtable-based database (persistence via filesystem)")
(defgeneric db:open ((db database))
"Initialize a connection to the database")
(defgeneric db:close ((db database))
"Close a connection to the database")
(defgeneric db:live-p ((db database))
"Is the connection alive?")
(defgeneric db:get ((db database) key &optional default)
"Extract value corresponding to KEY from the database")
(defgeneric db:put ((db database) key value &optional replace)
"Add a relation between KEY and VALUE in the database.
If REPLACE is non-nil, replace old value if it exists.")
(defgeneric db:map ((db database) function)
"Apply FUNCTION to each (key, value) pair in the database.
Order is unspecified; FUNCTION should take two arguments.")
(defgeneric db:remove ((db database) key)
"Remove the relation for KEY from the database.")
;; PostgreSQL methods (using pg.el)
(defmethod db:open ((db db:pg-database))
(require 'pg)
(setf (slot-value db :connection)
(with-slots (db)
(pg:connect dbname user password host port))))
(defmethod db:close ((db db:pg-database))
(require 'pg)
(pg:disconnect (slot-value db :connection))
(setf (db:pg-database-connection db) nil))
(defmethod db:live-p ((db db:pg-database))
(slot-value db :connection))
(defmethod db:get ((db db:pg-database) key &optional default)
(require 'pg)
(let* ((con (slot-value db :connection))
(sql (format "SELECT value FROM emacs_db WHERE key = %s" key))
(res (pg:exec con sql)))
(pg:result res :tuple 1)))
(defmethod db:put ((db db:pg-database) key value &optional replace)
)
(defmethod db:map ((db db:pg-database) function)
)
(defmethod db:remove ((db db:pg-database) key)
)
;; dbm methods using XEmacs API
(when (fboundp 'databasep)
(defmethod db:open ((db db:dbm-database))
(setf (slot-value db :db)
(with-slots ((f file) (a access) (m mode))
db
(open-database f 'dbm nil a m))))
(defmethod db:close ((db db:dbm-database))
(close-database (slot-value db :db)))
(defmethod db:live-p ((db db:dbm-database))
(slot-value db :db))
(defmethod db:get ((db db:dbm-database) key &optional default)
(get-database key (slot-value db :db) default))
(defmethod db:put ((db db:dbm-database) key val &optional replace)
(put-database key val (slot-value db :db) replace))
(defmethod db:map ((db db:dbm-database) function)
(map-database function (slot-value db :db)))
(defmethod db:remove ((db db:dbm-database) key)
(remove-database key (slot-value db :db)))
) ;; (fboundp 'databasep)
;; hash-table databases (in memory until closed)
(defmethod db:open ((db db:file-database))
(let* ((filename (slot-value db :file))
(access (slot-value db :access))
buf data table)
(cond ((position ?+ access)
;; start with an empty hash table
(setq table (make-hash-table :test #'string=))
(setf (slot-value db :db) table))
(t
;; read data from disk into hash table
(unless (file-readable-p filename)
(error "database is not readable" filename))
(setq buf (find-file-noselect filename t t))
(setq data (read buf))
(setq table (make-hash-table :test #'string=))
(mapcar #'(lambda (c)
(setf (gethash (car c) table) (cdr c)))
table)
(setf (slot-value db :db) table)))))
(defmethod db:close ((db db:file-database))
(let* ((filename (slot-value db :file))
(access (slot-value db :access))
(table (slot-value db :db))
(buf (get-buffer-create " *db-work*"))
(alist '()))
(when (and (position ?+ access)
(file-exists-p filename))
(delete-file filename))
;; write data to alist
(maphash #'(lambda (key val)
(push (cons key val) alist))
table)
(save-excursion
(set-buffer buf)
(erase-buffer)
(prin1 alist)
(write-file filename)
(kill-buffer buf))))
(defmethod db:live-p ((db db:file-database))
(slot-value db :db))
(defmethod db:get ((db db:file-database) key &optional default)
(gethash key (slot-value db :db) default))
(defmethod db:put ((db db:file-database) key val &optional replace)
(setf (gethash key (slot-value db :db)) val))
(defmethod db:map ((db db:file-database) function)
(maphash function (slot-value db :db)))
(defmethod db:remove ((db db:file-database) key)
(remhash key (slot-value db :db)))
;; testing
(defun db:test/dbm ()
(require 'db)
(let ((db (make-instance 'db:dbm-database :file "/tmp/try")))
(db:open db)
(loop for i from 1 to 200
for positive = (format "%d" i)
for negative = (format "%d" (- i))
do (db:put db positive negative))
(loop for i from 200 downto 1
for positive = (format "%d" i)
for negative = (format "%d" (- i))
unless (string= (db:get db positive) negative)
do (error "test failed for i = %d" i))
(db:close db)))
(defun db:test/file ()
(require 'db)
(let ((db (make-instance 'db:file-database
:file "/tmp/zob.db")))
(db:open db)
(loop for i from 1 to 200
for positive = (format "%d" i)
for negative = (format "%d" (- i))
do (db:put db positive negative))
(loop for i from 200 downto 1
for positive = (format "%d" i)
for negative = (format "%d" (- i))
unless (string= (db:get db positive) negative)
do (error "test failed for i = %d" i))
(db:close db)))
(provide 'db)
;; db.el ends here