Skip to content

Commit

Permalink
#428 auto complete list (#3980)
Browse files Browse the repository at this point in the history
  • Loading branch information
manticore-projects committed Feb 18, 2024
1 parent 4497462 commit 33da479
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 33 deletions.
64 changes: 32 additions & 32 deletions h2/src/main/org/h2/bnf/context/DbContextRule.java
Expand Up @@ -69,21 +69,27 @@ public void accept(BnfVisitor visitor) {

@Override
public boolean autoComplete(Sentence sentence) {
String query = sentence.getQuery(), s = query;
String up = sentence.getQueryUpper();
final String query = sentence.getQuery();
String s = query;
switch (type) {
case SCHEMA: {
DbSchema[] schemas = contents.getSchemas();
String best = null;
DbSchema bestSchema = null;
for (DbSchema schema: schemas) {
String name = StringUtils.toUpperEnglish(schema.name);
if (up.startsWith(name)) {
String name = schema.name;
String quotedName = StringUtils.quoteIdentifier(name);
if (StringUtils.startsWithIgnoringCase(query, name)) {
if (best == null || name.length() > best.length()) {
best = name;
bestSchema = schema;
}
} else if (s.length() == 0 || name.startsWith(up)) {
} else if (StringUtils.startsWith(query, quotedName)) {
if (best == null || name.length() > best.length()) {
best = quotedName;
bestSchema = schema;
}
} else if (s.isEmpty() || StringUtils.startsWithIgnoringCase(name, query) || StringUtils.startsWithIgnoringCase(quotedName, query)) {
if (s.length() < name.length()) {
sentence.add(name, name.substring(s.length()), type);
sentence.add(schema.quotedName + ".",
Expand All @@ -107,18 +113,15 @@ public boolean autoComplete(Sentence sentence) {
String best = null;
DbTableOrView bestTable = null;
for (DbTableOrView table : tables) {
String compare = up;
String name = StringUtils.toUpperEnglish(table.getName());
if (table.getQuotedName().length() > name.length()) {
name = table.getQuotedName();
compare = query;
}
if (compare.startsWith(name)) {
String name = table.getName();
String quotedName = StringUtils.quoteIdentifier(name);

if (StringUtils.startsWithIgnoringCase(query, name) || StringUtils.startsWithIgnoringCase("\"" + query, quotedName)) {
if (best == null || name.length() > best.length()) {
best = name;
bestTable = table;
}
} else if (s.length() == 0 || name.startsWith(compare)) {
} else if (s.isEmpty() || StringUtils.startsWithIgnoringCase(name, query) || StringUtils.startsWithIgnoringCase(quotedName, query)) {
if (s.length() < name.length()) {
sentence.add(table.getQuotedName(),
table.getQuotedName().substring(s.length()),
Expand All @@ -144,16 +147,14 @@ public boolean autoComplete(Sentence sentence) {
if (query.indexOf(' ') < 0) {
break;
}
for (; i < up.length(); i++) {
char ch = up.charAt(i);
if (ch != '_' && !Character.isLetterOrDigit(ch)) {
break;
}
}
if (i == 0) {
int l = query.length(), cp;
if (!Character.isJavaIdentifierStart(cp = query.codePointAt(i)) || cp == '$') {
break;
}
String alias = up.substring(0, i);
while ((i += Character.charCount(cp)) < l && Character.isJavaIdentifierPart(cp = query.codePointAt(i))) {
//
}
String alias = query.substring(0, i);
if (ParserUtil.isKeyword(alias, false)) {
break;
}
Expand All @@ -166,17 +167,17 @@ public boolean autoComplete(Sentence sentence) {
DbTableOrView last = sentence.getLastMatchedTable();
if (last != null && last.getColumns() != null) {
for (DbColumn column : last.getColumns()) {
String compare = up;
String name = StringUtils.toUpperEnglish(column.getName());
String compare = query;
String name = column.getName();
if (column.getQuotedName().length() > name.length()) {
name = column.getQuotedName();
compare = query;
}
if (compare.startsWith(name) && testColumnType(column)) {
if (StringUtils.startsWithIgnoringCase(compare, name) && testColumnType(column)) {
String b = s.substring(name.length());
if (best == null || b.length() < best.length()) {
best = b;
} else if (s.length() == 0 || name.startsWith(compare)) {
} else if (s.isEmpty() || StringUtils.startsWithIgnoringCase(name, compare)) {
if (s.length() < name.length()) {
sentence.add(column.getName(),
column.getName().substring(s.length()),
Expand All @@ -195,15 +196,14 @@ public boolean autoComplete(Sentence sentence) {
continue;
}
for (DbColumn column : table.getColumns()) {
String name = StringUtils.toUpperEnglish(column
.getName());
String name = column.getName();
if (testColumnType(column)) {
if (up.startsWith(name)) {
if (StringUtils.startsWithIgnoringCase(query, name)) {
String b = s.substring(name.length());
if (best == null || b.length() < best.length()) {
best = b;
}
} else if (s.length() == 0 || name.startsWith(up)) {
} else if (s.isEmpty() || StringUtils.startsWithIgnoringCase(name, query)) {
if (s.length() < name.length()) {
sentence.add(column.getName(),
column.getName().substring(s.length()),
Expand Down Expand Up @@ -326,7 +326,7 @@ private static String autoCompleteTableAlias(Sentence sentence,
return s;
}
s = s.substring(alias.length());
if (s.length() == 0) {
if (s.isEmpty()) {
sentence.add(alias + ".", ".", Sentence.CONTEXT);
}
return s;
Expand All @@ -341,15 +341,15 @@ private static String autoCompleteTableAlias(Sentence sentence,
(best == null || tableName.length() > best.length())) {
sentence.setLastMatchedTable(table);
best = tableName;
} else if (s.length() == 0 || tableName.startsWith(alias)) {
} else if (s.isEmpty() || tableName.startsWith(alias)) {
sentence.add(tableName + ".",
tableName.substring(s.length()) + ".",
Sentence.CONTEXT);
}
}
if (best != null) {
s = s.substring(best.length());
if (s.length() == 0) {
if (s.isEmpty()) {
sentence.add(alias + ".", ".", Sentence.CONTEXT);
}
return s;
Expand Down
32 changes: 32 additions & 0 deletions h2/src/main/org/h2/util/StringUtils.java
Expand Up @@ -9,12 +9,16 @@
import java.lang.ref.SoftReference;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.function.IntPredicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.h2.api.ErrorCode;
import org.h2.engine.SysProperties;
Expand Down Expand Up @@ -1383,4 +1387,32 @@ public static String escapeMetaDataPattern(String pattern) {
return replaceAll(pattern, "\\", "\\\\");
}

/**
* Case-sensitive check if a {@param text} starts with a {@param prefix}.
* It only calls {@code String.startsWith()} and is only here for API consistency
*
* @param text the full text starting with a prefix
* @param prefix the full text starting with a prefix
* @return TRUE only if text starts with the prefix
*/
public static boolean startsWith(String text, String prefix) {
return text.startsWith(prefix);
}

/**
* Case-Insensitive check if a {@param text} starts with a {@param prefix}.
*
* @param text the full text starting with a prefix
* @param prefix the full text starting with a prefix
* @return TRUE only if text starts with the prefix
*/
public static boolean startsWithIgnoringCase(String text, String prefix) {
if (text.length() < prefix.length()) {
return false;
} else {
Collator collator = Collator.getInstance();
collator.setStrength(Collator.PRIMARY);
return collator.equals(text.substring(0, prefix.length()), prefix);
}
}
}
27 changes: 26 additions & 1 deletion h2/src/test/org/h2/test/TestBase.java
Expand Up @@ -749,11 +749,24 @@ protected void assertSmaller(long a, long b) {
* @throws AssertionError if the term was not found
*/
protected void assertContains(String result, String contains) {
if (result.indexOf(contains) < 0) {
if (!result.contains(contains)) {
fail(result + " does not contain: " + contains);
}
}

/**
* Check that a result does not contain the given substring.
*
* @param result the result value
* @param shallNotContain the term that must not appear in the result
* @throws AssertionError if the term has been found
*/
protected void assertNotContaining(String result, String shallNotContain) {
if (result.contains(shallNotContain)) {
fail(result + " still contains: " + shallNotContain);
}
}

/**
* Check that a text starts with the expected characters..
*
Expand Down Expand Up @@ -852,6 +865,18 @@ public void assertNull(Object obj) {
}
}

/**
* Check that the passed String is empty.
*
* @param s the object
* @throws AssertionError if the String is not empty
*/
public void assertEmpty(String s) {
if (s != null && !s.isEmpty()) {
fail("Expected: empty String but got: " + s);
}
}

/**
* Check that the passed object is not null.
*
Expand Down
97 changes: 97 additions & 0 deletions h2/src/test/org/h2/test/server/TestWeb.java
Expand Up @@ -79,6 +79,8 @@ public void test() throws Exception {
testServer();
testWebApp();
testIfExists();

testSpecialAutoComplete();
}

private void testServlet() throws Exception {
Expand Down Expand Up @@ -561,6 +563,101 @@ private void testWebApp() throws Exception {
}
}

private void testSpecialAutoComplete() throws Exception {
Server server = new Server();
server.setOut(new PrintStream(new ByteArrayOutputStream()));
server.runTool("-ifNotExists", "-web", "-webPort", "8182",
"-properties", "null", "-tcp", "-tcpPort", "9101");
try {
String url = "http://localhost:8182";
WebClient client;
String result;
client = new WebClient();
result = client.get(url);
client.readSessionId(result);
client.get(url, "login.jsp");

result = client.get(url, "login.do?driver=org.h2.Driver" +
"&url=jdbc:h2:mem:" + getTestName() +
"&user=sa&password=sa&name=_test_");
result = client.get(url, "header.jsp");

result = client.get(url, "query.do?sql=" +
"create schema test_schema;" +
"create schema \"quoted schema\";" +
"create table test_schema.test_table(id int primary key, name varchar);" +
"insert into test_schema.test_table values(1, 'Hello');" +
"create table \"quoted schema\".\"quoted tablename\"(id int primary key, name varchar);");
result = client.get(url, "query.do?sql=create sequence test_schema.test_sequence");
result = client.get(url, "query.do?sql=" +
"create view test_schema.test_view as select * from test");
result = client.get(url, "tables.do");

result = client.get(url, "query.jsp");

// unquoted autoComplete
result = client.get(url, "autoCompleteList.do?query=select * from test_schema.test");
assertContains(StringUtils.urlDecode(result), "test_table");

// this shall succeed, because "TEST_SCHEMA" exists
result = client.get(url, "autoCompleteList.do?query=select * from TEST");
assertContains(StringUtils.urlDecode(result), "test_schema");

// this shall also succeed, because "TEST_SCHEMA" is similar
result = client.get(url, "autoCompleteList.do?query=select * from \"TEST");
assertContains(StringUtils.urlDecode(result), "test_schema");

// this shall succeed, because "TEST_SCHEMA" exists
result = client.get(url, "autoCompleteList.do?query=select * from \"TEST_SCHEMA\".test");
assertContains(StringUtils.urlDecode(result), "test_table");

// this shall succeed, because "TEST_SCHEMA" and "TEST_TABLE exist
result = client.get(url, "autoCompleteList.do?query=select * from \"TEST_SCHEMA\".\"TEST");
assertContains(StringUtils.urlDecode(result), "test_table");

// this shall also succeed, because we want to be lenient on table names
result = client.get(url, "autoCompleteList.do?query=select * from \"TEST_SCHEMA\".\"test");
assertContains(StringUtils.urlDecode(result), "test_table");

// this shall fail, because there is no "test_schema"
result = client.get(url, "autoCompleteList.do?query=select * from \"test_schema\".test");
assertNotContaining(StringUtils.urlDecode(result),"test_table");

// this shall not return any suggestion since there is no "test_schema"
result = client.get(url, "autoCompleteList.do?query=select * from \"test_schema\".");
assertEmpty(StringUtils.urlDecode(result));

// this shall not return anything, because there is no TEST_TABLE1
result = client.get(url, "autoCompleteList.do?query=select * from \"TEST_SCHEMA\".\"test_table1");
assertEmpty(StringUtils.urlDecode(result));

// explicitly quoted schemas
result = client.get(url, "autoCompleteList.do?query=select * from \"quoted");
assertContains(StringUtils.urlDecode(result),"quoted schema");

// explicitly quoted schemas, very lax
result = client.get(url, "autoCompleteList.do?query=select * from quoted");
assertContains(StringUtils.urlDecode(result),"quoted schema");

// explicitly quoted tablenames
result = client.get(url, "autoCompleteList.do?query=select * from \"quoted schema\".\"quoted");
assertContains(StringUtils.urlDecode(result),"quoted tablename");

// explicitly quoted tablename, but lax
result = client.get(url, "autoCompleteList.do?query=select * from \"quoted schema\".QUOTED");
assertContains(StringUtils.urlDecode(result),"quoted tablename");

// this one must fail
result = client.get(url, "autoCompleteList.do?query=select * from \"quoted schema\".QUOTED1");
assertNotContaining(StringUtils.urlDecode(result),"quoted tablename");

result = client.get(url, "logout.do");

} finally {
server.shutdown();
}
}

private void testStartWebServerWithConnection() throws Exception {
String old = System.getProperty(SysProperties.H2_BROWSER);
try {
Expand Down

0 comments on commit 33da479

Please sign in to comment.