+#define SIZE(x) (sizeof(x)/sizeof(void*))
+
+static char *tuple2string(relation *r,tuple *t) {
+ #define LIMIT 10000
+ static char tmp[10][ LIMIT ];
+ static int i = 0;
+ if ( t == 0 ) {
+ return "(null)";
+ }
+ if ( i >= 10 ) {
+ i = 0;
+ }
+ int n = r->content.type->tostring( r->content.type, t, tmp[i], LIMIT );
+ if ( n == 0 ) {
+ *(tmp[i]) = 0;
+ }
+ return tmp[i++];
+}
+
+static void query_report(relation *r,tuple *query) {
+ vector_index i;
+ for ( i = 0; i < r->content.table.size; i++ ) {
+ tuple *t = hashvector_next( &r->content, &i, query );
+ fprintf( stderr, "check %s\n", tuple2string( r, t ) );
+ }
+}
+
+// Report any knocked out tuples from relation_add or relation_delete
+// This will also clear and free the result vector.
+static void knockout_report(relation *r,vector *v) {
+ vector_index i;
+ if ( v ) {
+ for ( i = 0; i < v->size; i++ ) {
+ tuple **t = (tuple **) vector_next_used( v, &i );
+ fprintf( stderr, "knock %s\n", tuple2string( r, t? *t : 0 ) );
+ }
+ vector_resize( v, 0, vector_clear_any, 0 );
+ free( v );
+ }
+}
+
+// Test addition with several tuples, terminated by 0
+static void test_relation_add(relation *r,tuple *query[]) {
+ int j;
+ for ( j = 0; query[j]; j++ ) {
+ fprintf( stderr, "add %s\n", tuple2string( r, query[j] ) );
+ knockout_report( r, relation_add( r, query[j] ) );
+ }
+}
+
+// Test deletion with several queries, terminated by 0
+static void test_relation_delete(relation *r,tuple *query[]) {
+ int j;
+ for ( j = 0; query[j]; j++ ) {
+ fprintf( stderr, "delete %s\n", tuple2string( r, query[j] ) );
+ knockout_report( r, relation_delete( r, query[j] ) );
+ }
+}