Author: eevans
Date: Mon Dec 13 19:50:09 2010
New Revision: 1045342
URL: http://svn.apache.org/viewvc?rev=1045342&view=rev
Log:
CASSANDRA-1706: CQL DELETE w/ functional tests
Patch by eevans for CASSANDRA-1706
Added:
cassandra/trunk/src/java/org/apache/cassandra/cql/DeleteStatement.java
Modified:
cassandra/trunk/doc/cql/CQL.html
cassandra/trunk/doc/cql/CQL.textile
cassandra/trunk/src/java/org/apache/cassandra/cql/Cql.g
cassandra/trunk/src/java/org/apache/cassandra/cql/QueryProcessor.java
cassandra/trunk/src/java/org/apache/cassandra/cql/StatementType.java
cassandra/trunk/test/system/test_cql.py
Modified: cassandra/trunk/doc/cql/CQL.html
URL:
http://svn.apache.org/viewvc/cassandra/trunk/doc/cql/CQL.html?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/doc/cql/CQL.html (original)
+++ cassandra/trunk/doc/cql/CQL.html Mon Dec 13 19:50:09 2010
@@ -1,4 +1,4 @@
-<?xml version='1.0' encoding='utf-8' ?><!DOCTYPE html PUBLIC "-//W3C//DTD
XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html
xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type"
content="text/html; charset=utf-8"/></head><body><h1
id="CassandraQueryLanguageCQLv0.99.1">Cassandra Query Language (CQL)
v0.99.1</h1><h2 id="TableofContents">Table of Contents</h2><ol
style="list-style: none;"><li><a
href="#CassandraQueryLanguageCQLv0.99.1">Cassandra Query Language (CQL)
v0.99.1</a><ol style="list-style: none;"><li><a href="#TableofContents">Table
of Contents</a></li><li><a href="#USE">USE</a></li><li><a
href="#SELECT">SELECT</a><ol style="list-style: none;"><li><a
href="#SpecifyingColumns">Specifying Columns</a></li><li><a
href="#ColumnFamily">Column Family</a></li><li><a
href="#ConsistencyLevel">Consistency Level</a></li><li><a
href="#Filteringrows">Filtering rows</a></li><li><a
href="#Limits">Limits</a></li></ol></li>
<li><a href="#UPDATE">UPDATE</a><ol style="list-style: none;"><li><a
href="#ColumnFamily2">Column Family</a></li><li><a
href="#ConsistencyLevel2">Consistency Level</a></li><li><a
href="#SpecifyingColumnsandRow">Specifying Columns and
Row</a></li></ol></li><li><a href="#TRUNCATE">TRUNCATE</a></li><li><a
href="#CommonIdioms">Common Idioms</a><ol style="list-style: none;"><li><a
href="#consistency">Specifying Consistency</a></li><li><a href="#terms">Term
specification</a><ol style="list-style: none;"><li><a
href="#StringLiterals">String Literals</a></li><li><a
href="#Integerslongs">Integers /
longs</a></li></ol></li></ol></li></ol></li></ol><h2
id="USE">USE</h2><p><i>Synopsis:</i></p><pre><code>USE <KEYSPACE>;
+<?xml version='1.0' encoding='utf-8' ?><!DOCTYPE html PUBLIC "-//W3C//DTD
XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html
xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type"
content="text/html; charset=utf-8"/></head><body><h1
id="CassandraQueryLanguageCQLv0.99.1">Cassandra Query Language (CQL)
v0.99.1</h1><h2 id="TableofContents">Table of Contents</h2><ol
style="list-style: none;"><li><a
href="#CassandraQueryLanguageCQLv0.99.1">Cassandra Query Language (CQL)
v0.99.1</a><ol style="list-style: none;"><li><a href="#TableofContents">Table
of Contents</a></li><li><a href="#USE">USE</a></li><li><a
href="#SELECT">SELECT</a><ol style="list-style: none;"><li><a
href="#SpecifyingColumns">Specifying Columns</a></li><li><a
href="#ColumnFamily">Column Family</a></li><li><a
href="#ConsistencyLevel">Consistency Level</a></li><li><a
href="#Filteringrows">Filtering rows</a></li><li><a
href="#Limits">Limits</a></li></ol></li>
<li><a href="#UPDATE">UPDATE</a><ol style="list-style: none;"><li><a
href="#ColumnFamily2">Column Family</a></li><li><a
href="#ConsistencyLevel2">Consistency Level</a></li><li><a
href="#SpecifyingColumnsandRow">Specifying Columns and
Row</a></li></ol></li><li><a href="#DELETE">DELETE</a><ol style="list-style:
none;"><li><a href="#SpecifyingColumns2">Specifying Columns</a></li><li><a
href="#ColumnFamily3">Column Family</a></li><li><a
href="#ConsistencyLevel3">Consistency Level</a></li><li><a
href="#deleterows">Specifying Rows</a></li></ol></li><li><a
href="#TRUNCATE">TRUNCATE</a></li><li><a href="#CommonIdioms">Common
Idioms</a><ol style="list-style: none;"><li><a href="#consistency">Specifying
Consistency</a></li><li><a href="#terms">Term specification</a><ol
style="list-style: none;"><li><a href="#StringLiterals">String
Literals</a></li><li><a href="#Integerslongs">Integers /
longs</a></li></ol></li></ol></li></ol></li></ol><h2
id="USE">USE</h2><p><i>Synopsis:</i></p><pre><
code>USE <KEYSPACE>;
</code></pre><p>A <code>USE</code> statement consists of the <code>USE</code>
keyword, followed by a valid keyspace name. Its purpose is to assign the
per-connection, current working keyspace. All subsequent keyspace-specific
actions will be performed in the context of the supplied value.</p><h2
id="SELECT">SELECT</h2><p><i>Synopsis:</i></p><pre><code>SELECT [FIRST N]
[REVERSED] <SELECT EXPR> FROM <COLUMN FAMILY> [USING
<CONSISTENCY>]
[WHERE <CLAUSE>] [LIMIT N];
</code></pre><p>A <code>SELECT</code> is used to read one or more records from
a Cassandra column family. It returns a result-set of rows, where each row
consists of a key and a collection of columns corresponding to the
query.</p><h3 id="SpecifyingColumns">Specifying Columns</h3><pre><code>SELECT
[FIRST N] [REVERSED] name1, name2, name3 FROM ...
@@ -18,6 +18,13 @@ UPDATE CF1 SET name1 = value1, name2 = v
UPDATE CF1 SET name3 = value3 WHERE KEY = keyname2;
UPDATE CF2 SET name4 = value4, name5 = value5 WHERE KEY = keyname3;
APPLY BATCH
-</code></pre><p>When batching UPDATEs, a single consistency level is used for
the entire batch, it appears after the <code>BEGIN BATCH</code> statement, and
uses the standard <a href="#consistency">consistency level specification</a>.
Batch UPDATEs default to <code>CONSISTENCY.ONE</code> when left
unspecified.</p><p><em>NOTE: While there are no isolation guarantees,
<code>UPDATE</code> queries are atomic within a give record.</em></p><h2
id="TRUNCATE">TRUNCATE</h2><p><em>Synopsis:</em></p><pre><code>TRUNCATE
<COLUMN FAMILY>
+</code></pre><p>When batching UPDATEs, a single consistency level is used for
the entire batch, it appears after the <code>BEGIN BATCH</code> statement, and
uses the standard <a href="#consistency">consistency level specification</a>.
Batch UPDATEs default to <code>CONSISTENCY.ONE</code> when left
unspecified.</p><p><em>NOTE: While there are no isolation guarantees,
<code>UPDATE</code> queries are atomic within a give record.</em></p><h2
id="DELETE">DELETE</h2><p><em>Synopsis:</em></p><pre><code>DELETE [COLUMNS]
FROM <COLUMN FAMILY> [USING <CONSISTENCY>] WHERE KEY = keyname1
+DELETE [COLUMNS] FROM <COLUMN FAMILY> [USING <CONSISTENCY>] WHERE
KEY IN (keyname1, keyname2);
+</code></pre><p>A <code>DELETE</code> is used to perform the removal of one or
more columns from one or more rows.</p><h3 id="SpecifyingColumns2">Specifying
Columns</h3><pre><code>DELETE [COLUMNS] ...
+</code></pre><p>Following the <code>DELETE</code> keyword is an optional
comma-delimited list of column name terms. When no column names are specified,
the remove applies to the entire row(s) matched by the <a
href="#deleterows"><code>WHERE</code> clause</a></p><h3
id="ColumnFamily3">Column Family</h3><pre><code>DELETE ... FROM <COLUMN
FAMILY> ...
+</code></pre><p>The column family name follows the list of column
names.</p><h3 id="ConsistencyLevel3">Consistency Level</h3><pre><code>UPDATE
... [USING <CONSISTENCY>] ...
+</code></pre><p>Following the column family identifier is an optional <a
href="#consistency">consistency level specification</a>.</p><h3
id="deleterows">Specifying Rows</h3><pre><code>UPDATE ... WHERE KEY = keyname1
+UPDATE ... WHERE KEY IN (keyname1, keyname2)
+</code></pre><p>The <code>WHERE</code> clause is used to determine which
row(s) a <code>DELETE</code> applies to. The first form allows the
specification of a single keyname using the <code>KEY</code> keyword and the
<code>=</code> operator. The second form allows a list of keyname terms to be
specified using the <code>IN</code> notation and a parenthesized list of
comma-delimited keyname terms.</p><h2
id="TRUNCATE">TRUNCATE</h2><p><em>Synopsis:</em></p><pre><code>TRUNCATE
<COLUMN FAMILY>
</code></pre><p>Accepts a single argument for the column family name, and
permanently removes all data from said column family.</p><h2
id="CommonIdioms">Common Idioms</h2><h3 id="consistency">Specifying
Consistency</h3><pre><code>... USING <CONSISTENCY> ...
</code></pre><p>Consistency level specifications are made up the keyword
<code>USING</code>, followed by a consistency level identifier. Valid
consistency levels are as
follows:</p><ul><li><code>CONSISTENCY.ZERO</code></li><li><code>CONSISTENCY.ONE</code>
(default)</li><li><code>CONSISTENCY.QUORUM</code></li><li><code>CONSISTENCY.ALL</code></li><li><code>CONSISTENCY.DCQUORUM</code></li><li><code>CONSISTENCY.DCQUORUMSYNC</code></li></ul><h3
id="terms">Term specification</h3><p>Where possible, the type of terms are
inferred; the following term types are supported:</p><h4
id="StringLiterals">String Literals</h4><p>String literals are any value
enclosed in double-quotes, (`"`). String literals are treated as raw bytes; no
interpolation is performed.</p><h4 id="Integerslongs">Integers /
longs</h4><p>Integers are any term consisting soley of unquoted numericals,
longs are any otherwise valid integer term followed by an upper case
“L”, (e.g. 100L). It is an error to s
pecify an integer term that will not fit in 4 bytes unsigned, or a long that
will not fit in 8 bytes unsigned.</p></body></html>
\ No newline at end of file
Modified: cassandra/trunk/doc/cql/CQL.textile
URL:
http://svn.apache.org/viewvc/cassandra/trunk/doc/cql/CQL.textile?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/doc/cql/CQL.textile (original)
+++ cassandra/trunk/doc/cql/CQL.textile Mon Dec 13 19:50:09 2010
@@ -112,6 +112,45 @@ When batching UPDATEs, a single consiste
_NOTE: While there are no isolation guarantees, @UPDATE@ queries are atomic
within a give record._
+h2. DELETE
+
+_Synopsis:_
+
+bc.
+DELETE [COLUMNS] FROM <COLUMN FAMILY> [USING <CONSISTENCY>] WHERE KEY =
keyname1
+DELETE [COLUMNS] FROM <COLUMN FAMILY> [USING <CONSISTENCY>] WHERE KEY IN
(keyname1, keyname2);
+
+A @DELETE@ is used to perform the removal of one or more columns from one or
more rows.
+
+h3. Specifying Columns
+
+bc.
+DELETE [COLUMNS] ...
+
+Following the @DELETE@ keyword is an optional comma-delimited list of column
name terms. When no column names are specified, the remove applies to the
entire row(s) matched by the "@WHERE@ clause":#deleterows
+
+h3. Column Family
+
+bc.
+DELETE ... FROM <COLUMN FAMILY> ...
+
+The column family name follows the list of column names.
+
+h3. Consistency Level
+
+bc.
+UPDATE ... [USING <CONSISTENCY>] ...
+
+Following the column family identifier is an optional "consistency level
specification":#consistency.
+
+h3(#deleterows). Specifying Rows
+
+bc.
+UPDATE ... WHERE KEY = keyname1
+UPDATE ... WHERE KEY IN (keyname1, keyname2)
+
+The @WHERE@ clause is used to determine which row(s) a @DELETE@ applies to.
The first form allows the specification of a single keyname using the @KEY@
keyword and the @=@ operator. The second form allows a list of keyname terms
to be specified using the @IN@ notation and a parenthesized list of
comma-delimited keyname terms.
+
h2. TRUNCATE
_Synopsis:_
Modified: cassandra/trunk/src/java/org/apache/cassandra/cql/Cql.g
URL:
http://svn.apache.org/viewvc/cassandra/trunk/src/java/org/apache/cassandra/cql/Cql.g?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/src/java/org/apache/cassandra/cql/Cql.g (original)
+++ cassandra/trunk/src/java/org/apache/cassandra/cql/Cql.g Mon Dec 13 19:50:09
2010
@@ -8,6 +8,7 @@ options {
package org.apache.cassandra.cql;
import java.util.Map;
import java.util.HashMap;
+ import java.util.Collections;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.avro.InvalidRequestException;
}
@@ -48,6 +49,7 @@ query returns [CQLStatement stmnt]
| batchUpdateStatement { $stmnt = new
CQLStatement(StatementType.BATCH_UPDATE, $batchUpdateStatement.expr); }
| useStatement { $stmnt = new CQLStatement(StatementType.USE,
$useStatement.keyspace); }
| truncateStatement { $stmnt = new CQLStatement(StatementType.TRUNCATE,
$truncateStatement.cfam); }
+ | deleteStatement { $stmnt = new CQLStatement(StatementType.DELETE,
$deleteStatement.expr); }
;
// USE <KEYSPACE>;
@@ -136,12 +138,44 @@ updateStatement returns [UpdateStatement
}
;
+/**
+ * DELETE
+ * name1, name2
+ * FROM
+ * <CF>
+ * USING
+ * CONSISTENCY.<LVL>
+ * WHERE
+ * KEY = keyname;
+ */
+deleteStatement returns [DeleteStatement expr]
+ : {
+ ConsistencyLevel cLevel = ConsistencyLevel.ONE;
+ List<Term> keyList = null;
+ List<Term> columnsList = Collections.emptyList();
+ }
+ K_DELETE
+ ( cols=termList { columnsList = $cols.items; })?
+ K_FROM columnFamily=IDENT ( K_USING K_CONSISTENCY '.' K_LEVEL )?
+ K_WHERE ( K_KEY '=' key=term { keyList =
Collections.singletonList(key); }
+ | K_KEY K_IN '(' keys=termList { keyList = $keys.items; } ')'
+ )?
+ {
+ return new DeleteStatement(columnsList, $columnFamily.text, cLevel,
keyList);
+ }
+ ;
+
// TODO: date/time, utf8
term returns [Term item]
: ( t=STRING_LITERAL | t=LONG )
{ $item = new Term($t.text, $t.type); }
;
+termList returns [List<Term> items]
+ : { $items = new ArrayList<Term>(); }
+ t1=term { $items.add(t1); } (',' tN=term { $items.add(tN); })*
+ ;
+
// Note: ranges are inclusive so >= and >, and < and <= all have the same
semantics.
relation returns [Relation rel]
: { Term entity = new Term("KEY", STRING_LITERAL); }
@@ -208,6 +242,8 @@ K_BEGIN: B E G I N;
K_APPLY: A P P L Y;
K_BATCH: B A T C H;
K_TRUNCATE: T R U N C A T E;
+K_DELETE: D E L E T E;
+K_IN: I N;
// Case-insensitive alpha characters
fragment A: ('a'|'A');
Added: cassandra/trunk/src/java/org/apache/cassandra/cql/DeleteStatement.java
URL:
http://svn.apache.org/viewvc/cassandra/trunk/src/java/org/apache/cassandra/cql/DeleteStatement.java?rev=1045342&view=auto
==============================================================================
--- cassandra/trunk/src/java/org/apache/cassandra/cql/DeleteStatement.java
(added)
+++ cassandra/trunk/src/java/org/apache/cassandra/cql/DeleteStatement.java Mon
Dec 13 19:50:09 2010
@@ -0,0 +1,74 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.cassandra.cql;
+
+import java.util.List;
+
+import org.apache.cassandra.thrift.ConsistencyLevel;
+
+/**
+ * A <code>DELETE</code> parsed from a CQL query statement.
+ *
+ */
+public class DeleteStatement
+{
+ private List<Term> columns;
+ private String columnFamily;
+ private ConsistencyLevel cLevel;
+ private List<Term> keys;
+
+ public DeleteStatement(List<Term> columns, String columnFamily,
ConsistencyLevel cLevel, List<Term> keys)
+ {
+ this.columns = columns;
+ this.columnFamily = columnFamily;
+ this.cLevel = cLevel;
+ this.keys = keys;
+ }
+
+ public List<Term> getColumns()
+ {
+ return columns;
+ }
+
+ public String getColumnFamily()
+ {
+ return columnFamily;
+ }
+
+ public ConsistencyLevel getConsistencyLevel()
+ {
+ return cLevel;
+ }
+
+ public List<Term> getKeys()
+ {
+ return keys;
+ }
+
+ public String toString()
+ {
+ return String.format("DeleteStatement(columns=%s, columnFamily=%s,
consistency=%s keys=%s)",
+ columns,
+ columnFamily,
+ cLevel,
+ keys);
+ }
+}
Modified: cassandra/trunk/src/java/org/apache/cassandra/cql/QueryProcessor.java
URL:
http://svn.apache.org/viewvc/cassandra/trunk/src/java/org/apache/cassandra/cql/QueryProcessor.java?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/src/java/org/apache/cassandra/cql/QueryProcessor.java
(original)
+++ cassandra/trunk/src/java/org/apache/cassandra/cql/QueryProcessor.java Mon
Dec 13 19:50:09 2010
@@ -378,6 +378,8 @@ public class QueryProcessor
for (IColumn column : row.cf.getSortedColumns())
{
+ if (column.isMarkedForDelete())
+ continue;
Column avroColumn = new Column();
avroColumn.name = column.name();
avroColumn.value = column.value();
@@ -443,6 +445,40 @@ public class QueryProcessor
avroResult.type = CqlResultType.VOID;
return avroResult;
+
+ case DELETE:
+ DeleteStatement delete = (DeleteStatement)statement.statement;
+
+ List<RowMutation> rowMutations = new ArrayList<RowMutation>();
+ for (Term key : delete.getKeys())
+ {
+ RowMutation rm = new RowMutation(keyspace,
key.getByteBuffer());
+ if (delete.getColumns().size() < 1) // No columns,
delete the row
+ rm.delete(new QueryPath(delete.getColumnFamily()),
System.currentTimeMillis());
+ else // Delete specific columns
+ {
+ for (Term column : delete.getColumns())
+ rm.delete(new QueryPath(delete.getColumnFamily(),
null, column.getByteBuffer()),
+ System.currentTimeMillis());
+ }
+ rowMutations.add(rm);
+ }
+
+ try
+ {
+ StorageProxy.mutate(rowMutations,
delete.getConsistencyLevel());
+ }
+ catch (org.apache.cassandra.thrift.UnavailableException e)
+ {
+ throw newUnavailableException(e);
+ }
+ catch (TimeoutException e)
+ {
+ throw new TimedOutException();
+ }
+
+ avroResult.type = CqlResultType.VOID;
+ return avroResult;
}
return null; // We should never get here.
Modified: cassandra/trunk/src/java/org/apache/cassandra/cql/StatementType.java
URL:
http://svn.apache.org/viewvc/cassandra/trunk/src/java/org/apache/cassandra/cql/StatementType.java?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/src/java/org/apache/cassandra/cql/StatementType.java
(original)
+++ cassandra/trunk/src/java/org/apache/cassandra/cql/StatementType.java Mon
Dec 13 19:50:09 2010
@@ -22,5 +22,5 @@ package org.apache.cassandra.cql;
public enum StatementType
{
- SELECT, UPDATE, BATCH_UPDATE, USE, TRUNCATE;
+ SELECT, UPDATE, BATCH_UPDATE, USE, TRUNCATE, DELETE;
}
Modified: cassandra/trunk/test/system/test_cql.py
URL:
http://svn.apache.org/viewvc/cassandra/trunk/test/system/test_cql.py?rev=1045342&r1=1045341&r2=1045342&view=diff
==============================================================================
--- cassandra/trunk/test/system/test_cql.py (original)
+++ cassandra/trunk/test/system/test_cql.py Mon Dec 13 19:50:09 2010
@@ -166,3 +166,38 @@ class TestCql(AvroTester):
conn.execute('TRUNCATE Standard1;')
r = conn.execute('SELECT "cd1" FROM Standard1 WHERE KEY = "kd"')
assert len(r) == 0
+
+ def test_delete_columns(self):
+ "delete columns from a row"
+ conn = init()
+ r = conn.execute('SELECT "cd1", "col" FROM Standard1 WHERE KEY = "kd"')
+ assert "cd1" in [i['name'] for i in r[0]['columns']]
+ assert "col" in [i['name'] for i in r[0]['columns']]
+ conn.execute('DELETE "cd1", "col" FROM Standard1 WHERE KEY = "kd"')
+ r = conn.execute('SELECT "cd1", "col" FROM Standard1 WHERE KEY = "kd"')
+ assert len(r[0]['columns']) == 0
+
+ def test_delete_columns_multi_rows(self):
+ "delete columns from multiple rows"
+ conn = init()
+ r = conn.execute('SELECT "col" FROM Standard1 WHERE KEY = "kc"')
+ assert len(r[0]['columns']) == 1
+ r = conn.execute('SELECT "col" FROM Standard1 WHERE KEY = "kd"')
+ assert len(r[0]['columns']) == 1
+
+ conn.execute('DELETE "col" FROM Standard1 WHERE KEY IN ("kc", "kd")')
+ r = conn.execute('SELECT "col" FROM Standard1 WHERE KEY = "kc"')
+ assert len(r[0]['columns']) == 0
+ r = conn.execute('SELECT "col" FROM Standard1 WHERE KEY = "kd"')
+ assert len(r[0]['columns']) == 0
+
+ def test_delete_rows(self):
+ "delete entire rows"
+ conn = init()
+ r = conn.execute('SELECT "cd1", "col" FROM Standard1 WHERE KEY = "kd"')
+ assert "cd1" in [i['name'] for i in r[0]['columns']]
+ assert "col" in [i['name'] for i in r[0]['columns']]
+ conn.execute('DELETE FROM Standard1 WHERE KEY = "kd"')
+ r = conn.execute('SELECT "cd1", "col" FROM Standard1 WHERE KEY = "kd"')
+ assert len(r[0]['columns']) == 0
+