未验证 提交 301ec7a5 编写于 作者: P Patrick Mackinlay 提交者: GitHub

feat(griffin): `show tables` and `show columns` (#334)

上级 1bd1a13b
......@@ -91,6 +91,8 @@ public class JsonQueryProcessor implements HttpRequestProcessor, Closeable {
this.queryExecutors.extendAndSet(CompiledQuery.INSERT_AS_SELECT, sendConfirmation);
this.queryExecutors.extendAndSet(CompiledQuery.COPY_REMOTE, JsonQueryProcessor::cannotCopyRemote);
this.queryExecutors.extendAndSet(CompiledQuery.BACKUP_TABLE, sendConfirmation);
this.queryExecutors.extendAndSet(CompiledQuery.SHOW_TABLES, this::executeNewSelect);
this.queryExecutors.extendAndSet(CompiledQuery.SHOW_COLUMNS, this::executeNewSelect);
this.sqlExecutionContext = new SqlExecutionContextImpl(engine.getConfiguration(), messageBus, workerCount);
this.nanosecondClock = engine.getConfiguration().getNanosecondClock();
}
......
......@@ -42,6 +42,8 @@ public interface CompiledQuery {
int COPY_REMOTE = 10;
int RENAME_TABLE = 11;
int BACKUP_TABLE = 12;
int SHOW_TABLES = 13;
int SHOW_COLUMNS = 14;
RecordCursorFactory getRecordCursorFactory();
......
......@@ -121,4 +121,16 @@ public class CompiledQueryImpl implements CompiledQuery {
this.type = BACKUP_TABLE;
return this;
}
CompiledQuery ofShowTables(RecordCursorFactory recordCursorFactory) {
this.type = SHOW_TABLES;
this.recordCursorFactory = recordCursorFactory;
return this;
}
CompiledQuery ofShowColumns(RecordCursorFactory recordCursorFactory) {
this.type = SHOW_COLUMNS;
this.recordCursorFactory = recordCursorFactory;
return this;
}
}
......@@ -24,26 +24,88 @@
package io.questdb.griffin;
import static io.questdb.griffin.SqlKeywords.isCapacityKeyword;
import static io.questdb.griffin.SqlKeywords.isColumnsKeyword;
import static io.questdb.griffin.SqlKeywords.isDatabaseKeyword;
import static io.questdb.griffin.SqlKeywords.isFromKeyword;
import static io.questdb.griffin.SqlKeywords.isOnlyKeyword;
import static io.questdb.griffin.SqlKeywords.isTableKeyword;
import static io.questdb.griffin.SqlKeywords.isTablesKeyword;
import java.io.Closeable;
import java.util.ServiceLoader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import io.questdb.MessageBus;
import io.questdb.cairo.*;
import io.questdb.cairo.sql.*;
import io.questdb.cairo.AppendMemory;
import io.questdb.cairo.CairoConfiguration;
import io.questdb.cairo.CairoEngine;
import io.questdb.cairo.CairoError;
import io.questdb.cairo.CairoException;
import io.questdb.cairo.CairoSecurityContext;
import io.questdb.cairo.ColumnFilter;
import io.questdb.cairo.ColumnType;
import io.questdb.cairo.ColumnTypes;
import io.questdb.cairo.DefaultLifecycleManager;
import io.questdb.cairo.EntityColumnFilter;
import io.questdb.cairo.GenericRecordMetadata;
import io.questdb.cairo.ListColumnFilter;
import io.questdb.cairo.PartitionBy;
import io.questdb.cairo.SymbolMapReader;
import io.questdb.cairo.SymbolMapWriter;
import io.questdb.cairo.TableReader;
import io.questdb.cairo.TableReaderMetadata;
import io.questdb.cairo.TableStructure;
import io.questdb.cairo.TableUtils;
import io.questdb.cairo.TableWriter;
import io.questdb.cairo.sql.Function;
import io.questdb.cairo.sql.ReaderOutOfDateException;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.RecordCursor;
import io.questdb.cairo.sql.RecordCursorFactory;
import io.questdb.cairo.sql.RecordMetadata;
import io.questdb.cairo.sql.VirtualRecord;
import io.questdb.cutlass.text.Atomicity;
import io.questdb.cutlass.text.TextException;
import io.questdb.cutlass.text.TextLoader;
import io.questdb.griffin.model.*;
import io.questdb.griffin.engine.table.ShowColumnsRecordCursorFactory;
import io.questdb.griffin.engine.table.TableListRecordCursorFactory;
import io.questdb.griffin.model.ColumnCastModel;
import io.questdb.griffin.model.CopyModel;
import io.questdb.griffin.model.CreateTableModel;
import io.questdb.griffin.model.ExecutionModel;
import io.questdb.griffin.model.ExpressionNode;
import io.questdb.griffin.model.InsertModel;
import io.questdb.griffin.model.QueryColumn;
import io.questdb.griffin.model.QueryModel;
import io.questdb.griffin.model.RenameTableModel;
import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.std.*;
import io.questdb.std.BytecodeAssembler;
import io.questdb.std.CharSequenceHashSet;
import io.questdb.std.CharSequenceObjHashMap;
import io.questdb.std.Chars;
import io.questdb.std.Files;
import io.questdb.std.FilesFacade;
import io.questdb.std.FindVisitor;
import io.questdb.std.GenericLexer;
import io.questdb.std.IntIntHashMap;
import io.questdb.std.IntList;
import io.questdb.std.Misc;
import io.questdb.std.Numbers;
import io.questdb.std.NumericException;
import io.questdb.std.ObjHashSet;
import io.questdb.std.ObjList;
import io.questdb.std.ObjectPool;
import io.questdb.std.Os;
import io.questdb.std.Sinkable;
import io.questdb.std.Transient;
import io.questdb.std.Unsafe;
import io.questdb.std.microtime.TimestampFormat;
import io.questdb.std.str.NativeLPSZ;
import io.questdb.std.str.Path;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.util.ServiceLoader;
import static io.questdb.griffin.SqlKeywords.*;
public class SqlCompiler implements Closeable {
......@@ -107,8 +169,7 @@ public class SqlCompiler implements Closeable {
this.queryModelPool = new ObjectPool<>(QueryModel.FACTORY, configuration.getSqlModelPoolCapacity());
this.characterStore = new CharacterStore(
configuration.getSqlCharacterStoreCapacity(),
configuration.getSqlCharacterStoreSequencePoolCapacity()
);
configuration.getSqlCharacterStoreSequencePoolCapacity());
this.lexer = new GenericLexer(configuration.getSqlLexerPoolCapacity());
this.functionParser = new FunctionParser(configuration, ServiceLoader.load(FunctionFactory.class));
this.codeGenerator = new SqlCodeGenerator(engine, configuration, functionParser);
......@@ -128,6 +189,8 @@ public class SqlCompiler implements Closeable {
keywordBasedExecutors.put("DROP", this::dropTable);
keywordBasedExecutors.put("backup", this::sqlBackup);
keywordBasedExecutors.put("BACKUP", this::sqlBackup);
keywordBasedExecutors.put("show", this::sqlShow);
keywordBasedExecutors.put("SHOW", this::sqlShow);
configureLexer(lexer);
......@@ -171,16 +234,14 @@ public class SqlCompiler implements Closeable {
public static boolean isAssignableFrom(int to, int from) {
return to == from
|| (
from >= ColumnType.BYTE
|| (from >= ColumnType.BYTE
&& to >= ColumnType.BYTE
&& to <= ColumnType.DOUBLE
&& from < to)
|| (from == ColumnType.STRING && to == ColumnType.SYMBOL)
|| (from == ColumnType.SYMBOL && to == ColumnType.STRING)
|| (from == ColumnType.CHAR && to == ColumnType.SYMBOL)
|| (from == ColumnType.CHAR && to == ColumnType.STRING)
;
|| (from == ColumnType.CHAR && to == ColumnType.STRING);
}
@Override
......@@ -1644,6 +1705,34 @@ public class SqlCompiler implements Closeable {
}
}
@SuppressWarnings("resource")
private CompiledQuery sqlShow(SqlExecutionContext executionContext) throws SqlException {
CharSequence tok = SqlUtil.fetchNext(lexer);
if (null != tok) {
if (isTablesKeyword(tok)) {
return compiledQuery.ofShowTables(new TableListRecordCursorFactory(configuration.getFilesFacade(), configuration.getRoot()));
}
if (isColumnsKeyword(tok)) {
tok = SqlUtil.fetchNext(lexer);
if (null == tok || !isFromKeyword(tok)) {
throw SqlException.position(lexer.getPosition()).put("expected 'from'");
}
tok = SqlUtil.fetchNext(lexer);
if (null == tok) {
throw SqlException.position(lexer.getPosition()).put("expected a table name");
}
final CharSequence tableName = GenericLexer.assertNoDotsAndSlashes(GenericLexer.unquote(tok), lexer.lastTokenPosition());
int status = engine.getStatus(executionContext.getCairoSecurityContext(), path, tableName, 0, tableName.length());
if (status != TableUtils.TABLE_EXISTS) {
throw SqlException.position(lexer.lastTokenPosition()).put('\'').put(tableName).put("' is not a valid table");
}
return compiledQuery.ofShowTables(new ShowColumnsRecordCursorFactory(engine, tableName));
}
}
throw SqlException.position(lexer.lastTokenPosition()).put("expected 'tables' or 'columns'");
}
private void tableExistsOrFail(int position, CharSequence tableName, SqlExecutionContext executionContext) throws SqlException {
if (engine.getStatus(executionContext.getCairoSecurityContext(), path, tableName) == TableUtils.TABLE_DOES_NOT_EXIST) {
throw SqlException.$(position, "table '").put(tableName).put("' does not exist");
......
......@@ -337,6 +337,22 @@ public class SqlKeywords {
&& (tok.charAt(i) | 32) == 'e';
}
public static boolean isTablesKeyword(CharSequence tok) {
if (tok.length() != 6) {
return false;
}
int i = 0;
// @formatter:off
return (tok.charAt(i++) | 32) == 't'
&& (tok.charAt(i++) | 32) == 'a'
&& (tok.charAt(i++) | 32) == 'b'
&& (tok.charAt(i++) | 32) == 'l'
&& (tok.charAt(i++) | 32) == 'e'
&& (tok.charAt(i) | 32) == 's';
// @formatter:off
}
public static boolean isUnionKeyword(CharSequence tok) {
if (tok.length() != 5) {
return false;
......@@ -642,4 +658,21 @@ public class SqlKeywords {
&& (tok.charAt(i++) | 32) == 'm'
&& (tok.charAt(i) | 32) == 'n';
}
public static boolean isColumnsKeyword(CharSequence tok) {
if (tok.length() != 7) {
return false;
}
int i = 0;
// @formatter:off
return (tok.charAt(i++) | 32) == 'c'
&& (tok.charAt(i++) | 32) == 'o'
&& (tok.charAt(i++) | 32) == 'l'
&& (tok.charAt(i++) | 32) == 'u'
&& (tok.charAt(i++) | 32) == 'm'
&& (tok.charAt(i++) | 32) == 'n'
&& (tok.charAt(i) | 32) == 's';
// @formatter:on
}
}
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2020 QuestDB
*
* Licensed 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 io.questdb.griffin.engine.table;
import io.questdb.cairo.CairoEngine;
import io.questdb.cairo.CairoSecurityContext;
import io.questdb.cairo.ColumnType;
import io.questdb.cairo.GenericRecordMetadata;
import io.questdb.cairo.TableColumnMetadata;
import io.questdb.cairo.TableReader;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.RecordCursor;
import io.questdb.cairo.sql.RecordCursorFactory;
import io.questdb.cairo.sql.RecordMetadata;
import io.questdb.griffin.SqlExecutionContext;
public class ShowColumnsRecordCursorFactory implements RecordCursorFactory {
private static final RecordMetadata METADATA;
private static final int N_NAME_COL = 0;
private static final int N_TYPE_COL = 1;
static {
final GenericRecordMetadata metadata = new GenericRecordMetadata();
metadata.add(new TableColumnMetadata("columnName", ColumnType.STRING));
metadata.add(new TableColumnMetadata("columnType", ColumnType.STRING));
METADATA = metadata;
}
private final ShowColumnsCursor cursor = new ShowColumnsCursor();
private final CairoEngine engine;
private final CharSequence tableName;
public ShowColumnsRecordCursorFactory(CairoEngine engine, CharSequence tableName) {
this.engine = engine;
this.tableName = tableName.toString();
}
@Override
public RecordCursor getCursor(SqlExecutionContext executionContext) {
return cursor.of(executionContext.getCairoSecurityContext());
}
@Override
public RecordMetadata getMetadata() {
return METADATA;
}
@Override
public boolean recordCursorSupportsRandomAccess() {
return false;
}
private class ShowColumnsCursor implements RecordCursor {
private final ShowColumnsRecord record = new ShowColumnsRecord();
private TableReader reader;
private int columnIndex;
@Override
public void close() {
if (null != reader) {
reader.close();
reader = null;
}
}
@Override
public Record getRecord() {
return record;
}
@Override
public boolean hasNext() {
columnIndex++;
if (columnIndex < reader.getMetadata().getColumnCount()) {
return true;
}
columnIndex--;
return false;
}
@Override
public Record getRecordB() {
throw new UnsupportedOperationException();
}
@Override
public void recordAt(Record record, long atRowId) {
throw new UnsupportedOperationException();
}
@Override
public void toTop() {
columnIndex = -1;
}
@Override
public long size() {
return -1;
}
private ShowColumnsCursor of(CairoSecurityContext securityContext) {
reader = engine.getReader(securityContext, tableName);
toTop();
return this;
}
public class ShowColumnsRecord implements Record {
@Override
public CharSequence getStr(int col) {
if (col == N_NAME_COL) {
return reader.getMetadata().getColumnName(columnIndex);
}
if (col == N_TYPE_COL) {
return ColumnType.nameOf(reader.getMetadata().getColumnType(columnIndex));
}
return null;
}
@Override
public CharSequence getStrB(int col) {
return getStr(col);
}
@Override
public int getStrLen(int col) {
return getStr(col).length();
}
}
}
}
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2020 QuestDB
*
* Licensed 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 io.questdb.griffin.engine.table;
import io.questdb.cairo.ColumnType;
import io.questdb.cairo.GenericRecordMetadata;
import io.questdb.cairo.TableColumnMetadata;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.RecordCursor;
import io.questdb.cairo.sql.RecordCursorFactory;
import io.questdb.cairo.sql.RecordMetadata;
import io.questdb.griffin.SqlExecutionContext;
import io.questdb.std.Files;
import io.questdb.std.FilesFacade;
import io.questdb.std.str.NativeLPSZ;
import io.questdb.std.str.Path;
public class TableListRecordCursorFactory implements RecordCursorFactory {
private static final RecordMetadata METADATA;
static {
final GenericRecordMetadata metadata = new GenericRecordMetadata();
metadata.add(new TableColumnMetadata("tableName", ColumnType.STRING));
METADATA = metadata;
}
private final FilesFacade ff;
private Path path;
private final TableListRecordCursor cursor;
public TableListRecordCursorFactory(FilesFacade ff, CharSequence dbRoot) {
this.ff = ff;
path = new Path().of(dbRoot).$();
cursor = new TableListRecordCursor();
}
@Override
public RecordCursor getCursor(SqlExecutionContext executionContext) {
return cursor.of();
}
@Override
public RecordMetadata getMetadata() {
return METADATA;
}
@Override
public boolean recordCursorSupportsRandomAccess() {
return false;
}
@Override
public void close() {
if (null != path) {
path.close();
path = null;
}
}
private class TableListRecordCursor implements RecordCursor {
private final NativeLPSZ nativeLPSZ = new NativeLPSZ();
private final TableListRecord record = new TableListRecord();
private long findPtr = 0;
@Override
public void close() {
if (findPtr > 0) {
ff.findClose(findPtr);
findPtr = 0;
}
}
@Override
public Record getRecord() {
return record;
}
@Override
public boolean hasNext() {
while (true) {
if (findPtr == 0) {
findPtr = ff.findFirst(path);
if (findPtr <= 0) {
return false;
}
} else {
if (ff.findNext(findPtr) <= 0) {
return false;
}
}
nativeLPSZ.of(ff.findName(findPtr));
int type = ff.findType(findPtr);
if (type == Files.DT_DIR && nativeLPSZ.charAt(0) != '.') {
return true;
}
}
}
@Override
public Record getRecordB() {
throw new UnsupportedOperationException();
}
@Override
public void recordAt(Record record, long atRowId) {
throw new UnsupportedOperationException();
}
@Override
public void toTop() {
close();
}
@Override
public long size() {
return -1;
}
private TableListRecordCursor of() {
toTop();
return this;
}
public class TableListRecord implements Record {
@Override
public CharSequence getStr(int col) {
if (col == 0) {
return nativeLPSZ;
}
return null;
}
@Override
public CharSequence getStrB(int col) {
return getStr(col);
}
@Override
public int getStrLen(int col) {
return getStr(col).length();
}
}
}
}
......@@ -503,12 +503,18 @@ public class AbstractGriffinTest extends AbstractCairoTest {
}
void assertFactoryCursor(String expected, String expectedTimestamp, RecordCursorFactory factory, boolean supportsRandomAccess, SqlExecutionContext sqlExecutionContext) {
assertFactoryCursor(expected, expectedTimestamp, factory, supportsRandomAccess, sqlExecutionContext, true);
}
void assertFactoryCursor(
String expected, String expectedTimestamp, RecordCursorFactory factory, boolean supportsRandomAccess, SqlExecutionContext sqlExecutionContext, boolean checkSameStr
) {
assertTimestamp(expectedTimestamp, factory, sqlExecutionContext);
assertCursor(expected, factory, supportsRandomAccess, true, sqlExecutionContext);
assertCursor(expected, factory, supportsRandomAccess, checkSameStr, sqlExecutionContext);
// make sure we get the same outcome when we get factory to create new cursor
assertCursor(expected, factory, supportsRandomAccess, true, sqlExecutionContext);
assertCursor(expected, factory, supportsRandomAccess, checkSameStr, sqlExecutionContext);
// make sure strings, binary fields and symbols are compliant with expected record behaviour
assertVariableColumns(factory, true);
assertVariableColumns(factory, checkSameStr);
}
protected void assertFailure(
......@@ -547,8 +553,13 @@ public class AbstractGriffinTest extends AbstractCairoTest {
}
protected void assertQuery(String expected, String query, String expectedTimestamp, boolean supportsRandomAccess, SqlExecutionContext sqlExecutionContext) throws SqlException {
assertQuery(expected, query, expectedTimestamp, supportsRandomAccess, sqlExecutionContext, true);
}
protected void assertQuery(String expected, String query, String expectedTimestamp, boolean supportsRandomAccess, SqlExecutionContext sqlExecutionContext, boolean checkSameStr)
throws SqlException {
try (final RecordCursorFactory factory = compiler.compile(query, sqlExecutionContext).getRecordCursorFactory()) {
assertFactoryCursor(expected, expectedTimestamp, factory, supportsRandomAccess, sqlExecutionContext);
assertFactoryCursor(expected, expectedTimestamp, factory, supportsRandomAccess, sqlExecutionContext, checkSameStr);
}
}
......
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2020 QuestDB
*
* Licensed 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 io.questdb.griffin;
import org.junit.Assert;
import org.junit.Test;
public class ShowTablesTest extends AbstractGriffinTest {
@Test
public void testShowTablesWithSingleTable() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
assertQuery("tableName\nbalances\n", "show tables", null, false, sqlExecutionContext, false);
});
}
@Test
public void testShowTablesWithDrop() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
assertQuery("tableName\nbalances\n", "show tables", null, false, sqlExecutionContext, false);
compiler.compile("create table balances2(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
compiler.compile("drop table balances", sqlExecutionContext);
assertQuery("tableName\nbalances2\n", "show tables", null, false, sqlExecutionContext, false);
});
}
@Test
public void testShowColumnsWithSimpleTable() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
assertQuery("columnName\tcolumnType\ncust_id\tINT\nccy\tSYMBOL\nbalance\tDOUBLE\n", "show columns from balances", null, false, sqlExecutionContext, false);
});
}
@Test
public void testShowColumnsWithMissingTable() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
try {
assertQuery("columnName\tcolumnType\ncust_id\tINT\nccy\tSYMBOL\nbalance\tDOUBLE\n", "show columns from balances2", null, false, sqlExecutionContext, false);
Assert.fail();
} catch (SqlException ex) {
Assert.assertTrue(ex.toString().contains("'balances2' is not a valid table"));
}
});
}
@Test
public void testSqlSyntax1() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
try {
assertQuery("columnName\tcolumnType\ncust_id\tINT\nccy\tSYMBOL\nbalance\tDOUBLE\n", "show", null, false, sqlExecutionContext, false);
Assert.fail();
} catch (SqlException ex) {
Assert.assertTrue(ex.toString().contains("expected 'tables' or 'columns'"));
}
});
}
@Test
public void testSqlSyntax2() throws Exception {
assertMemoryLeak(() -> {
compiler.compile("create table balances(cust_id int, ccy symbol, balance double)", sqlExecutionContext);
try {
assertQuery("columnName\tcolumnType\ncust_id\tINT\nccy\tSYMBOL\nbalance\tDOUBLE\n", "show columns balances", null, false, sqlExecutionContext, false);
Assert.fail();
} catch (SqlException ex) {
Assert.assertTrue(ex.toString().contains("expected 'from'"));
}
});
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册