+2010-03-02 Nathan Bird <nathan@acceleration.net>
+ * doc/: Added a README on how to build doc; now builds on Ubuntu.
+ * sql/oodml.lisp: READ-SQL-VALUE now has explicit method for
+ handling double-floats and the default method will no longer
+ attempt to convert values that have already been converted.
+ * sql/syntax.lisp: Introduce file-enable-sql-reader-syntax which
+ enables the syntax for the scope of the file without trying to
+ keep track of the current syntax state.
+ * sql/pool.lisp: Introduce
+ clsql-sys:*db-pool-max-free-connections* which is a heuristic
+ threshold for when to disconnect a connection rather than
+ returning it to the pool.
+ * sql/pool.lisp: Check connections for validity before returning
+ to the user.
+
2010-03-01 Kevin Rosenberg <kevin@rosenberg.net>
* db-mysql/mysql-api.lisp: Remove spurious enumeration
* improve large object api and extend to databases beyond postgresql
* add support for prepared statements
+
+RACE CONDITIONS
+* sql/databases.lisp: *connected-databases* is shared globally but not modified in a threadsafe manner.
REDHAT=$(shell expr "`cat /etc/issue 2> /dev/null`" : '.*Red Hat.*')
MANDRAKE=$(shell expr "`cat /etc/issue 2> /dev/null`" : '.*Mandrake.*')
DARWIN=$(shell expr "`uname -a`" : '.*Darwin.*')
+UBUNTU=$(shell expr "`cat /etc/issue 2> /dev/null`" : '.*Ubuntu.*')
ifneq (${DEBIAN},0)
else
ifneq (${DARWIN},0)
OS=darwin
+ else
+ ifneq (${UBUNTU},0)
+ OS:=debian
+ endif
endif
endif
endif
--- /dev/null
+Building the documentation:
+
+You will need the following packages:
+ * xsltproc
+ * docbook
+ * docbook-xml
+ * docbook-xsl
+ * docbook-xsl-doc-html
+ * fop
+
+These are the debian/ubuntu package names; on other systems there are probably similar.
+
+
+General Build:
+> make
+
+Check the validity of the source
+> make check
+
+Build just the html:
+> make html
+
+Build just the pdf:
+> make pdf
save those till we get to the many-to-many relation examples.
</para>
-
- <title>Object Oriented Class Relations</title>
-
-<para>
-&clsql; provides an Object Oriented Data Definition Language, which
-provides a mapping from &sql; tables to CLOS objects. By default class
-inheritance is handled by including all the columns from parent
-classes into the child class. This means your database schema becomes
-very much denormalized. The class option <symbol>:normalizedp</symbol>
-can be used to disable the default behaviour and have &clsql;
-normalize the database schemas of inherited classes.
-</para>
-
-<para>
-See <link linkend="def-view-class"><function>def-view-class</function></link>
-for more information.
-</para>
-
+<simplesect>
+ <title>Object Oriented Class Relations</title>
+
+ <para>
+ &clsql; provides an Object Oriented Data Definition Language, which
+ provides a mapping from &sql; tables to CLOS objects. By default class
+ inheritance is handled by including all the columns from parent
+ classes into the child class. This means your database schema becomes
+ very much denormalized. The class option <symbol>:normalizedp</symbol>
+ can be used to disable the default behaviour and have &clsql;
+ normalize the database schemas of inherited classes.
+ </para>
+
+ <para>
+ See <link linkend="def-view-class"><function>def-view-class</function></link>
+ for more information.
+ </para>
+</simplesect>
</sect1>
<sect1 id="csql-creat">
this class.
</para>
+ <refsect2>
<title>Normalized inheritance schemas</title>
<para>
Specifying that <symbol>:normalizedp</symbol> is <symbol>T</symbol>
CLSQL> (nick test-user)
"test-user"
</screen>
-
+ </refsect2>
</refsect1>
<refsect1>
<title>Examples</title>
utilities for enabling and disabling the square bracket reader
syntax and for constructing symbolic SQL expressions.
</para>
+ <tip>
+ <title>Tip: just want it on</title>
+ <simpara>
+ <link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link> at the top of each file is easiest.
+ </simpara>
+ </tip>
</partintro>
<refentry id="enable-sql-reader-syntax">
<para>
Modifies the default readtable.
</para>
+ <warning>
+ <para>
+ &clsql; tries to keep track of whether the syntax has already been enabled. This can be problematic if the syntax is somehow disabled externally to &clsql; as future attempts to enable the syntax will do nothing--the system thinks it is already enabled. This may happen if there is an enable, but no disable, in a file that is processed with load or compile-file as the lisp implementation will restore the readtable on completion. Or, even if there is a disable but a compiler-error is encountered before running the disable. If you encounter this try running <link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link> a couple times in the REPL.
+ </para>
+ <para>See <link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link> for an alternative.</para>
+ </warning>
</refsect1>
<refsect1>
<title>Affected by</title>
<member><link linkend="locally-enable-sql-reader-syntax"><function>locally-enable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-disable-sql-reader-syntax"><function>locally-disable-sql-reader-syntax</function></link></member>
<member><link linkend="restore-sql-reader-syntax-state"><function>restore-sql-reader-syntax-state</function></link></member>
+ <member><link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link></member>
</simplelist>
</refsect1>
<refsect1>
<member><link linkend="locally-enable-sql-reader-syntax"><function>locally-enable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-disable-sql-reader-syntax"><function>locally-disable-sql-reader-syntax</function></link></member>
<member><link linkend="restore-sql-reader-syntax-state"><function>restore-sql-reader-syntax-state</function></link></member>
+ <member><link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link></member>
</simplelist>
</refsect1>
<refsect1>
</refmeta>
<refnamediv>
<refname>LOCALLY-ENABLE-SQL-READER-SYNTAX</refname>
- <refpurpose>Globally enable square bracket reader syntax.</refpurpose>
+ <refpurpose>Locally enable square bracket reader syntax.</refpurpose>
<refclass>Macro</refclass>
</refnamediv>
<refsect1>
<para>
Modifies the default readtable.
</para>
+ <warning>
+ <para>
+ &clsql; tries to keep track of whether the syntax has already been enabled. This can be problematic if the syntax is somehow disabled externally to &clsql; as future attempts to enable the syntax will do nothing--the system thinks it is already enabled. This may happen if there is an enable, but no disable, in a file that is processed with load or compile-file as the lisp implementation will restore the readtable on completion. Or, even if there is a disable but a compiler-error is encountered before running the disable. If you encounter this try running <link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link> a couple times in the REPL.
+ </para>
+ <para>See <link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link> for an alternative.</para>
+ </warning>
</refsect1>
<refsect1>
<title>Affected by</title>
<member><link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-disable-sql-reader-syntax"><function>locally-disable-sql-reader-syntax</function></link></member>
<member><link linkend="restore-sql-reader-syntax-state"><function>restore-sql-reader-syntax-state</function></link></member>
+ <member><link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link></member>
</simplelist>
</refsect1>
<refsect1>
<member><link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-enable-sql-reader-syntax"><function>locally-enable-sql-reader-syntax</function></link></member>
<member><link linkend="restore-sql-reader-syntax-state"><function>restore-sql-reader-syntax-state</function></link></member>
+ <member><link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link></member>
</simplelist>
</refsect1>
<refsect1>
<member><link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-enable-sql-reader-syntax"><function>locally-enable-sql-reader-syntax</function></link></member>
<member><link linkend="locally-disable-sql-reader-syntax"><function>locally-disable-sql-reader-syntax</function></link></member>
+ <member><link linkend="file-enable-sql-reader-syntax"><function>file-enable-sql-reader-syntax</function></link></member>
</simplelist>
</refsect1>
<refsect1>
</refsect1>
</refentry>
+ <refentry id="file-enable-sql-reader-syntax">
+ <refmeta>
+ <refentrytitle>FILE-ENABLE-SQL-READER-SYNTAX</refentrytitle>
+ </refmeta>
+ <refnamediv>
+ <refname>FILE-ENABLE-SQL-READER-SYNTAX</refname>
+ <refpurpose>
+ Enable the square bracket reader syntax for the duration of the file.
+ </refpurpose>
+ <refclass>Macro</refclass>
+ </refnamediv>
+ <refsect1>
+ <title>Syntax</title>
+ <synopsis>
+ <function>file-enable-sql-reader-syntax</function> => <returnvalue></returnvalue></synopsis>
+ </refsect1>
+ <refsect1>
+ <title>Arguments and Values</title>
+ <para>None.</para>
+ </refsect1>
+ <refsect1>
+ <title>Description</title>
+ <para>Uncoditionally enables the SQL reader syntax. Unlike <link
+ linkend="enable-sql-reader-syntax">
+ <function>enable-sql-reader-syntax</function></link> and <link
+ linkend="disable-sql-reader-syntax">
+ <function>disable-sql-reader-syntax</function></link> which try to keep track of whether
+ the syntax has been enabled or disabled and keep track of the old read-table for restoration this function just enables it unconditionally.
+ </para>
+ <para>Once enabled this way there is no corresponding disable function but instead relies on being used in a file context. The spec for <ulink url="http://www.lispworks.com/documentation/lw51/CLHS/Body/f_load.htm">load</ulink> and <ulink url="http://www.lispworks.com/documentation/lw51/CLHS/Body/f_cmp_fi.htm">compile-file</ulink> states that the *readtable* will be restored after processing the file.</para>
+ </refsect1>
+ <refsect1>
+ <title>Examples</title>
+ <para>Intended to be used at the top of a file that contains sql reader syntax.</para>
+ <screen>
+ (in-package :my-package)
+ (clsql:file-enable-sql-reader-syntax)
+ ...
+ ;;functions that use the square bracket syntax.
+ </screen>
+ </refsect1>
+ <refsect1>
+ <title>Side Effects</title>
+ <para>
+ Modifies the readtable for #\[ and #\]
+ </para>
+ </refsect1>
+ <refsect1>
+ <title>Affected by</title>
+ <para>None.</para>
+ </refsect1>
+ <refsect1>
+ <title>Exceptional Situations</title>
+ <para>
+ None.
+ </para>
+ </refsect1>
+ <refsect1>
+ <title>See Also</title>
+ <simplelist>
+ <member><link linkend="enable-sql-reader-syntax"><function>enable-sql-reader-syntax</function></link></member>
+ <member><link linkend="disable-sql-reader-syntax"><function>disable-sql-reader-syntax</function></link></member>
+ <member><link linkend="locally-enable-sql-reader-syntax"><function>locally-enable-sql-reader-syntax</function></link></member>
+ <member><link linkend="locally-disable-sql-reader-syntax"><function>locally-disable-sql-reader-syntax</function></link></member>
+ </simplelist>
+ </refsect1>
+ <refsect1>
+ <title>Notes</title>
+ <para>
+ Unique to &clsql;, not present in &commonsql;.
+ </para>
+ </refsect1>
+ </refentry>
+
<refentry id="sql">
<refmeta>
<refentrytitle>SQL</refentrytitle>
CONNECT. Meaningful values are :new, :warn-new, :error, :warn-old
and :old.")
+;;TODO: this variable appears to be global, not thread specific and is
+;; not protected when modifying the list.
(defvar *connected-databases* nil
"List of active database objects.")
(setf *default-database* (car *connected-databases*)))
t))
(when (database-disconnect database)
+ ;;TODO: RACE COND: 2 threads disconnecting could stomp on *connected-databases*
(setf *connected-databases* (delete database *connected-databases*))
(when (eq database *default-database*)
(setf *default-database* (car *connected-databases*)))
nil)
(:documentation "Free the resources of a prepared statement."))
+(defgeneric database-acquire-from-conn-pool (database)
+ (:documentation "Acquire a database connection from the pool. This
+is a chance to test the connection for validity before returning it to
+the user. If this function returns NIL or throws an error that
+database connection is considered bad and we make a new one.
+
+Database objects have a chance to specialize, otherwise the default
+method uses the database-underlying-type and tries to do something
+appropriate."))
+
+(defgeneric database-release-to-conn-pool (database)
+ (:documentation "Chance for the database to cleanup before it is
+ returned to the connection pool."))
+
;; Checks for closed database
(defmethod database-disconnect :before ((database database))
:result-types nil
:database vd))))
(when res
+ (setf (slot-value instance 'view-database) vd)
(get-slot-values-from-view instance (mapcar #'car sels) (car res))))
(pres)
(t nil)))))
(res (select att-ref :from view-table :where view-qual
:result-types nil)))
(when res
+ (setf (slot-value instance 'view-database) vd)
(get-slot-values-from-view instance (list slot-def) (car res))))))
(defmethod update-slot-with-null ((object standard-db-object)
(format nil "~F" val))))
(defmethod read-sql-value (val type database db-type)
- (declare (ignore type database db-type))
- (read-from-string val))
+ (declare (ignore database db-type))
+ (cond
+ ((null type) val) ;;we have no desired type, just give the value
+ ((typep val type) val) ;;check that it hasn't already been converted.
+ ((typep val 'string) (read-from-string val)) ;;maybe read will just take care of it?
+ (T (error "Unable to read-sql-value ~a as type ~a" val type))))
(defmethod read-sql-value (val (type (eql 'string)) database db-type)
(declare (ignore database db-type))
(declare (ignore database db-type))
;; writing 1.0 writes 1, so we we *really* want a float, must do (float ...)
(etypecase val
- (string
- (float (read-from-string val)))
- (float
- val)))
+ (string (float (read-from-string val)))
+ (float val)))
+
+(defmethod read-sql-value (val (type (eql 'double-float)) database db-type)
+ (declare (ignore database db-type))
+ ;; writing 1.0 writes 1, so if we *really* want a float, must do (float ...)
+ (etypecase val
+ (string (float
+ (let ((*read-default-float-format* 'double-float))
+ (read-from-string val))
+ 1.0d0))
+ (double-float val)
+ (float (coerce val 'double-float))))
(defmethod read-sql-value (val (type (eql 'boolean)) database db-type)
(declare (ignore database db-type))
#:database-destroy
#:database-probe
#:database-list
+ #:database-acquire-from-conn-pool
+ #:database-release-to-conn-pool
#:db-backend-has-create/destroy-db?
#:db-type-has-views?
#:*loaded-database-types*
#:reload-database-types
#:is-database-open
+ #:*db-pool-max-free-connections*
;; Large objects
#:database-create-large-object
#:locally-disable-sql-reader-syntax
#:locally-enable-sql-reader-syntax
#:restore-sql-reader-syntax-state
+ #:file-enable-sql-reader-syntax
;; SQL operations (operations.lisp)
#:sql-query
(in-package #:clsql-sys)
+(defparameter *db-pool-max-free-connections* 4
+ "Threshold of free-connections in the pool before we disconnect a
+ database rather than returning it to the pool. This is really a heuristic
+that should, on avg keep the free connections about this size.")
+
(defvar *db-pool* (make-hash-table :test #'equal))
(defvar *db-pool-lock* (make-process-lock "DB Pool lock"))
(defclass conn-pool ()
((connection-spec :accessor connection-spec :initarg :connection-spec)
(database-type :accessor pool-database-type :initarg :pool-database-type)
- (free-connections :accessor free-connections
- :initform (make-array 5 :fill-pointer 0 :adjustable t))
- (all-connections :accessor all-connections
- :initform (make-array 5 :fill-pointer 0 :adjustable t))
+ (free-connections :accessor free-connections :initform nil)
+ (all-connections :accessor all-connections :initform nil)
(lock :accessor conn-pool-lock
- :initform (make-process-lock "Connection pool"))))
-
-(defun acquire-from-conn-pool (pool)
- (or (with-process-lock ((conn-pool-lock pool) "Acquire from pool")
- (when (plusp (length (free-connections pool)))
- (let ((pconn (vector-pop (free-connections pool))))
- ;; test if connection still valid.
- ;; Currently, on supported on MySQL
- (cond
- ((eq :mysql (database-type pconn))
- (handler-case
- (database-query "SHOW ERRORS LIMIT 1" pconn nil nil)
- (error (e)
- ;; we could check for error type 2006 for "SERVER GONE AWAY",
- ;; but, it's safer just to disconnect the pooled conn for any error
- (warn "Database connection ~S had an error when attempted to be acquired from the pool:
+ :initform (make-process-lock "Connection pool"))))
+
+
+(defun acquire-from-pool (connection-spec database-type &optional pool)
+ "Try to find a working database connection in the pool or create a new
+one if needed. This performs 1 query against the DB to ensure it's still
+valid. When possible (postgres, mssql) that query will be a reset
+command to put the connection back into its default state."
+ (unless (typep pool 'conn-pool)
+ (setf pool (find-or-create-connection-pool connection-spec database-type)))
+ (or
+ (loop for pconn = (with-process-lock ((conn-pool-lock pool) "Acquire")
+ (pop (free-connections pool)))
+ always pconn
+ thereis
+ ;; test if connection still valid.
+ ;; (e.g. db reboot -> invalid connection )
+ (handler-case
+ (progn (database-acquire-from-conn-pool pconn)
+ pconn)
+ (sql-database-error (e)
+ ;; we could check for a specific error,
+ ;; but, it's safer just to disconnect the pooled conn for any error ?
+ (warn "Database connection ~S had an error while acquiring from the pool:
~S
Disconnecting.~%"
- pconn e)
- (ignore-errors (database-disconnect pconn))
- nil)
- (:no-error (res fields)
- (declare (ignore res fields))
- pconn)))
- (t
- pconn)))))
- (let ((conn (connect (connection-spec pool)
- :database-type (pool-database-type pool)
- :if-exists :new
- :make-default nil)))
- (with-process-lock ((conn-pool-lock pool) "Acquire from pool")
- (vector-push-extend conn (all-connections pool))
- (setf (conn-pool conn) pool))
- conn)))
-
-(defun release-to-conn-pool (conn)
- (let ((pool (conn-pool conn)))
- (with-process-lock ((conn-pool-lock pool) "Release to pool")
- (vector-push-extend conn (free-connections pool)))))
+ pconn e)
+ ;;run database disconnect to give chance for cleanup
+ ;;there, then remove it from the lists of connected
+ ;;databases.
+ (%pool-force-disconnect pconn)
+ (with-process-lock ((conn-pool-lock pool) "remove dead conn")
+ (setf (all-connections pool)
+ (delete pconn (all-connections pool))))
+ nil)))
+ (let ((conn (connect (connection-spec pool)
+ :database-type (pool-database-type pool)
+ :if-exists :new
+ :make-default nil)))
+ (with-process-lock ((conn-pool-lock pool) "new conection")
+ (push conn (all-connections pool))
+ (setf (conn-pool conn) pool))
+ conn)))
+
+(defun release-to-pool (database)
+ "Release a database connection to the pool. The backend will have a
+chance to do cleanup."
+ (let ((pool (conn-pool database)))
+ (cond
+ ;;We read the list of free-connections outside the lock. This
+ ;;should be fine as long as that list is never dealt with
+ ;;destructively (push and pop destructively modify the place,
+ ;;not the list). Multiple threads getting to this test at the
+ ;;same time might result in the free-connections getting
+ ;;longer... meh.
+ ((>= (length (free-connections pool))
+ *db-pool-max-free-connections*)
+ (%pool-force-disconnect database)
+ (with-process-lock ((conn-pool-lock pool) "Remove extra Conn")
+ (setf (all-connections pool)
+ (delete database (all-connections pool)))))
+ (t
+ ;;let it do cleanup
+ (database-release-to-conn-pool database)
+ (with-process-lock ((conn-pool-lock pool) "Release to pool")
+ (push database (free-connections pool)))))))
+
+(defmethod database-acquire-from-conn-pool (database)
+ (case (database-underlying-type database)
+ (:postgresql
+ (database-execute-command "RESET ALL" database))
+ (:mysql
+ (database-query "SHOW ERRORS LIMIT 1" database nil nil))
+ (:mssql
+ ;; rpc escape sequence since this can't be called as a normal sp.
+ ;;http://msdn.microsoft.com/en-us/library/aa198358%28SQL.80%29.aspx
+ (database-execute-command "{rpc sp_reset_connection}" database))
+ (T
+ (database-query "SELECT 1;" database '(integer) nil))))
+
+(defmethod database-release-to-conn-pool (database)
+ (case (database-underlying-type database)
+ (:postgresql
+ (ignore-errors
+ ;;http://www.postgresql.org/docs/current/static/sql-discard.html
+ ;;this was introduced relatively recently, wrap in ignore-errors
+ ;;so that it doesn't choke older versions.
+ (database-execute-command "DISCARD ALL" database)))))
(defun clear-conn-pool (pool)
(with-process-lock ((conn-pool-lock pool) "Clear pool")
- (loop for conn across (all-connections pool)
- do (setf (conn-pool conn) nil)
- ;; disconnect may error if remote side closed connection
- (ignore-errors (disconnect :database conn)))
- (setf (fill-pointer (free-connections pool)) 0)
- (setf (fill-pointer (all-connections pool)) 0))
+ (mapc #'%pool-force-disconnect (all-connections pool))
+ (setf (all-connections pool) nil
+ (free-connections pool) nil))
nil)
(defun find-or-create-connection-pool (connection-spec database-type)
if not found"
(with-process-lock (*db-pool-lock* "Find-or-create connection")
(let* ((key (list connection-spec database-type))
- (conn-pool (gethash key *db-pool*)))
+ (conn-pool (gethash key *db-pool*)))
(unless conn-pool
- (setq conn-pool (make-instance 'conn-pool
- :connection-spec connection-spec
- :pool-database-type database-type))
- (setf (gethash key *db-pool*) conn-pool))
+ (setq conn-pool (make-instance 'conn-pool
+ :connection-spec connection-spec
+ :pool-database-type database-type))
+ (setf (gethash key *db-pool*) conn-pool))
conn-pool)))
-(defun acquire-from-pool (connection-spec database-type &optional pool)
- (unless (typep pool 'conn-pool)
- (setf pool (find-or-create-connection-pool connection-spec database-type)))
- (acquire-from-conn-pool pool))
-
-(defun release-to-pool (database)
- (release-to-conn-pool database))
-
(defun disconnect-pooled (&optional clear)
- "Disconnects all connections in the pool."
+ "Disconnects all connections in the pool. When clear, also deletes
+the pool objects."
(with-process-lock (*db-pool-lock* "Disconnect pooled")
(maphash
#'(lambda (key conn-pool)
- (declare (ignore key))
- (clear-conn-pool conn-pool))
+ (declare (ignore key))
+ (clear-conn-pool conn-pool))
*db-pool*)
(when clear (clrhash *db-pool*)))
t)
+(defun %pool-force-disconnect (database)
+ "Force disconnection of a connection from the pool."
+ ;;so it isn't just returned to pool
+ (setf (conn-pool database) nil)
+ ;; disconnect may error if remote side closed connection
+ (ignore-errors (disconnect :database database)))
+
;(defun pool-start-sql-recording (pool &key (types :command))
; "Start all stream in the pool recording actions of TYPES"
; (dolist (con (pool-connections pool))
'(eval-when (:compile-toplevel :load-toplevel :execute)
(%enable-sql-reader-syntax)))
+(defmacro file-enable-sql-reader-syntax ()
+ "Turns on the SQL reader syntax for the rest of the file.
+The CL spec says that when finished loading a file the original
+*readtable* is restored. clhs COMPILE-FILE"
+ '(eval-when (:compile-toplevel :load-toplevel :execute)
+ (setf *readtable* (copy-readtable))
+ (set-macro-character *sql-macro-open-char* #'sql-reader-open)
+ (set-macro-character *sql-macro-close-char* (get-macro-character #\)))))
+
(defun %enable-sql-reader-syntax ()
(unless *original-readtable*
(setf *original-readtable* *readtable*