Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package org.eclipse.daanse.sql.dialect.db.common;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -39,21 +40,29 @@ StringBuilder generateInlineGeneric(List<String> columnNames, List<String> colum
int columnCount = columnNames.size();
assert columnTypes.size() == columnCount;

// an inline table may carry zero rows; a single all-null row behind
// WHERE 1 = 0 keeps the SQL parseable and rowless
final boolean empty = valueList.isEmpty();
final List<String[]> rows = empty ? Collections.singletonList(new String[columnCount]) : valueList;

Integer[] maxLengths = new Integer[columnCount];
if (cast) {
fillMaxLengthsArray(maxLengths, columnTypes, valueList);
fillMaxLengthsArray(maxLengths, columnTypes, rows);
}

for (int i = 0; i < valueList.size(); i++) {
for (int i = 0; i < rows.size(); i++) {
if (i > 0) {
buf.append(" union all ");
}
String[] values = valueList.get(i);
String[] values = rows.get(i);
buf.append("select ");
formSelectFieldsForInlineGeneric(buf, values, columnTypes, columnNames, maxLengths);
if (fromClause != null) {
buf.append(fromClause);
}
if (empty) {
buf.append(" where 1 = 0");
}
}
return buf;
}
Expand All @@ -62,12 +71,16 @@ StringBuilder generateInlineGeneric(List<String> columnNames, List<String> colum
StringBuilder generateInlineForAnsi(String alias, List<String> columnNames, List<String> columnTypes,
List<String[]> valueList, boolean cast) {
final StringBuilder buf = new StringBuilder();
// an inline table may carry zero rows; VALUES with no row list does
// not parse, so emit a single all-null row behind WHERE 1 = 0
final boolean empty = valueList.isEmpty();
final List<String[]> rows = empty ? Collections.singletonList(new String[columnNames.size()]) : valueList;
buf.append("SELECT * FROM (VALUES ");
String[] castTypes = null;
if (cast) {
castTypes = getCastTypes(columnNames, columnTypes, valueList);
castTypes = getCastTypes(columnNames, columnTypes, rows);
}
formSelectFieldsForInlineForAnsi(buf, valueList, columnTypes, castTypes);
formSelectFieldsForInlineForAnsi(buf, rows, columnTypes, castTypes);
buf.append(") AS ");
dialect.quoteIdentifier(alias, buf);
buf.append(" (");
Expand All @@ -79,6 +92,9 @@ StringBuilder generateInlineForAnsi(String alias, List<String> columnNames, List
dialect.quoteIdentifier(columnName, buf);
}
buf.append(")");
if (empty) {
buf.append(" WHERE 1 = 0");
}
return buf;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ public Optional<String> generateRegularExpression(String source, String javaRege
sb.append(" IS NOT NULL AND regexp_matches(CAST(");
sb.append(source);
sb.append(" AS VARCHAR), ");
quoteStringLiteral(sb, javaRegex);
// anchored like Pattern.matches (RE2 supports \A and \z)
quoteStringLiteral(sb, "\\A(?:" + javaRegex + ")\\z");
if (mappedFlags.length() > 0) {
sb.append(", ");
quoteStringLiteral(sb, mappedFlags.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,53 @@ public class H2Dialect extends AbstractJdbcDialect {

private static final String SUPPORTED_PRODUCT_NAME = "H2";

@Override
public boolean allowsRegularExpressionInWhereClause() {
return true;
}

/**
* {@code source IS NOT NULL AND REGEXP_LIKE(source, pattern[, flags])}.
*
* <p>H2 evaluates REGEXP_LIKE with java.util.regex FIND semantics, while
* the MDX MATCHES operator is anchored ({@code Pattern.matches}) — the
* pattern is therefore wrapped in {@code \A(?:...)\z} so the SQL result
* equals the calc engine's. Embedded flags are extracted BEFORE anchoring
* (they only match at the start of the expression).
*/
@Override
public Optional<String> generateRegularExpression(String source, String javaRegex) {
try {
java.util.regex.Pattern.compile(javaRegex);
} catch (java.util.regex.PatternSyntaxException e) {
// Not a valid Java regex. Too risky to continue.
return Optional.empty();
}
javaRegex = org.eclipse.daanse.sql.dialect.db.common.DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex);
StringBuilder mappedFlags = new StringBuilder();
String[][] mapping = new String[][] { { "c", "c" }, { "i", "i" }, { "m", "m" } };
javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags);

final java.util.regex.Matcher escapeMatcher =
org.eclipse.daanse.sql.dialect.db.common.DialectUtil.ESCAPE_PATTERN.matcher(javaRegex);
while (escapeMatcher.find()) {
javaRegex = javaRegex.replace(escapeMatcher.group(1), escapeMatcher.group(2));
}
final StringBuilder sb = new StringBuilder();
sb.append(source);
sb.append(" IS NOT NULL AND ");
sb.append("REGEXP_LIKE(");
sb.append(source);
sb.append(", ");
quoteStringLiteral(sb, "\\A(?:" + javaRegex + ")\\z");
if (mappedFlags.length() > 0) {
sb.append(", ");
quoteStringLiteral(sb, mappedFlags.toString());
}
sb.append(")");
return Optional.of(sb.toString());
}

/**
* {@code ANALYZE}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,41 @@ void testGetDialectName() {
assertEquals("h2", dialect.name());
}

@Nested
@DisplayName("Regular Expression Tests")
class RegularExpressionTests {

@Test
void testAllowsRegularExpressionInWhereClause() {
assertTrue(dialect.allowsRegularExpressionInWhereClause());
}

@Test
void testGenerateRegularExpression_InvalidRegex() {
assertTrue(dialect.regexGenerator().generateRegularExpression("table.column", "(a").isEmpty(),
"Invalid regex should be ignored");
}

@Test
void testGenerateRegularExpression_CaseInsensitive() {
String sql = dialect.regexGenerator()
.generateRegularExpression("table.column", "(?i)|(?u).*a.*").orElseThrow();
// pattern anchored: MDX MATCHES is Pattern.matches, H2 REGEXP_LIKE finds
assertEquals(
"table.column IS NOT NULL AND REGEXP_LIKE(table.column, '\\A(?:.*a.*)\\z', 'i')",
sql);
}

@Test
void testGenerateRegularExpression_CaseSensitive() {
String sql = dialect.regexGenerator()
.generateRegularExpression("table.column", ".*a.*").orElseThrow();
assertEquals(
"table.column IS NOT NULL AND REGEXP_LIKE(table.column, '\\A(?:.*a.*)\\z')",
sql);
}
}

@Nested
@DisplayName("Bit Aggregation Tests")
class BitAggregationTests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,12 @@ public Optional<String> generateRegularExpression(String source, String javaRege
sb.append(")");
}
sb.append(" REGEXP ");
// MDX MATCHES is anchored (Pattern.matches); REGEXP is contains —
// anchor AFTER the case fold, or the fold would turn \z into \Z
if (caseSensitive) {
quoteStringLiteral(sb, javaRegex);
quoteStringLiteral(sb, "\\A(?:" + javaRegex + ")\\z");
} else {
quoteStringLiteral(sb, javaRegex.toUpperCase());
quoteStringLiteral(sb, "\\A(?:" + javaRegex.toUpperCase() + ")\\z");
}
return Optional.of(sb.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ void testGenerateRegularExpression_CaseInsensitive() {
assertTrue(sql.contains("UPPER"));
assertTrue(sql.contains("cast(table.column as string)"));
assertTrue(sql.contains("REGEXP"));
assertTrue(sql.contains("'.*A.*'"));
assertTrue(sql.contains("'\\A(?:.*A.*)\\z'"));
}

@Test
Expand All @@ -66,7 +66,7 @@ void testGenerateRegularExpression_CaseSensitive() {
assertFalse(sql.contains("UPPER"));
assertTrue(sql.contains("cast(table.column as string)"));
assertTrue(sql.contains("REGEXP"));
assertTrue(sql.contains("'.*1.*'"));
assertTrue(sql.contains("'\\A(?:.*1.*)\\z'"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,12 @@ public Optional<String> generateRegularExpression(String source, String javaRege
sb.append(")");
}
sb.append(" REGEXP ");
// MDX MATCHES is anchored (Pattern.matches); REGEXP is contains —
// anchor AFTER the case fold, or the fold would turn \z into \Z
if (caseSensitive) {
quoteStringLiteral(sb, javaRegex);
quoteStringLiteral(sb, "\\A(?:" + javaRegex + ")\\z");
} else {
quoteStringLiteral(sb, javaRegex.toUpperCase());
quoteStringLiteral(sb, "\\A(?:" + javaRegex.toUpperCase() + ")\\z");
}
return Optional.of(sb.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ void testGenerateRegularExpression_InvalidRegex() throws Exception {
void testGenerateRegularExpression_CaseInsensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get()
.toString();
assertEquals("table.column IS NOT NULL AND UPPER(table.column) REGEXP '.*A.*'", sql);
assertEquals("table.column IS NOT NULL AND UPPER(table.column) REGEXP '\\\\A(?:.*A.*)\\\\z'", sql);
}

@Test
void testGenerateRegularExpression_CaseSensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString();
assertEquals("table.column IS NOT NULL AND table.column REGEXP '.*a.*'", sql);
assertEquals("table.column IS NOT NULL AND table.column REGEXP '\\\\A(?:.*a.*)\\\\z'", sql);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ public Optional<String> generateRegularExpression(String source, String javaRege
StringBuilder mappedFlags = new StringBuilder();
String[][] mapping = new String[][] { { "c", "c" }, { "i", "i" }, { "m", "m" } };
javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags);
if (mappedFlags.toString().contains("m")) {
// MDX MATCHES is anchored over the WHOLE value; Oracle's POSIX
// regex has no \A/\z, and under the m flag ^/$ anchor per
// line - no correct rendering, the calc engine takes over
return Optional.empty();
}

final Matcher escapeMatcher = DialectUtil.ESCAPE_PATTERN.matcher(javaRegex);
while (escapeMatcher.find()) {
Expand All @@ -295,7 +301,8 @@ public Optional<String> generateRegularExpression(String source, String javaRege
sb.append("REGEXP_LIKE(");
sb.append(source);
sb.append(", ");
quoteStringLiteral(sb, javaRegex);
// anchored like Pattern.matches; POSIX has no non-capturing group
quoteStringLiteral(sb, "^(" + javaRegex + ")$");
sb.append(", ");
quoteStringLiteral(sb, mappedFlags.toString());
sb.append(")");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ void testGenerateRegularExpression_InvalidRegex() throws Exception {
void testGenerateRegularExpression_CaseInsensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get()
.toString();
assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '.*a.*', 'i')", sql);
assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '^(.*a.*)$', 'i')", sql);
}

@Test
void testGenerateRegularExpression_MultilineFlag() throws Exception {
assertTrue(dialect.regexGenerator().generateRegularExpression("table.column", "(?m).*a.*").isEmpty(),
"under (?m) Oracle's ^/$ anchor per line, not per value - no correct rendering");
}

@Test
void testGenerateRegularExpression_CaseSensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString();
assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '.*a.*', '')", sql);
assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '^(.*a.*)$', '')", sql);
}
}
//End OracleDialectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.sql.Types;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

Expand All @@ -46,6 +47,8 @@
*/
public class PostgreSqlDialect extends AbstractJdbcDialect {

private static final Pattern LEADING_EMBEDDED_OPTIONS = Pattern.compile("^(?:\\(\\?[a-zA-Z-]+\\))+");

// Lazy-initialized generator caches — built on first call. Stateless
// implementations that capture the dialect's quoter / version, so safe
// to memoize for the dialect's lifetime.
Expand Down Expand Up @@ -290,7 +293,17 @@ public Optional<String> generateRegularExpression(String source, String javaRege
sb.append("cast(");
sb.append(source);
sb.append(" as text) ~ ");
quoteStringLiteral(sb, javaRegex);
// anchored like Pattern.matches; \A/\Z stay string anchors even
// under newline-sensitive matching ((?m) maps to Postgres' n mode).
// Postgres allows embedded options only at the very start of an ARE,
// so a leading (?i)-style block goes in front of the anchor.
String options = "";
final Matcher optionsMatcher = LEADING_EMBEDDED_OPTIONS.matcher(javaRegex);
if (optionsMatcher.find()) {
options = optionsMatcher.group();
javaRegex = javaRegex.substring(optionsMatcher.end());
}
quoteStringLiteral(sb, options + "\\A(?:" + javaRegex + ")\\Z");
return Optional.of(sb.toString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ void testGenerateRegularExpression_InvalidRegex() throws Exception {
void testGenerateRegularExpression_CaseInsensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get()
.toString();
assertEquals("cast(table.column as text) is not null and cast(table.column as text) ~ '(?i).*a.*'", sql);
assertEquals(
"cast(table.column as text) is not null and cast(table.column as text) ~ '(?i)\\A(?:.*a.*)\\Z'",
sql);
}

@Test
void testGenerateRegularExpression_CaseSensitive() throws Exception {
String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString();
assertEquals("cast(table.column as text) is not null and cast(table.column as text) ~ '.*a.*'", sql);
assertEquals("cast(table.column as text) is not null and cast(table.column as text) ~ '\\A(?:.*a.*)\\Z'",
sql);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ private static JdbcMetaData calculateMetaData(String currentCatalogName, String
for (DatabaseSchema schema : databaseCatalog.getDatabaseSchemas()) {
for (DatabaseTable table : schema.getDatabaseTables()) {

List<JdbcColumn> jdbcColumns = table.getDatabaseColumns().parallelStream()
List<JdbcColumn> jdbcColumns = table.getDatabaseColumns().stream()
.map(c -> new JdbcColumn(c.getName())).toList();
jdbcMetaData.addTable(schema.getName(), table.getName(), jdbcColumns);
}
Expand Down
1 change: 0 additions & 1 deletion jdbc/impl/test.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
org.mockito.mockito-core;version='[4.9.0,4.9.1)',\
org.objenesis;version='[3.3.0,3.3.1)',\
org.opentest4j;version='[1.3.0,1.3.1)',\
org.osgi.service.cm;version='[1.6.1,1.6.2)',\
org.osgi.service.component;version='[1.5.1,1.5.2)',\
org.osgi.test.common;version='[1.3.0,1.3.1)',\
org.osgi.test.junit5;version='[1.3.0,1.3.1)',\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ private void loadTable(Connection connection, Path path) throws SQLException {

String statementCreateSchema = dialect.ddlGenerator().createSchema(s.name(), true);

try {
connection.createStatement().execute(statementCreateSchema);
try (Statement stmt = connection.createStatement()) {
stmt.execute(statementCreateSchema);
} catch (SQLException e) {
// https://github.com/h2database/h2database/issues/4188
// throw new CsvDataImporterException(EXCEPTION_WHILE_CREATING_SCHEMA, e);
Expand Down Expand Up @@ -279,14 +279,14 @@ private void batchExecute(Connection connection, PreparedStatement ps, Closeable
}
ps.addBatch();
ps.clearParameters();
count++;
if (count % config.batchSize() == 0) {
ps.executeBatch();
LOGGER.debug("execute batch time {}", (System.currentTimeMillis() - start));
ps.getConnection().commit();
LOGGER.debug("execute commit time {}", (System.currentTimeMillis() - start));
start = System.currentTimeMillis();
}
count++;
}

ps.executeBatch();
Expand Down Expand Up @@ -450,7 +450,7 @@ private void delete(Path path) {

@Override
public void handleInitialPaths(List<Path> initialPaths) {
initialPaths.parallelStream().forEach(this::checkPathAndLoadCsv);
initialPaths.forEach(this::checkPathAndLoadCsv);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@
* @return Batch Size. Use Batch operation if dialect support it
*/
@AttributeDefinition(description = "batchSize", defaultValue = "5000")
int batchSize() default 1000;
int batchSize() default 5000;
}
Loading