diff --git a/build.sh b/build.sh
index 2db33c11e30..6259bbaa043 100755
--- a/build.sh
+++ b/build.sh
@@ -10,6 +10,7 @@ mkdir -p $BUILD
# Core API
gcc -Iinclude -c src/api/api.c -o $BUILD/api.o
+gcc -Iinclude -c src/api/config.c -o $BUILD/config.o
gcc -Iinclude -c src/api/cur_std.c -o $BUILD/cur_std.o
gcc -Iinclude -c src/api/pack.c -o $BUILD/pack.o
g++ -Iinclude -I$DB_HOME -c src/bdb/bdb.cpp -o $BUILD/bdb.o
diff --git a/docs/src/config-strings.dox b/docs/src/config-strings.dox
index bfe6e364c2f..950585c2396 100644
--- a/docs/src/config-strings.dox
+++ b/docs/src/config-strings.dox
@@ -24,6 +24,32 @@ Superfluous commas and whitespace in the configuration string are ignored (inclu
Keys are processed in order from left to right, with later settings overriding earlier ones unless multiple settings for a key are permitted.
+@section config_json JSON compatibility
+
+The parser for configuration strings will accept additional formatting as follows:
+
+- parentheses may be round or square brackets or curly braces: '()', '[]' or '{}'
+- the whole configuration string may optionally be wrapped in parentheses
+- the key/value separator can be a colon: ':'
+- keys and values may be in double quotes: '"'
+- quoted strings are interpreted as UTF-8 values
+
+The result of this relaxed parsing is that applications may pass strings representing valid JSON objects wherever configuration strings are required.
+
+For example, in Python, code might look as follows:
+
+\code
+ import json
+ config = json.dumps({
+ "key_format" : "r",
+ "value_format" : "5sHQ",
+ "columns" : ("id", "country", "year", "population"),
+ "colgroup.population" : ["population"],
+ "index.country_year" : ["country", "year"]
+ })
+\endcode
+
+
@section config_examples Code Samples
The code below is taken from the complete example program @ex_ref{ex_config.c}.
@@ -39,7 +65,8 @@ Open a connection to a database, creating it if it does not exist and set a cach
Create a table that uses C language strings for keys and values:
-@skipline create_table
+@skip create_table
+@until ;
Assign a priority to a transaction and give it a name for debugging:
diff --git a/docs/src/cursors.dox b/docs/src/cursors.dox
index 00dfb2f6453..a0d57dd6e59 100644
--- a/docs/src/cursors.dox
+++ b/docs/src/cursors.dox
@@ -34,7 +34,7 @@ The following are builtin cursor types:
| URI | Function |
| table:[\] | ordinary table cursor |
-| columnset:[\.\] | column set cursor |
+| colgroup:[\.\] | column group cursor |
| index:[\.\] | index cursor |
| join:\\&\[&\...] | Join the contents of multiple cursors together. |
| module: | loadable modules (key=(string)name, data=(string)path) |
@@ -46,9 +46,9 @@ The following are builtin cursor types:
@section cursor_projections Projections
-Cursors on tables, column sets and indices can return a subset of columns. This is done by listing the column names in parantheses in the uri parameter to WT_SESSION::open_cursor. Only the fields from the listed columns are returned by WT_CURSOR::get_key and WT_CURSOR::get_value.
+Cursors on tables, column groups and indices can return a subset of columns. This is done by listing the column names in parantheses in the uri parameter to WT_SESSION::open_cursor. Only the fields from the listed columns are returned by WT_CURSOR::get_key and WT_CURSOR::get_value.
-This is particularly useful with index cursors, because if all columns in the projection are available in the index (including primary key columns, which are the values of the index), there is no need to access any column set.
+This is particularly useful with index cursors, because if all columns in the projection are available in the index (including primary key columns, which are the values of the index), there is no need to access any column groups.
@section cursor_ranges Restricting the Range of a Scan
diff --git a/docs/src/schema.dox b/docs/src/schema.dox
index 9c2050a00dd..a121d1b6c9a 100644
--- a/docs/src/schema.dox
+++ b/docs/src/schema.dox
@@ -40,9 +40,9 @@ Applications describe the format of their data by supplying a schema to WT_SESSI
@todo describe how to add columns to a schema
-@section schema_column_sets Storing Sets of Columns Together
+@section schema_column_groups Storing Groups of Columns Together
-@todo define and describe column sets
+@todo define and describe column groups
@section schema_indices Adding an Index
diff --git a/examples/c/ex_call_center.c b/examples/c/ex_call_center.c
index 677ca69a93e..4bfcc558759 100644
--- a/examples/c/ex_call_center.c
+++ b/examples/c/ex_call_center.c
@@ -68,14 +68,14 @@ int main()
"key_format=r,"
"value_format=SSS,"
"columns=(id,name,address,phone),"
- "column_set=cust_address(address),"
- "index=cust_phone(phone)");
+ "colgroup.cust_address=(address),"
+ "index.cust_phone=(phone)");
ret = session->create_table(session, "calls",
"key_format=r,"
"value_format=qrrSS,"
"columns=(id,call_date,cust_id,emp_id,call_type,notes),"
- "index=call_cust_date(cust_id,call_date)");
+ "index.calls_cust_date=(cust_id,call_date)");
/* Omitted: populate the tables with some data. */
@@ -92,7 +92,8 @@ int main()
* Specify the columns we want: the customer ID and the name. This
* means the cursor's value format will be "rS".
*/
- ret = session->open_cursor(session, "index:cust_phone(id,name)",
+ ret = session->open_cursor(session,
+ "index:cust_phone(id,name)",
NULL, NULL, &cursor);
cursor->set_key(cursor, "212-555-1000");
ret = cursor->search(cursor);
@@ -114,7 +115,7 @@ int main()
* getting 3 records.
*/
ret = session->open_cursor(session,
- "index:call_cust_date(cust_id,call_type,notes)",
+ "index:calls_cust_date(cust_id,call_type,notes)",
NULL, NULL, &cursor);
/*
diff --git a/examples/c/ex_schema.c b/examples/c/ex_schema.c
index 57686c87d61..7b7bdc52a97 100644
--- a/examples/c/ex_schema.c
+++ b/examples/c/ex_schema.c
@@ -62,8 +62,8 @@ int main()
"key_format=r,"
"value_format=5sHQ,"
"columns=(id,country,year,population),"
- "column_set=population(population),"
- "index=country_year(country,year)");
+ "colgroup.population=(population),"
+ "index.country_year=(country,year)");
ret = session->open_cursor(session, "table:population",
NULL, NULL, &cursor);
diff --git a/include/config.h b/include/config.h
new file mode 100644
index 00000000000..ccd974f496c
--- /dev/null
+++ b/include/config.h
@@ -0,0 +1,25 @@
+/* Copyright (c) 2010 WiredTiger, Inc. All rights reserved. */
+
+typedef struct WT_CONFIG WT_CONFIG;
+typedef struct WT_CONFIG_ITEM WT_CONFIG_ITEM;
+
+struct WT_CONFIG
+{
+ const char *orig;
+ const char *end;
+ const char *cur;
+
+ int depth, top;
+ void **go;
+};
+
+struct WT_CONFIG_ITEM
+{
+ const char *str;
+ size_t len;
+ uint64_t val;
+ enum { ITEM_STRING, ITEM_ID, ITEM_NUM, ITEM_STRUCT } type;
+};
+
+int config_init(WT_CONFIG *conf, const char *confstr, int len);
+int config_next(WT_CONFIG *conf, WT_CONFIG_ITEM *key, WT_CONFIG_ITEM *value);
diff --git a/include/wiredtiger.h b/include/wiredtiger.h
index 237b8b53b9c..a982496900d 100644
--- a/include/wiredtiger.h
+++ b/include/wiredtiger.h
@@ -13,6 +13,7 @@
*/
#include
+#include
#include
#include
@@ -369,22 +370,16 @@ struct WT_SESSION {
* Comma-separated list of the form (column[\,...]).
* The number of entries must match the total number of values in
* #key_format and #value_format.}
- * @config{column_set,,Named set of columns to store together.
- * Name and comma-separated list of the form
- * name(column[\,...]). Multiple column sets can
- * be specified by repeating the \c "column_set" key in the
- * configuration string. Each column set is stored separately\,
- * keyed by the primary key of the table. Any column that does
- * not appear in a column set is stored in an unnamed default
- * column set for the table.}
+ * @config{colgroup.\,,Named group of columns to store together.
+ * Comma-separated list of the form (column[\,...]).
+ * Each column group is stored separately\, keyed by the primary
+ * key of the table. Any column that does not appear in a column
+ * group is stored in a default unnamed column group for the table.}
* @config{exclusive,,Fail if the table exists (if "no"\, the
* default\, verifies that the table exists and has the specified
* schema.}
- * @config{index,,Named index on a set of columns. Name and
- * comma-separated list of the form
- * name(column[\,...]). Multiple indices can be
- * specified by repeating the \c "index" key in the configuration
- * string.}
+ * @config{index.\,,Named index on a list of columns.
+ * Comma-separated list of the form (column[\,...]).}
* @config{key_format,,The format of the data packed into key items.
* See ::wiredtiger_struct_pack for details. If not set\, a
* default value of \c "u" is assumed\, and applications use the
@@ -627,7 +622,7 @@ struct WT_CONNECTION {
int __F(add_collator)(WT_CONNECTION *connection,
const char *name, WT_COLLATOR *collator, const char *config);
- /*! Add a custom extractor for index keys or column sets.
+ /*! Add a custom extractor for index keys or column groups.
*
* @dontinclude ex_all.c
*
@@ -743,6 +738,20 @@ int wiredtiger_open(const char *home,
*/
const char *wiredtiger_strerror(int err);
+/*!
+ * The interface implemented by applications in order to handle errors.
+ */
+struct WT_ERROR_HANDLER {
+ /*! Callback to handle errors within the session. */
+ int (*handle_error)(WT_ERROR_HANDLER *handler,
+ int err, const char *errmsg);
+
+ /*! Optional callback to retrieve buffered messages. */
+ int (*get_messages)(WT_ERROR_HANDLER *handler, const char **errmsgp);
+
+ /*! Optional callback to clear buffered messages. */
+ int (*clear_messages)(WT_ERROR_HANDLER *handler);
+};
/*! Pack a structure into a buffer.
*
* Uses format strings mostly as specified in the Python struct module:
diff --git a/include/wiredtiger_ext.h b/include/wiredtiger_ext.h
index 9941b90dbbb..a3929875b42 100644
--- a/include/wiredtiger_ext.h
+++ b/include/wiredtiger_ext.h
@@ -60,28 +60,13 @@ struct WT_CURSOR_FACTORY {
const char *config, WT_CURSOR *new_cursor);
};
-/*!
- * The interface implemented by applications in order to handle errors.
- */
-struct WT_ERROR_HANDLER {
- /*! Callback to handle errors within the session. */
- int (*handle_error)(WT_ERROR_HANDLER *handler,
- int err, const char *errmsg);
-
- /*! Optional callback to retrieve buffered messages. */
- int (*get_messages)(WT_ERROR_HANDLER *handler, const char **errmsgp);
-
- /*! Optional callback to clear buffered messages. */
- int (*clear_messages)(WT_ERROR_HANDLER *handler);
-};
-
/*!
* The interface implemented by applications to provide custom extraction of
- * index keys or column set values. Applications register their implementation
+ * index keys or column group values. Applications register their implementation
* with WiredTiger by calling WT_CONNECTION::add_extractor.
*/
struct WT_EXTRACTOR {
- /*! Callback to extract a value for an index or column set.
+ /*! Callback to extract a value for an index or column group.
*
* @errors
*/
diff --git a/include/wt_int.h b/include/wt_int.h
index 66b4e050a84..76ccce6d912 100644
--- a/include/wt_int.h
+++ b/include/wt_int.h
@@ -2,6 +2,9 @@
#include "wiredtiger.h"
+#include
+
+#include "config.h"
#include "cur_std.h"
#include "extern.h"
diff --git a/lang/python/src/wiredtiger/impl/__init__.py b/lang/python/src/wiredtiger/impl/__init__.py
index cf6e71fb2ac..cb213eef5a1 100644
--- a/lang/python/src/wiredtiger/impl/__init__.py
+++ b/lang/python/src/wiredtiger/impl/__init__.py
@@ -14,23 +14,23 @@ from wiredtiger.util import parse_config
from bsddb3 import db
class Table:
- def __init__(self, db, name, key_format='u', value_format='u', columns=(,), column_sets=(,), indices=(,)):
+ def __init__(self, db, name, key_format='u', value_format='u', columns=(,), colgroups=(,), indices=(,)):
self.db = db
self.name = name
self.key_format = key_format
self.value_format = value_format
self.columns = columns
- self.column_sets = column_sets
+ self.colgroups = colgroups
self.indices = indices
def close(self):
self.db.close(db.DB_NOSYNC)
- def check_schema(self, key_format='u', value_format='u', columns=(,), column_sets=(,), indices=(,)):
+ def check_schema(self, key_format='u', value_format='u', columns=(,), colgroups=(,), indices=(,)):
if (self.key_format != key_format or
self.value_format != value_format or
self.columns != columns or
- self.column_sets != column_sets or
+ self.colgroups != colgroups or
self.indices != indices):
raise 'Schemas don\'t match for table "' + self.name + '"'
@@ -126,10 +126,10 @@ class Session:
for k, v in parse_config(config):
if k in ('key_format', 'value_format', 'columns'):
schema[k] = v
- elif k == 'column_set':
- schema['column_sets'] = schema.get('column_sets', (,)) + (v,)
- elif k == 'index':
- schema['indices'] = schema.get('indices', (,)) + (v,)
+ elif k.startswith('colgroup'):
+ schema['colgroup'] = schema.get('colgroup', (,)) + (k[len('colgroup')+1:], v)
+ elif k.startswith('index'):
+ schema['indices'] = schema.get('indices', (,)) + (k[len('index')+1:], v)
else:
raise 'Unknown configuration "' + k + '"'
if name in self.tables:
@@ -185,7 +185,7 @@ class Connection:
# The schema of the schema table.
self.schematab = Table(schemadb, key_format='S', value_format='SSSSS',
- columns=('name', 'key_format', 'value_format', 'column_sets', 'indices'))
+ columns=('name', 'key_format', 'value_format', 'colgroups', 'indices'))
def close(self, config=''):
# Work on a copy of the list because Session.close removes itself
diff --git a/src/api/config.c b/src/api/config.c
new file mode 100644
index 00000000000..162f470a97a
--- /dev/null
+++ b/src/api/config.c
@@ -0,0 +1,232 @@
+/* Copyright (c) 2011 WiredTiger, Inc. All rights reserved. */
+
+// Some code from js0n by jeremie miller - 2010
+// public domain, https://github.com/quartzjer/js0n
+//
+// XXX This code relies on two GCC extensions.
+//
+// The first allows labels to be treated as values. This is handy for
+// state machines but will require a rewrite with a big switch statement
+// for portability.
+//
+// The second is for range array initialization.
+// The syntax for subscription a single element is C99:
+//
+// To specify an array index, write [index] = before the element value.
+// For example,
+//
+// int a[6] = { [4] = 29, [2] = 15 };
+// ...
+// To initialize a range of elements to the same value, write
+// [first ... last] = value. This is a GNU extension. For example,
+//
+// int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
+//
+//
+// Both are relatively easy to transform into portable C99, but a pain until // the interface is stable.
+
+#include
+
+int
+config_init(WT_CONFIG *conf, const char *confstr, int len)
+{
+ conf->orig = conf->cur = confstr;
+ conf->end = confstr + len;
+ conf->depth = 0;
+ conf->top = -1;
+ conf->go = NULL;
+
+ return (0);
+}
+
+#define PUSH(i, t) do { \
+ if(conf->top == -1) \
+ conf->top = conf->depth; \
+ if(conf->depth == conf->top) { \
+ out->type = t; \
+ out->str = (conf->cur + i); \
+ } \
+} while (0)
+
+#define CAP(i) do { \
+ if(conf->depth == conf->top) \
+ out->len = (conf->cur + i + 1) - out->str; \
+} while (0)
+
+int
+config_next(WT_CONFIG *conf, WT_CONFIG_ITEM *key, WT_CONFIG_ITEM *value)
+{
+ WT_CONFIG_ITEM *out = key;
+ int utf8_remain = 0;
+ static void *gostruct[] =
+ {
+ [0 ... 255] = &&l_bad,
+ ['\t'] = &&l_loop, [' '] = &&l_loop,
+ ['\r'] = &&l_loop, ['\n'] = &&l_loop,
+ ['"'] = &&l_qup,
+ [':'] = &&l_value, ['='] = &&l_value,
+ [','] = &&l_next,
+ // tracking [] and {} individually would allow fuller
+ // validation but is really messy
+ ['('] = &&l_up, [')'] = &&l_down,
+ ['['] = &&l_up, [']'] = &&l_down,
+ ['{'] = &&l_up, ['}'] = &&l_down,
+ // bare identifiers
+ ['-'] = &&l_numbare,
+ ['0' ... '9'] = &&l_numbare,
+ ['_'] = &&l_bare,
+ ['A' ... 'Z'] = &&l_bare, ['a' ... 'z'] = &&l_bare,
+ };
+ static void *gobare[] =
+ {
+ [0 ... 31] = &&l_bad,
+ // could be more pedantic/validation-checking
+ [32 ... 126] = &&l_loop,
+ ['\t'] = &&l_unbare, [' '] = &&l_unbare,
+ ['\r'] = &&l_unbare, ['\n'] = &&l_unbare,
+ [':'] = &&l_unbare, ['='] = &&l_unbare,
+ [','] = &&l_unbare,
+ [')'] = &&l_unbare, [']'] = &&l_unbare, ['}'] = &&l_unbare,
+ [127 ... 255] = &&l_bad
+ };
+ static void *gostring[] =
+ {
+ [0 ... 31] = &&l_bad, [127] = &&l_bad,
+ [32 ... 126] = &&l_loop,
+ ['\\'] = &&l_esc, ['"'] = &&l_qdown,
+ [128 ... 191] = &&l_bad,
+ [192 ... 223] = &&l_utf8_2,
+ [224 ... 239] = &&l_utf8_3,
+ [240 ... 247] = &&l_utf8_4,
+ [248 ... 255] = &&l_bad
+ };
+ static void *goutf8_continue[] =
+ {
+ [0 ... 127] = &&l_bad,
+ [128 ... 191] = &&l_utf_continue,
+ [192 ... 255] = &&l_bad
+ };
+ static void *goesc[] =
+ {
+ [0 ... 255] = &&l_bad,
+ ['"'] = &&l_unesc, ['\\'] = &&l_unesc,
+ ['/'] = &&l_unesc, ['b'] = &&l_unesc,
+ ['f'] = &&l_unesc, ['n'] = &&l_unesc,
+ ['r'] = &&l_unesc, ['t'] = &&l_unesc, ['u'] = &&l_unesc
+ };
+ static WT_CONFIG_ITEM default_value = {
+ "1", 1, 1, ITEM_NUM
+ };
+
+ key->len = 0;
+
+ if (conf->go == NULL)
+ conf->go = gostruct;
+
+ for(; conf->cur < conf->end; conf->cur++)
+ {
+ goto *conf->go[*conf->cur];
+l_loop: ;
+ }
+
+ // Might have a trailing key/value without a closing brace
+ if (conf->go == gobare) {
+ CAP(0);
+ conf->go = gostruct;
+ }
+
+ if (conf->depth <= conf->top && key->len > 0) {
+ if (out == key)
+ *value = default_value;
+ return (0);
+ }
+
+ // 0 if successful full parse, >0 for incomplete data
+ return ((conf->depth == 0) ? WT_NOTFOUND : EINVAL);
+
+l_bad:
+ return (EINVAL);
+
+l_down:
+ --conf->depth;
+ CAP(0);
+ goto l_loop;
+
+l_up:
+ if(conf->top == -1)
+ conf->top = 1;
+ PUSH(0, ITEM_STRUCT);
+ ++conf->depth;
+ goto l_loop;
+
+l_value:
+ if (conf->depth == conf->top) {
+ if (out == value)
+ goto l_bad;
+ out = value;
+ }
+ goto l_loop;
+
+l_next:
+ if (conf->depth == conf->top && key->len > 0) {
+ // Handle the case with no value
+ if (out == key)
+ *value = default_value;
+ ++conf->cur;
+ return (0);
+ } else
+ goto l_loop;
+
+l_qdown:
+ CAP(-1);
+ conf->go = gostruct;
+ goto l_loop;
+
+l_qup:
+ PUSH(1, ITEM_STRING);
+ conf->go = gostring;
+ goto l_loop;
+
+l_esc:
+ conf->go = goesc;
+ goto l_loop;
+
+l_unesc:
+ conf->go = gostring;
+ goto l_loop;
+
+l_bare:
+ PUSH(0, ITEM_ID);
+ conf->go = gobare;
+ goto l_loop;
+
+l_numbare:
+ PUSH(0, ITEM_NUM);
+ conf->go = gobare;
+ goto l_loop;
+
+l_unbare:
+ CAP(-1);
+ conf->go = gostruct;
+ goto *conf->go[*conf->cur];
+
+l_utf8_2:
+ conf->go = goutf8_continue;
+ utf8_remain = 1;
+ goto l_loop;
+
+l_utf8_3:
+ conf->go = goutf8_continue;
+ utf8_remain = 2;
+ goto l_loop;
+
+l_utf8_4:
+ conf->go = goutf8_continue;
+ utf8_remain = 3;
+ goto l_loop;
+
+l_utf_continue:
+ if (!--utf8_remain)
+ conf->go = gostring;
+ goto l_loop;
+}
diff --git a/test/config.i b/test/config.i
new file mode 100644
index 00000000000..2cb8794f340
--- /dev/null
+++ b/test/config.i
@@ -0,0 +1,37 @@
+%module config
+
+%{
+#include
+%}
+
+typedef struct WT_CONFIG WT_CONFIG;
+typedef struct WT_CONFIG_ITEM WT_CONFIG_ITEM;
+
+struct WT_CONFIG
+{
+ char *orig;
+ char *end;
+ char *cur;
+
+ int depth, top;
+ void **go;
+};
+
+struct WT_CONFIG_ITEM
+{
+ char *str;
+ size_t len;
+ uint64_t val;
+ enum { ITEM_STRING, ITEM_ID, ITEM_NUM, ITEM_STRUCT } type;
+};
+
+/*
+ * XXX: leak, but the string needs to last beyond the call to config_init.
+ * It should be freed when the WT_CONFIG object is deleted.
+ */
+%typemap(in) char *confstr {
+ $1 = strdup(PyString_AsString($input));
+}
+
+int config_init(WT_CONFIG *conf, char *confstr, int len);
+int config_next(WT_CONFIG *conf, WT_CONFIG_ITEM *key, WT_CONFIG_ITEM *value);
diff --git a/test/config_test.c b/test/config_test.c
new file mode 100644
index 00000000000..13702342153
--- /dev/null
+++ b/test/config_test.c
@@ -0,0 +1,21 @@
+#include
+
+#include
+
+int main(int argc, char *argv[])
+{
+ int ret;
+ WT_CONFIG c;
+ WT_CONFIG_ITEM k, v;
+ const char *cstr = "create,cachesize=10MB";
+
+ config_init(&c, cstr, strlen(cstr));
+ while ((ret = config_next(&c, &k, &v)) == 0) {
+ printf("Got key '%.*s', value '%.*s'\n",
+ (int)k.len, k.str, (int)v.len, v.str);
+ }
+
+ printf("Last call to config_next failed with %d\n", ret);
+
+ return (0);
+}
diff --git a/test/config_test.py b/test/config_test.py
new file mode 100644
index 00000000000..c1998ee8ab6
--- /dev/null
+++ b/test/config_test.py
@@ -0,0 +1,39 @@
+import config
+
+def parse(s):
+ print "Parsing '%s':" % s
+
+ c = config.WT_CONFIG()
+ k = config.WT_CONFIG_ITEM()
+ v = config.WT_CONFIG_ITEM()
+
+ config.config_init(c, s, len(s))
+ ret = config.config_next(c, k, v)
+ while ret == 0:
+ print " => '%s' = '%s'" % (k.str[:k.len], v.str[:v.len])
+ ret = config.config_next(c, k, v)
+
+ # XXX hard-coding WT_NOTFOUND until we fix this
+ if ret != -10001:
+ print "Last call to config_next failed with %d" % ret
+
+if __name__ == '__main__':
+ parse("create")
+ parse("create,cachesize=10MB")
+ parse('create,cachesize=10MB,path="/foo/bar"')
+ parse('columns=(first,second, third)')
+ parse('key_format="S", value_format="5sq", columns=(first,second, third)')
+ parse('key_columns=(first=S),value_columns=(second="5s", third=q)')
+ parse(',,columns=(first=S,second="5s", third=q),,')
+ parse('index.country_year=(country,year),key_format=r,colgroup.population=(population),columns=(id,country,year,population),value_format=5sHQ')
+
+ import json
+ parse(json.dumps({'hello' : 'world', 'columns' : ('one', 'two', 'three')}))
+
+ parse(json.dumps({
+ "key_format" : "r",
+ "value_format" : "5sHQ",
+ "columns" : ("id", "country", "year", "population"),
+ "colgroup.population" : ("population",),
+ "index.country_year" : ("country","year")
+ }))