diff --git a/README.md b/README.md index dd41c97..a4389c6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ The virtual table requires 4 SQL constraints to be set for all queries: - `tocolumn`: The node id column where an edge goes to (must be integer). - `root`: The root node id of the breadth-first traversal. +As well as an optional constraint to determine the order of node neighbour traversal. +- `order_by_column`: The name of the column used to determine the order of node neighbour traversal. Adjacent edges are traversed in descending order. + The virtual table also provides the following columns that can be returned or used as contraints: - `id`: The id of the current node being visited. - `distance`: The shortest distance to the current node from the root node. diff --git a/bfsvtab.c b/bfsvtab.c index 88aed33..b9c9dfe 100644 --- a/bfsvtab.c +++ b/bfsvtab.c @@ -10,8 +10,9 @@ ** ************************************************************************* ** -** This file code for a virtual table that performs a breadth-first search of -** any graph represented in a real table. The virtual table is called "bfs". +** This file contains code for a virtual table that performs a breadth-first +** search of any graph represented in a real or virtual table. +** The virtual table is called "bfsvtab". ** ** A bfsvtab virtual table is created liske this: ** @@ -21,7 +22,7 @@ ** tocolumn=, ** ) ** -** This bfs is minimal, in the sense that it uses only the required +** This bfsvtab extension is minimal, in the sense that it uses only the required ** methods on the sqlite3_module object. As a result, bfsvtab is ** a read-only and eponymous-only table. Those limitation can be removed ** by adding new methods. @@ -390,6 +391,8 @@ struct bfsvtab_vtab { char *zTableName; char *zFromColumn; char *zToColumn; + char *zOrderByColumn; + sqlite3 *db; }; @@ -409,6 +412,7 @@ struct bfsvtab_cursor { char *zTableName; /* Name of table holding edge relation */ char *zFromColumn; /* Name of from column of zTableName */ char *zToColumn; /* Name of to column of zTableName */ + char *zOrderByColumn; /* Name of column to order neighbours by during BFS traversal */ bfsvtab_avl *pVisited; /* Set of Visited Nodes */ @@ -428,6 +432,7 @@ static void bfsvtabFree(bfsvtab_vtab *p) { sqlite3_free(p->zTableName); sqlite3_free(p->zFromColumn); sqlite3_free(p->zToColumn); + sqlite3_free(p->zOrderByColumn); memset(p, 0, sizeof(*p)); sqlite3_free(p); } @@ -512,6 +517,17 @@ static int bfsvtabConnect( } continue; } + zVal = bfsvtabValueOfKey("order_by_column", argv[i]); + if (zVal) { + sqlite3_free(pNew->zOrderByColumn); + pNew->zOrderByColumn = bfsvtabDequote(zVal); + if (pNew->zOrderByColumn == 0) { + rc = SQLITE_NOMEM; + goto connectError; + } + continue; + } + *pzErr = sqlite3_mprintf("unrecognized argument: [%s]\n", argv[i]); bfsvtabFree(pNew); return SQLITE_ERROR; @@ -520,7 +536,7 @@ static int bfsvtabConnect( rc = sqlite3_declare_vtab(db, "CREATE TABLE x(id,parent,distance,shortest_path,root HIDDEN," "tablename HIDDEN,fromcolumn HIDDEN," - "tocolumn HIDDEN)" + "tocolumn HIDDEN, order_by_column HIDDEN)" ); /* For convenience, define symbolic names for the index to each column. */ #define BFSVTAB_COL_ID 0 @@ -531,6 +547,7 @@ static int bfsvtabConnect( #define BFSVTAB_COL_TABLENAME 5 #define BFSVTAB_COL_FROMCOLUMN 6 #define BFSVTAB_COL_TOCOLUMN 7 +#define BFSVTAB_COL_ORDER_BY_COLUMN 8 if (rc != SQLITE_OK) { bfsvtabFree(pNew); } @@ -575,12 +592,14 @@ static void bfsvtabClearCursor(bfsvtab_cursor *pCur) { sqlite3_free(pCur->zTableName); sqlite3_free(pCur->zFromColumn); sqlite3_free(pCur->zToColumn); + sqlite3_free(pCur->zOrderByColumn); sqlite3_finalize(pCur->pStmt); pCur->zTableName = 0; pCur->zFromColumn = 0; pCur->zToColumn = 0; + pCur->zOrderByColumn = 0; pCur->pCurrent = 0; pCur->pVisited = 0; } @@ -731,13 +750,19 @@ static int bfsvtabColumn( pCur->zFromColumn : pCur->pVtab->zFromColumn, -1, SQLITE_TRANSIENT); break; - default: - assert( i==BFSVTAB_COL_TOCOLUMN ); + case BFSVTAB_COL_TOCOLUMN: sqlite3_result_text(ctx, pCur->zToColumn ? pCur->zToColumn : pCur->pVtab->zToColumn, -1, SQLITE_TRANSIENT); break; + default: + assert( i==BFSVTAB_COL_ORDER_BY_COLUMN ); + sqlite3_result_text(ctx, + pCur->zOrderByColumn ? + pCur->zOrderByColumn : pCur->pVtab->zOrderByColumn, + -1, SQLITE_TRANSIENT); + break; } return SQLITE_OK; } @@ -779,6 +804,7 @@ static int bfsvtabFilter( const char *zTableName = pVtab->zTableName; const char *zFromColumn = pVtab->zFromColumn; const char *zToColumn = pVtab->zToColumn; + const char *zOrderByColumn = pVtab->zOrderByColumn; bfsvtab_node *root; bfsvtab_avl *rootAvlNode; @@ -801,10 +827,20 @@ static int bfsvtabFilter( zToColumn = (const char*)sqlite3_value_text(argv[(idxNum>>16)&0x0f]); pCur->zToColumn = sqlite3_mprintf("%s", zToColumn); } + if((idxNum & 0xf00000) != 0) { + zOrderByColumn = (const char*)sqlite3_value_text(argv[(idxNum>>20)&0x0f]); + pCur->zOrderByColumn = sqlite3_mprintf("%s", zOrderByColumn); + } - zSql = sqlite3_mprintf( - "SELECT \"%w\".\"%w\" FROM \"%w\" WHERE \"%w\".\"%w\"=?1", - zTableName, zToColumn, zTableName, zTableName, zFromColumn); + if (zOrderByColumn) { + zSql = sqlite3_mprintf( + "SELECT \"%w\".\"%w\" FROM \"%w\" WHERE \"%w\".\"%w\"=?1 ORDER BY \"%w\".\"%w\"", + zTableName, zToColumn, zTableName, zTableName, zFromColumn, zTableName, zOrderByColumn); + } else { + zSql = sqlite3_mprintf( + "SELECT \"%w\".\"%w\" FROM \"%w\" WHERE \"%w\".\"%w\"=?1", + zTableName, zToColumn, zTableName, zTableName, zFromColumn); + } if (zSql == 0) { return SQLITE_NOMEM; } @@ -937,6 +973,14 @@ static int bfsvtabBestIndex( pIdxInfo->aConstraintUsage[i].argvIndex = ++idx; pIdxInfo->aConstraintUsage[i].omit = 1; } + if ((iPlan & 0xf00000) == 0 + && pConstraint->iColumn == BFSVTAB_COL_ORDER_BY_COLUMN + && pConstraint->op == SQLITE_INDEX_CONSTRAINT_EQ + ) { + iPlan |= idx<<20; + pIdxInfo->aConstraintUsage[i].argvIndex = ++idx; + pIdxInfo->aConstraintUsage[i].omit = 1; + } } if ((pVtab->zTableName == 0 && (iPlan & 0x000f00) == 0) || (pVtab->zFromColumn == 0 && (iPlan & 0x00f000) == 0) diff --git a/test.sh b/test.sh index 0255684..321411f 100755 --- a/test.sh +++ b/test.sh @@ -1,5 +1,10 @@ #!/bin/bash -valgrind --leak-check=full --show-leak-kinds=all --keep-debuginfo=yes -s sqlite3 < test/bfsvtab.sql +set -e + +echo "===== Testing sqlite-bfsvtab-ext ======" + +ls test/test_*.sh | xargs -I - bash - + +echo "========= Testing Completed ===========" -cmp <(sqlite3 < test/rcte.sql) <(sqlite3 < test/bfsvtab.sql) diff --git a/test/test_basic.sh b/test/test_basic.sh new file mode 100755 index 0000000..295f773 --- /dev/null +++ b/test/test_basic.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +FILENAME="$(basename $0)" + +echo "$FILENAME" + +cmp <(sqlite3 < $DIR/test_basic_rcte.sql) <(sqlite3 < $DIR/test_basic_bfsvtab.sql) diff --git a/test/bfsvtab.sql b/test/test_basic_bfsvtab.sql similarity index 84% rename from test/bfsvtab.sql rename to test/test_basic_bfsvtab.sql index 375061d..0958aa3 100644 --- a/test/bfsvtab.sql +++ b/test/test_basic_bfsvtab.sql @@ -1,5 +1,5 @@ .load ./bfsvtab -.read ./test/fixture.sql +.read ./test/test_basic_fixture.sql create virtual table bfs using bfsvtab( tablename='edges', fromcolumn='fromNode', diff --git a/test/fixture.sql b/test/test_basic_fixture.sql similarity index 100% rename from test/fixture.sql rename to test/test_basic_fixture.sql diff --git a/test/rcte.sql b/test/test_basic_rcte.sql similarity index 91% rename from test/rcte.sql rename to test/test_basic_rcte.sql index d3c591c..0e94c57 100644 --- a/test/rcte.sql +++ b/test/test_basic_rcte.sql @@ -1,4 +1,4 @@ -.read ./test/fixture.sql +.read ./test/test_basic_fixture.sql with recursive bfs(id, parent, shortest_path, distance) as ( select 1, null, '/' || 1 || '/', 0 diff --git a/test/test_memory_leaks.sh b/test/test_memory_leaks.sh new file mode 100755 index 0000000..1c2972c --- /dev/null +++ b/test/test_memory_leaks.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +FILENAME="$(basename $0)" + +echo "$FILENAME" + +valgrind --quiet --child-silent-after-fork=yes --leak-check=full --show-leak-kinds=all --keep-debuginfo=yes sqlite3 < $DIR/test_memory_leaks.sql > /dev/null diff --git a/test/test_memory_leaks.sql b/test/test_memory_leaks.sql new file mode 100644 index 0000000..b843ad5 --- /dev/null +++ b/test/test_memory_leaks.sql @@ -0,0 +1,21 @@ +.load ./bfsvtab +create table edges(fromNode integer, toNode integer, primary key(fromNode, toNode)); +insert into edges(fromNode, toNode) values + (1, 2), + (1, 3), + (2, 4), + (3, 4), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + (7, 8), + (7, 9), + (8, 10), + (9, 10); +create virtual table bfs using bfsvtab( + tablename='edges', + fromcolumn='fromNode', + tocolumn='toNode', +); +select id, parent, shortest_path, distance from bfs where root = 1; diff --git a/test/test_ordered_traversal.sh b/test/test_ordered_traversal.sh new file mode 100755 index 0000000..9c91832 --- /dev/null +++ b/test/test_ordered_traversal.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +FILENAME="$(basename $0)" + +echo "$FILENAME" + +GOT="$(sqlite3 < root + and order_by_column = 'key'; +EOF +)" +EXPECTED="3 2 1" + +cmp <(echo $GOT) <(echo $EXPECTED)