[dovecot-cvs] dovecot/src/lib-sql driver-mysql.c, 1.9,
1.10 driver-pgsql.c, 1.3, 1.4 sql-api-private.h, 1.3,
1.4 sql-api.c, 1.4, 1.5 sql-api.h, 1.4, 1.5
cras at dovecot.org
cras at dovecot.org
Sat Dec 10 20:57:14 EET 2005
Update of /var/lib/cvs/dovecot/src/lib-sql
In directory talvi:/tmp/cvs-serv30564
Modified Files:
driver-mysql.c driver-pgsql.c sql-api-private.h sql-api.c
sql-api.h
Log Message:
Added support for transactions and synchronous SQL queries.
Index: driver-mysql.c
===================================================================
RCS file: /var/lib/cvs/dovecot/src/lib-sql/driver-mysql.c,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -d -r1.9 -r1.10
--- driver-mysql.c 9 Jun 2005 18:44:22 -0000 1.9
+++ driver-mysql.c 10 Dec 2005 18:57:11 -0000 1.10
@@ -2,6 +2,7 @@
#include "lib.h"
#include "buffer.h"
+#include "str.h"
#include "sql-api-private.h"
#ifdef HAVE_MYSQL
@@ -61,6 +62,12 @@
unsigned int fields_count;
};
+struct mysql_transaction_context {
+ struct sql_transaction_context ctx;
+
+ string_t *queries;
+};
+
extern struct sql_result driver_mysql_result;
extern struct sql_result driver_mysql_error_result;
@@ -340,43 +347,59 @@
(void)driver_mysql_do_query(db, query, &conn);
}
-static void driver_mysql_query(struct sql_db *_db, const char *query,
+static void driver_mysql_query(struct sql_db *db, const char *query,
sql_query_callback_t *callback, void *context)
{
+ struct sql_result *result;
+
+ result = sql_query_s(db, query);
+ result->callback = TRUE;
+ callback(result, context);
+ sql_result_free(result);
+}
+
+static struct sql_result *
+driver_mysql_query_s(struct sql_db *_db, const char *query)
+{
struct mysql_db *db = (struct mysql_db *)_db;
struct mysql_connection *conn;
- struct mysql_result result;
+ struct mysql_result *result;
+
+ result = i_new(struct mysql_result, 1);
+ result->api = driver_mysql_result;
+ result->api.db = _db;
+ result->conn = conn;
switch (driver_mysql_do_query(db, query, &conn)) {
case 0:
/* not connected */
- callback(&sql_not_connected_result, context);
- return;
-
+ result->api = sql_not_connected_result;
+ break;
case 1:
/* query ok */
- memset(&result, 0, sizeof(result));
- result.api = driver_mysql_result;
- result.api.db = _db;
- result.conn = conn;
- result.result = mysql_store_result(conn->mysql);
- if (result.result == NULL)
+ result->result = mysql_store_result(conn->mysql);
+ if (result->result != NULL)
break;
-
- callback(&result.api, context);
- mysql_free_result(result.result);
- return;
+ /* fallback */
case -1:
/* error */
+ result->api = driver_mysql_error_result;
break;
}
- /* error */
- memset(&result, 0, sizeof(result));
- result.api = driver_mysql_error_result;
- result.api.db = _db;
- result.conn = conn;
- callback(&result.api, context);
+ return &result->api;
+}
+
+static void driver_mysql_result_free(struct sql_result *_result)
+{
+ struct mysql_result *result = (struct mysql_result *)_result;
+
+ if (_result == &sql_not_connected_result || _result->callback)
+ return;
+
+ if (result->result != NULL)
+ mysql_free_result(result->result);
+ i_free(result);
}
static int driver_mysql_result_next_row(struct sql_result *_result)
@@ -468,18 +491,106 @@
return mysql_error(result->conn->mysql);
}
+static struct sql_transaction_context *
+driver_mysql_transaction_begin(struct sql_db *db)
+{
+ struct mysql_transaction_context *ctx;
+
+ ctx = i_new(struct mysql_transaction_context, 1);
+ ctx->ctx.db = db;
+ ctx->queries = str_new(default_pool, 1024);
+ return &ctx->ctx;
+}
+
+static void
+driver_mysql_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ const char *error;
+
+ if (sql_transaction_commit_s(ctx, &error) < 0)
+ callback(error, context);
+ else
+ callback(NULL, context);
+}
+
+static int
+driver_mysql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+ struct sql_result *result;
+ int ret = 0;
+
+ *error_r = NULL;
+
+ if (str_len(ctx->queries) > 0) {
+ str_append(ctx->queries, "COMMIT;");
+
+ result = sql_query_s(_ctx->db, str_c(ctx->queries));
+ if (sql_result_next_row(result) < 0) {
+ *error_r = sql_result_get_error(result);
+ ret = -1;
+ }
+ sql_result_free(result);
+ }
+ sql_transaction_rollback(_ctx);
+ return ret;
+}
+
+static void
+driver_mysql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+
+ str_free(ctx->queries);
+ i_free(ctx);
+}
+
+static void
+driver_mysql_update(struct sql_transaction_context *_ctx, const char *query)
+{
+ struct mysql_transaction_context *ctx =
+ (struct mysql_transaction_context *)_ctx;
+
+ /* FIXME: with mysql we're just appending everything into one big
+ string which gets committed in sql_transaction_commit(). we could
+ avoid this if we knew for sure that transactions actually worked,
+ but I don't know how to do that.. */
+ if (str_len(ctx->queries) == 0) {
+ /* try to use a transaction in any case,
+ even if it doesn't work. */
+ str_append(ctx->queries, "BEGIN;");
+ }
+ str_append(ctx->queries, query);
+ str_append_c(ctx->queries, ';');
+}
+
struct sql_db driver_mysql_db = {
+ "mysql",
+
driver_mysql_init,
driver_mysql_deinit,
driver_mysql_get_flags,
driver_mysql_connect_all,
driver_mysql_exec,
- driver_mysql_query
+ driver_mysql_query,
+ driver_mysql_query_s,
+
+ driver_mysql_transaction_begin,
+ driver_mysql_transaction_commit,
+ driver_mysql_transaction_commit_s,
+ driver_mysql_transaction_rollback,
+
+ driver_mysql_update
};
struct sql_result driver_mysql_result = {
NULL,
+ driver_mysql_result_free,
driver_mysql_result_next_row,
driver_mysql_result_get_fields_count,
driver_mysql_result_get_field_name,
@@ -487,7 +598,9 @@
driver_mysql_result_get_field_value,
driver_mysql_result_find_field_value,
driver_mysql_result_get_values,
- driver_mysql_result_get_error
+ driver_mysql_result_get_error,
+
+ FALSE
};
static int
@@ -499,8 +612,11 @@
struct sql_result driver_mysql_error_result = {
NULL,
+ driver_mysql_result_free,
driver_mysql_result_error_next_row,
NULL, NULL, NULL, NULL, NULL, NULL,
- driver_mysql_result_get_error
+ driver_mysql_result_get_error,
+
+ FALSE
};
#endif
Index: driver-pgsql.c
===================================================================
RCS file: /var/lib/cvs/dovecot/src/lib-sql/driver-pgsql.c,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- driver-pgsql.c 9 Jun 2005 18:44:22 -0000 1.3
+++ driver-pgsql.c 10 Dec 2005 18:57:11 -0000 1.4
@@ -2,6 +2,7 @@
#include "lib.h"
#include "ioloop.h"
+#include "ioloop-internal.h" /* kind of dirty, but it should be fine.. */
#include "sql-api-private.h"
#ifdef HAVE_PGSQL
@@ -22,6 +23,10 @@
struct pgsql_queue *queue, **queue_tail;
struct timeout *queue_to;
+ struct ioloop *ioloop;
+ struct sql_result *sync_result;
+
+ char *error;
time_t last_connect;
unsigned int connecting:1;
unsigned int connected:1;
@@ -49,6 +54,18 @@
struct pgsql_result *result;
};
+struct pgsql_transaction_context {
+ struct sql_transaction_context ctx;
+
+ sql_commit_callback_t *callback;
+ void *context;
+
+ const char *error;
+
+ unsigned int opened:1;
+ unsigned int failed:1;
+};
+
extern struct sql_result driver_pgsql_result;
static void queue_send_next(struct pgsql_db *db);
@@ -167,11 +184,12 @@
struct pgsql_db *db = (struct pgsql_db *)_db;
driver_pgsql_close(db);
+ i_free(db->error);
i_free(db);
}
static enum sql_db_flags
-driver_mysql_get_flags(struct sql_db *db __attr_unused__)
+driver_pgsql_get_flags(struct sql_db *db __attr_unused__)
{
return 0;
}
@@ -199,12 +217,14 @@
queue_send_next(db);
}
-static void result_finish(struct pgsql_result *result)
+static void driver_pgsql_result_free(struct sql_result *_result)
{
- struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
+ struct pgsql_result *result = (struct pgsql_result *)_result;
+
+ if (result->api.callback)
+ return;
- if (result->callback != NULL)
- result->callback(&result->api, result->context);
if (result->pgres != NULL) {
PQclear(result->pgres);
@@ -212,6 +232,7 @@
i_assert(db->io == NULL);
db->io = io_add(PQsocket(db->pg), IO_READ,
consume_results, db);
+ db->io_dir = IO_READ;
consume_results(db);
} else {
db->querying = FALSE;
@@ -225,6 +246,20 @@
queue_send_next(db);
}
+static void result_finish(struct pgsql_result *result)
+{
+ struct pgsql_db *db = (struct pgsql_db *)result->api.db;
+ int free_result = TRUE;
+
+ if (result->callback != NULL) {
+ result->api.callback = TRUE;
+ result->callback(&result->api, result->context);
+ free_result = db->sync_result != &result->api;
+ }
+ if (free_result)
+ driver_pgsql_result_free(&result->api);
+}
+
static void get_result(void *context)
{
struct pgsql_result *result = context;
@@ -240,6 +275,7 @@
if (db->io == NULL) {
db->io = io_add(PQsocket(db->pg), IO_READ,
get_result, result);
+ db->io_dir = IO_READ;
}
return;
}
@@ -302,6 +338,7 @@
/* write blocks */
db->io = io_add(PQsocket(db->pg), IO_WRITE,
flush_callback, result);
+ db->io_dir = IO_WRITE;
} else {
get_result(result);
}
@@ -418,6 +455,44 @@
do_query(result, query);
}
+static void pgsql_query_s_callback(struct sql_result *result, void *context)
+{
+ struct pgsql_db *db = context;
+
+ db->sync_result = result;
+ io_loop_stop(db->ioloop);
+}
+
+static struct sql_result *
+driver_pgsql_query_s(struct sql_db *_db, const char *query)
+{
+ struct pgsql_db *db = (struct pgsql_db *)_db;
+ struct io old_io;
+
+ if (db->io == NULL)
+ db->ioloop = io_loop_create(default_pool);
+ else {
+ /* have to move our existing I/O handler to new I/O loop */
+ old_io = *db->io;
+ io_remove(db->io);
+
+ db->ioloop = io_loop_create(default_pool);
+
+ db->io = io_add(old_io.fd, old_io.condition,
+ old_io.callback, old_io.context);
+ }
+
+ driver_pgsql_query(_db, query, pgsql_query_s_callback, db);
+
+ io_loop_run(db->ioloop);
+ io_loop_destroy(db->ioloop);
+ db->ioloop = NULL;
+
+ i_assert(db->io == NULL);
+
+ return db->sync_result;
+}
+
static int driver_pgsql_result_next_row(struct sql_result *_result)
{
struct pgsql_result *result = (struct pgsql_result *)_result;
@@ -543,6 +618,7 @@
static const char *driver_pgsql_result_get_error(struct sql_result *_result)
{
struct pgsql_result *result = (struct pgsql_result *)_result;
+ struct pgsql_db *db = (struct pgsql_db *)_result->db;
const char *msg;
size_t len;
@@ -552,22 +628,141 @@
/* Error message should contain trailing \n, we don't want it */
len = strlen(msg);
- return len == 0 || msg[len-1] != '\n' ? msg :
- t_strndup(msg, len-1);
+ i_free(db->error);
+ db->error = len == 0 || msg[len-1] != '\n' ?
+ i_strdup(msg) : i_strndup(msg, len-1);
+
+ return db->error;
+}
+
+static struct sql_transaction_context *
+driver_pgsql_transaction_begin(struct sql_db *db)
+{
+ struct pgsql_transaction_context *ctx;
+
+ ctx = i_new(struct pgsql_transaction_context, 1);
+ ctx->ctx.db = db;
+ return &ctx->ctx;
+}
+
+static void
+transaction_commit_callback(struct sql_result *result, void *context)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)context;
+
+ if (sql_result_next_row(result) < 0)
+ ctx->callback(sql_result_get_error(result), ctx->context);
+ else
+ ctx->callback(NULL, ctx->context);
+}
+
+static void
+driver_pgsql_transaction_commit(struct sql_transaction_context *_ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+
+ if (ctx->failed) {
+ callback(ctx->error, context);
+ sql_exec(_ctx->db, "ROLLBACK");
+ i_free(ctx);
+ return;
+ }
+
+ ctx->callback = callback;
+ ctx->context = context;
+
+ sql_query(_ctx->db, "COMMIT", transaction_commit_callback, ctx);
+}
+
+static int
+driver_pgsql_transaction_commit_s(struct sql_transaction_context *_ctx,
+ const char **error_r)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+ struct sql_result *result;
+
+ if (ctx->failed) {
+ *error_r = ctx->error;
+ sql_exec(_ctx->db, "ROLLBACK");
+ } else {
+ result = sql_query_s(_ctx->db, "COMMIT");
+ if (sql_result_next_row(result) < 0)
+ *error_r = sql_result_get_error(result);
+ else
+ *error_r = NULL;
+ sql_result_free(result);
+ }
+
+ i_free(ctx);
+ return *error_r == NULL ? 0 : -1;
+}
+
+static void
+driver_pgsql_transaction_rollback(struct sql_transaction_context *_ctx)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+
+ sql_exec(_ctx->db, "ROLLBACK");
+ i_free(ctx);
+}
+
+static void
+transaction_update_callback(struct sql_result *result, void *context)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)context;
+
+ if (sql_result_next_row(result) < 0) {
+ ctx->failed = TRUE;
+ ctx->error = sql_result_get_error(result);
+ }
+}
+
+static void
+driver_pgsql_update(struct sql_transaction_context *_ctx, const char *query)
+{
+ struct pgsql_transaction_context *ctx =
+ (struct pgsql_transaction_context *)_ctx;
+
+ if (ctx->failed)
+ return;
+
+ if (!ctx->opened) {
+ ctx->opened = TRUE;
+ sql_query(_ctx->db, "BEGIN", transaction_update_callback, ctx);
+ }
+
+ sql_query(_ctx->db, query, transaction_update_callback, ctx);
}
struct sql_db driver_pgsql_db = {
+ "pgsql",
+
driver_pgsql_init,
driver_pgsql_deinit,
- driver_mysql_get_flags,
+ driver_pgsql_get_flags,
driver_pgsql_connect,
driver_pgsql_exec,
- driver_pgsql_query
+ driver_pgsql_query,
+ driver_pgsql_query_s,
+
+ driver_pgsql_transaction_begin,
+ driver_pgsql_transaction_commit,
+ driver_pgsql_transaction_commit_s,
+ driver_pgsql_transaction_rollback,
+
+ driver_pgsql_update
};
struct sql_result driver_pgsql_result = {
NULL,
+ driver_pgsql_result_free,
driver_pgsql_result_next_row,
driver_pgsql_result_get_fields_count,
driver_pgsql_result_get_field_name,
@@ -575,7 +770,9 @@
driver_pgsql_result_get_field_value,
driver_pgsql_result_find_field_value,
driver_pgsql_result_get_values,
- driver_pgsql_result_get_error
+ driver_pgsql_result_get_error,
+
+ FALSE
};
#endif
Index: sql-api-private.h
===================================================================
RCS file: /var/lib/cvs/dovecot/src/lib-sql/sql-api-private.h,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- sql-api-private.h 9 Jun 2005 18:44:22 -0000 1.3
+++ sql-api-private.h 10 Dec 2005 18:57:11 -0000 1.4
@@ -4,6 +4,8 @@
#include "sql-api.h"
struct sql_db {
+ const char *name;
+
struct sql_db *(*init)(const char *connect_string);
void (*deinit)(struct sql_db *db);
@@ -13,11 +15,23 @@
void (*exec)(struct sql_db *db, const char *query);
void (*query)(struct sql_db *db, const char *query,
sql_query_callback_t *callback, void *context);
+ struct sql_result *(*query_s)(struct sql_db *db, const char *query);
+
+ struct sql_transaction_context *(*transaction_begin)(struct sql_db *db);
+ void (*transaction_commit)(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback,
+ void *context);
+ int (*transaction_commit_s)(struct sql_transaction_context *ctx,
+ const char **error_r);
+ void (*transaction_rollback)(struct sql_transaction_context *ctx);
+
+ void (*update)(struct sql_transaction_context *ctx, const char *query);
};
struct sql_result {
struct sql_db *db;
+ void (*free)(struct sql_result *result);
int (*next_row)(struct sql_result *result);
unsigned int (*get_fields_count)(struct sql_result *result);
@@ -32,6 +46,12 @@
const char *const *(*get_values)(struct sql_result *result);
const char *(*get_error)(struct sql_result *result);
+
+ unsigned int callback:1;
+};
+
+struct sql_transaction_context {
+ struct sql_db *db;
};
extern struct sql_db driver_mysql_db;
Index: sql-api.c
===================================================================
RCS file: /var/lib/cvs/dovecot/src/lib-sql/sql-api.c,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- sql-api.c 9 Jun 2005 18:44:22 -0000 1.4
+++ sql-api.c 10 Dec 2005 18:57:11 -0000 1.5
@@ -3,17 +3,25 @@
#include "lib.h"
#include "sql-api-private.h"
-struct sql_db *sql_init(const char *db_driver,
- const char *connect_string __attr_unused__)
-{
+struct sql_db *sql_db_drivers[] = {
#ifdef HAVE_PGSQL
- if (strcmp(db_driver, "pgsql") == 0)
- return driver_pgsql_db.init(connect_string);
+ &driver_pgsql_db,
#endif
#ifdef HAVE_MYSQL
- if (strcmp(db_driver, "mysql") == 0)
- return driver_mysql_db.init(connect_string);
+ &driver_mysql_db,
#endif
+ NULL
+};
+
+struct sql_db *sql_init(const char *db_driver,
+ const char *connect_string __attr_unused__)
+{
+ int i;
+
+ for (i = 0; sql_db_drivers[i] != NULL; i++) {
+ if (strcmp(db_driver, sql_db_drivers[i]->name) == 0)
+ return sql_db_drivers[i]->init(connect_string);
+ }
i_fatal("Unknown database driver '%s'", db_driver);
}
@@ -44,6 +52,16 @@
db->query(db, query, callback, context);
}
+struct sql_result *sql_query_s(struct sql_db *db, const char *query)
+{
+ return db->query_s(db, query);
+}
+
+void sql_result_free(struct sql_result *result)
+{
+ result->free(result);
+}
+
int sql_result_next_row(struct sql_result *result)
{
return result->next_row(result);
@@ -87,6 +105,11 @@
return result->get_error(result);
}
+static void
+sql_result_not_connected_free(struct sql_result *result __attr_unused__)
+{
+}
+
static int
sql_result_not_connected_next_row(struct sql_result *result __attr_unused__)
{
@@ -99,10 +122,40 @@
return "Not connected to database";
}
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db)
+{
+ return db->transaction_begin(db);
+}
+
+void sql_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context)
+{
+ ctx->db->transaction_commit(ctx, callback, context);
+}
+
+int sql_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r)
+{
+ return ctx->db->transaction_commit_s(ctx, error_r);
+}
+
+void sql_transaction_rollback(struct sql_transaction_context *ctx)
+{
+ ctx->db->transaction_rollback(ctx);
+}
+
+void sql_update(struct sql_transaction_context *ctx, const char *query)
+{
+ ctx->db->update(ctx, query);
+}
+
struct sql_result sql_not_connected_result = {
NULL,
+ sql_result_not_connected_free,
sql_result_not_connected_next_row,
NULL, NULL, NULL, NULL, NULL, NULL,
- sql_result_not_connected_get_error
+ sql_result_not_connected_get_error,
+
+ FALSE
};
Index: sql-api.h
===================================================================
RCS file: /var/lib/cvs/dovecot/src/lib-sql/sql-api.h,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- sql-api.h 7 Aug 2005 11:41:25 -0000 1.4
+++ sql-api.h 10 Dec 2005 18:57:11 -0000 1.5
@@ -1,18 +1,21 @@
#ifndef __SQL_API_H
#define __SQL_API_H
+/* This SQL API is designed to work asynchronously. The underlying drivers
+ however may not. */
+
enum sql_db_flags {
/* Set if queries are not executed asynchronously */
SQL_DB_FLAG_BLOCKING = 0x01,
};
-/* This SQL API is designed to work asynchronously. The underlying drivers
- however may not. */
-
struct sql_db;
struct sql_result;
typedef void sql_query_callback_t(struct sql_result *result, void *context);
+typedef void sql_commit_callback_t(const char *error, void *context);
+
+extern struct sql_db *sql_db_drivers[];
/* Initialize database connections. db_driver is the database driver name,
eg. "mysql" or "pgsql". connect_string is driver-specific. */
@@ -32,11 +35,16 @@
/* Execute SQL query and return result in callback. */
void sql_query(struct sql_db *db, const char *query,
sql_query_callback_t *callback, void *context);
+/* Execute blocking SQL query and return result. */
+struct sql_result *sql_query_s(struct sql_db *db, const char *query);
/* Go to next row, returns 1 if ok, 0 if this was the last row or -1 if error
occurred. This needs to be the first call for result. */
int sql_result_next_row(struct sql_result *result);
+/* Needs to be called only with sql_query_s(). */
+void sql_result_free(struct sql_result *result);
+
/* Return number of fields in result. */
unsigned int sql_result_get_fields_count(struct sql_result *result);
/* Return name of the given field index. */
@@ -56,4 +64,18 @@
/* Return last error message in result. */
const char *sql_result_get_error(struct sql_result *result);
+/* Begin a new transaction. Currently you're limited to only one open
+ transaction at a time. */
+struct sql_transaction_context *sql_transaction_begin(struct sql_db *db);
+/* Commit transaction. */
+void sql_transaction_commit(struct sql_transaction_context *ctx,
+ sql_commit_callback_t *callback, void *context);
+/* Synchronous commit. Returns 0 if ok, -1 if error. */
+int sql_transaction_commit_s(struct sql_transaction_context *ctx,
+ const char **error_r);
+void sql_transaction_rollback(struct sql_transaction_context *ctx);
+
+/* Execute query in given transaction. */
+void sql_update(struct sql_transaction_context *ctx, const char *query);
+
#endif
More information about the dovecot-cvs
mailing list