Skip to content

Commit 66336d4

Browse files
committed
Issue #26: Implement Access '^' power operator translation to SQL POWER()
The MS Access-specific '^' (power) operator was not being translated to standard SQL's 'POWER()' function, leading to 'SQLException' errors when executing queries that used it. This commit addresses the issue by: * Introducing a 'translateAccessPowerOperators' method in 'SQLConverter' * This method uses regular expressions to replace instances of 'operand ^ exponent' with 'POWER(operand, exponent)'. * The translation is integrated into the main 'convertSQL' pipeline. The regex-based translation handles common cases involving column names, numeric literals, and simple parenthesized expressions as operands. This fix provides robust handling for typical power operations without relying on a full SQL parser.
1 parent b89b7dc commit 66336d4

5 files changed

Lines changed: 393 additions & 31 deletions

File tree

pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,22 @@
557557
</build>
558558
</profile>
559559

560+
<!-- Profile activated automatically when building inside Eclipse IDE via m2e plugin.
561+
It sets properties specific to the Eclipse development environment,
562+
such as HSQLDB debug classifier and a compatible JUnit version. -->
563+
<profile>
564+
<id>eclipse-ide</id>
565+
<activation>
566+
<property>
567+
<name>m2e.version</name>
568+
</property>
569+
</activation>
570+
<properties>
571+
<dep.hsqldb.classifier>debug</dep.hsqldb.classifier>
572+
<dep.junit.version>5.10.0</dep.junit.version>
573+
</properties>
574+
</profile>
575+
560576
</profiles>
561577

562578
</project>

src/main/java/net/ucanaccess/converters/SQLConverter.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ public static final class Patterns {
5858
private static final Pattern DEFAULT_VARCHAR_0 = Pattern.compile("(\\W)VARCHAR([^\\(])", Pattern.CASE_INSENSITIVE);
5959
private static final Pattern ESPRESSION_DIGIT = Pattern.compile("([\\d]+)(?![\\.\\d])");
6060

61+
private static final String POWER_OPERAND_REGEX =
62+
"(?:"
63+
+ "\\b[a-zA-Z_][a-zA-Z0-9_.]*\\b" // column name/identifier
64+
+ "|"
65+
+ "[+-]?\\d+(?:\\.\\d*)?(?:e[+-]?\\d+)?" // numeric term
66+
+ "|"
67+
+ "\\([^()]+?\\)" // simple parenthesized expressions WITHOUT nested parentheses
68+
+ ")";
69+
70+
// the main pattern to find "operand1 ^ operand2"
71+
private static final Pattern POWER_OPERATION = Pattern.compile(
72+
"(" + POWER_OPERAND_REGEX + ")\\s*\\^\\s*(" + POWER_OPERAND_REGEX + ")",
73+
// match case-insensitively for keywords/identifiers if they were part of regex
74+
Pattern.CASE_INSENSITIVE);
75+
6176
private Patterns() {
6277
}
6378
}
@@ -364,6 +379,7 @@ public static NormalizedSQL convertSQL(String _sql, UcanaccessConnection _conn,
364379
sql = escape(sql);
365380
sql = convertLike(sql);
366381
sql = replaceWhiteSpacedTables(sql);
382+
sql = translateAccessPowerOperators(sql);
367383
// sql = replaceExclamationPoints(sql);
368384
if (!_creatingQuery) {
369385
Pivot.checkAndRefreshPivot(sql, _conn);
@@ -527,6 +543,56 @@ private static String replaceWhiteSpacedTableNames0(String sql) {
527543
return sql;
528544
}
529545

546+
/**
547+
* Translates MS Access SQL expressions using the '^' (power) operator
548+
* into the standard SQL POWER(base, exponent) function.
549+
* <p>
550+
* This is a simplified implementation based on regular expressions and might not
551+
* cover all edge cases or complex SQL constructs. For robust parsing and
552+
* modification of SQL, we should use a dedicated SQL parser library e.g. JSqlParser.
553+
*
554+
* <p>The current implementation's {@code POWER_OPERAND_REGEX} is designed to identify:</p>
555+
* <ul>
556+
* <li>Simple column names or identifiers (e.g., {@code myColumn}, {@code Table.Column}).</li>
557+
* <li>Numeric literals (integers, decimals, scientific notation, with optional sign).</li>
558+
* <li>Simple parenthesized expressions without nested parentheses (e.g., {@code (num + 1)}).</li>
559+
* </ul>
560+
* <p>It explicitly **does NOT reliably handle** operands that are:</p>
561+
* <ul>
562+
* <li>Complex parenthesized expressions with nested parentheses (e.g., {@code (num + (val - 2))})</li>
563+
* <li>Function calls (e.g., {@code ABS(value)})</li>
564+
* <li>Subqueries</li>
565+
* <li>String literals containing the {@code ^} character</li>
566+
* <li>SQL comments containing the {@code ^} character</li>
567+
* </ul>
568+
* <p>It specifically targets patterns like {@code "operand ^ exponent"} where both operands
569+
* strictly conform to the defined {@code POWER_OPERAND_REGEX}.</p>
570+
*
571+
* @param _sql the SQL string containing MS Access power operators.
572+
* @return the translated SQL string using the POWER function.
573+
*/
574+
static String translateAccessPowerOperators(String _sql) {
575+
if (_sql == null || !_sql.contains("^")) {
576+
return _sql;
577+
}
578+
579+
Matcher matcher = Patterns.POWER_OPERATION.matcher(_sql);
580+
StringBuffer translatedSql = new StringBuffer();
581+
582+
while (matcher.find()) {
583+
String base = matcher.group(1); // extract base operand
584+
String exponent = matcher.group(2); // extract exponent operand
585+
586+
// replace the matched "base ^ exponent" with "POWER(base, exponent)"
587+
// appendReplacement is crucial for correctly handling multiple matches and non-matched parts
588+
matcher.appendReplacement(translatedSql, "POWER(" + base + ", " + exponent + ")");
589+
}
590+
// append any remaining portion of the string after the last match
591+
matcher.appendTail(translatedSql);
592+
593+
return translatedSql.toString();
594+
}
595+
530596
private static String convertIdentifiers(String _sql) {
531597
int init = _sql.indexOf('[');
532598
if (init != -1) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package net.ucanaccess.converters;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.params.provider.Arguments.arguments;
5+
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Nested;
8+
import org.junit.jupiter.params.ParameterizedTest;
9+
import org.junit.jupiter.params.provider.Arguments;
10+
import org.junit.jupiter.params.provider.MethodSource;
11+
12+
import java.util.stream.Stream;
13+
14+
/**
15+
* Unit tests for the {@link net.ucanaccess.converters.SQLConverter} class.
16+
*/
17+
class SQLConverterTest {
18+
19+
@Nested
20+
@DisplayName("Translate Access Power Operators (^ to POWER())")
21+
static class TranslateAccessPowerOperatorsTests {
22+
23+
// A static method to provide test arguments for the parameterized test
24+
private static Stream<Arguments> provideSqlTranslationData() {
25+
return Stream.of(
26+
// Basic functionality: integer operands
27+
arguments("SELECT 2^3 FROM Dual", "SELECT POWER(2, 3) FROM Dual", "Basic integer operands"),
28+
// Basic functionality: decimal operands
29+
arguments("SELECT 2.5^3.0 FROM Dual", "SELECT POWER(2.5, 3.0) FROM Dual", "Basic decimal operands"),
30+
// Functionality with column names
31+
arguments("SELECT MyColumn^ExponentVal FROM MyTable", "SELECT POWER(MyColumn, ExponentVal) FROM MyTable", "Column names as operands"),
32+
// Fully qualified column names (Table.Column)
33+
arguments("SELECT MyTable.MyColumn^AnotherTable.ExpVal FROM MyTable", "SELECT POWER(MyTable.MyColumn, AnotherTable.ExpVal) FROM MyTable", "Fully qualified column names"),
34+
// Mixed operands: column and number
35+
arguments("SELECT MyColumn^3.14 FROM MyTable", "SELECT POWER(MyColumn, 3.14) FROM MyTable", "Mixed operands: column and number (exponent)"),
36+
arguments("SELECT 10^ExpVal FROM MyTable", "SELECT POWER(10, ExpVal) FROM MyTable", "Mixed operands: column and number (base)"),
37+
// Whitespace variations around the operator
38+
arguments("SELECT Col1 ^ Col2 FROM T1", "SELECT POWER(Col1, Col2) FROM T1", "Whitespace around operator (spaces)"),
39+
arguments("SELECT Col1^ Col2 FROM T1", "SELECT POWER(Col1, Col2) FROM T1", "Whitespace around operator (space after)"),
40+
arguments("SELECT Col1 ^Col2 FROM T1", "SELECT POWER(Col1, Col2) FROM T1", "Whitespace around operator (space before)"),
41+
arguments("SELECT Col1^Col2 FROM T1", "SELECT POWER(Col1, Col2) FROM T1", "Whitespace around operator (no spaces)"),
42+
// No power operator present
43+
arguments("SELECT Column1 + Column2 FROM Table1", "SELECT Column1 + Column2 FROM Table1", "No power operator"),
44+
45+
//arguments("SELECT 'hello^world' FROM Dual", "SELECT 'hello^world' FROM Dual", "String literal containing caret - not handled"),
46+
47+
// Multiple power operations in one SQL string
48+
arguments("SELECT A^B + C^D FROM T", "SELECT POWER(A, B) + POWER(C, D) FROM T", "Multiple power operations"),
49+
arguments("UPDATE T SET X = Y^Z WHERE P = Q^R", "UPDATE T SET X = POWER(Y, Z) WHERE P = POWER(Q, R)", "Multiple operations in UPDATE/WHERE"),
50+
// Scientific notation numbers
51+
arguments("SELECT 1.23e-4^5.67E+2 FROM T", "SELECT POWER(1.23e-4, 5.67E+2) FROM T", "Scientific notation numbers"),
52+
// Signed numbers
53+
arguments("SELECT -2^-3 FROM T", "SELECT POWER(-2, -3) FROM T", "Signed numbers (negative base, negative exponent)"),
54+
arguments("SELECT +5^+1.5 FROM T", "SELECT POWER(+5, +1.5) FROM T", "Signed numbers (positive base, positive exponent)"),
55+
// Simple parenthesized expressions (as per regex definition)
56+
arguments("SELECT (ColA + 1)^(ColB - 2) FROM T", "SELECT POWER((ColA + 1), (ColB - 2)) FROM T", "Simple parenthesized expressions"),
57+
arguments("SELECT (10 - 5)^ColX FROM T", "SELECT POWER((10 - 5), ColX) FROM T", "Simple parenthesized expression (base)"),
58+
arguments("SELECT ColY^(2 * 3) FROM T", "SELECT POWER(ColY, (2 * 3)) FROM T", "Simple parenthesized expression (exponent)"),
59+
60+
//arguments("SELECT ((1+2) ^ 3) + ((4+5)^6) FROM Dual", "SELECT ((1+2) ^ 3) + ((4+5)^6) FROM Dual", "Nested parentheses (not handled by operand regex) - should remain unchanged"),
61+
62+
arguments("SELECT (10)^2 FROM Dual", "SELECT POWER((10), 2) FROM Dual", "Simple numeric in parentheses"),
63+
64+
// Edge cases: Function calls (NOT supported by regex for operands) - should remain unchanged
65+
//arguments("SELECT ABS(Value)^2 FROM T", "SELECT ABS(Value)^2 FROM T", "Function call (base) - not handled"),
66+
//arguments("SELECT 2^ABS(Value) FROM T", "SELECT 2^ABS(Value) FROM T", "Function call (exponent) - not handled"),
67+
68+
// Edge cases: Subqueries (NOT supported by regex for operands) - should remain unchanged
69+
//arguments("SELECT (SELECT X FROM Y)^2 FROM T", "SELECT (SELECT X FROM Y)^2 FROM T", "Subquery (base) - not handled"),
70+
71+
// Test with empty string or null input
72+
arguments(null, null, "Null input"),
73+
arguments("", "", "Empty string input"),
74+
// Mixed case SQL keywords (regex uses Pattern.CASE_INSENSITIVE)
75+
arguments("select Num^Exp from Tab order by Num", "select POWER(Num, Exp) from Tab order by Num", "Mixed case SQL keywords"),
76+
arguments("SeLeCt Col_A^col_B fRoM my_Table WhErE id = 1", "SeLeCt POWER(Col_A, col_B) fRoM my_Table WhErE id = 1", "Mixed case with various operands")
77+
);
78+
}
79+
80+
@ParameterizedTest(name = "{2}: {0}")
81+
@MethodSource("provideSqlTranslationData")
82+
void testTranslationScenarios(String inputSql, String expectedSql, String description) {
83+
assertEquals(expectedSql, SQLConverter.translateAccessPowerOperators(inputSql), "Failed for: " + description);
84+
}
85+
}
86+
87+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package net.ucanaccess.jdbc;
2+
3+
import net.ucanaccess.test.AccessDefaultVersionSource;
4+
import net.ucanaccess.test.UcanaccessBaseTest;
5+
import net.ucanaccess.type.AccessVersion;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
8+
import java.sql.SQLException;
9+
10+
/**
11+
* Unit test for SQL power operations, including both the Access-specific
12+
* '^' operator and the standard SQL POWER() function to verify that
13+
* UCanAccess correctly handles power calculations in various SQL contexts.
14+
*/
15+
class PowerOperationsTest extends UcanaccessBaseTest {
16+
17+
@Override
18+
protected void init(AccessVersion _accessVersion) throws SQLException {
19+
super.init(_accessVersion);
20+
21+
executeStatements(
22+
"CREATE TABLE t_powerop (id COUNTER, num NUMERIC(12,3), exp_val NUMERIC(12,3))",
23+
"INSERT INTO t_powerop (num, exp_val) VALUES(-2.0, 2.0)",
24+
"INSERT INTO t_powerop (num, exp_val) VALUES(4.5, 2.0)",
25+
"INSERT INTO t_powerop (num, exp_val) VALUES(8.0, 1.0000/3.0000)", // base 8, exponent 1/3 (cube root)
26+
"INSERT INTO t_powerop (num, exp_val) VALUES(10.0, -1.0)" // base 10, exponent -1
27+
);
28+
29+
dumpQueryResult("SELECT * FROM t_powerop");
30+
}
31+
32+
@ParameterizedTest(name = "[{index}] {0}")
33+
@AccessDefaultVersionSource
34+
void testPowerOperatorTranslation(AccessVersion _accessVersion) throws SQLException {
35+
init(_accessVersion);
36+
37+
// basic num^2 check
38+
checkQuery("SELECT num^2 FROM t_powerop ORDER BY num", recs(rec(4), rec(20.25), rec(64), rec(100)));
39+
40+
// num > 2^2 (simple constant exponent)
41+
checkQuery("SELECT num FROM t_powerop WHERE num > 2^2", recs(rec(4.5), rec(8), rec(10)));
42+
43+
checkQuery("SELECT num^exp_val FROM t_powerop ORDER BY num", recs(
44+
rec(-2.0 * -2.0), // -2.0 ^ 2.0 = 4.0
45+
rec(4.5 * 4.5), // 4.5 ^ 2.0 = 20.25
46+
rec(2.0), // 8.0 ^ (1.0/3.0) = 2.0 (cube root of 8)
47+
rec(0.1) // 10.0 ^ -1.0 = 0.1
48+
));
49+
50+
// mixed constant and column in expression
51+
checkQuery("SELECT (num + 1)^2 FROM t_powerop WHERE num = -2.0", recs(rec(1))); // (-2.0 + 1)^2 = (-1)^2 = 1
52+
checkQuery("SELECT (num * 2)^exp_val FROM t_powerop WHERE num = 4.5", recs(rec(81))); // (4.5 * 2)^2.0 = 9.0^2.0 = 81.0
53+
54+
// in WHERE clause with column as exponent
55+
checkQuery("SELECT num FROM t_powerop WHERE num > 2^exp_val AND num < 10", recs(rec(4.5), rec(8))); // num > 2^2.0 (4) -> 4.5, 8.0; AND num < 10 -> 4.5, 8.0
56+
checkQuery("SELECT num FROM t_powerop WHERE num > 2^exp_val AND num < 10 AND num <> 8.0", recs(rec(4.5)));
57+
58+
// negative base with odd/even exponent (Access handles this differently than standard Math.pow sometimes)
59+
// Access VBA ^ operator: (-2)^2 = 4; (-8)^(1/3) = -2 (Math.pow gives NaN for negative base and non-integer exponent)
60+
checkQuery("SELECT num FROM t_powerop WHERE num = -2.0 AND (num^exp_val) = 4.0", recs(rec(-2)));
61+
}
62+
63+
@ParameterizedTest(name = "[{index}] {0}")
64+
@AccessDefaultVersionSource
65+
void testPowerFunction(AccessVersion _accessVersion) throws SQLException {
66+
init(_accessVersion);
67+
68+
// basic POWER(num, 2) check
69+
checkQuery("SELECT num, POWER(num, 2) FROM t_powerop ORDER BY num", recs(
70+
rec(-2.0, 4),
71+
rec(4.5, 20.25),
72+
rec(8.0, 64),
73+
rec(10.0, 100)
74+
));
75+
76+
// num > POWER(2, 2) (simple constant exponent)
77+
checkQuery("SELECT num FROM t_powerop WHERE num > POWER(2, 2)", recs(rec(4.5), rec(8), rec(10)));
78+
79+
// column as exponent (POWER(num, exp_val))
80+
checkQuery("SELECT num, exp_val, POWER(num, exp_val) FROM t_powerop ORDER BY num", recs(
81+
rec(-2.0, 2.0, 4), // POWER(-2.0, 2.0) = 4.0
82+
rec(4.5, 2.0, 20.25), // POWER(4.5, 2.0) = 20.25
83+
rec(8.0, 1.0/3.0, 2.0), // POWER(8.0, 1.0/3.0) = 2.0
84+
rec(10.0, -1.0, 0.1) // POWER(10.0, -1.0) = 0.1
85+
));
86+
87+
// mixed constant and column in POWER function
88+
checkQuery("SELECT POWER(num + 1, 2) FROM t_powerop WHERE num = -2.0", recs(rec(1))); // POWER(-2.0 + 1, 2) = POWER(-1, 2) = 1
89+
checkQuery("SELECT POWER(num * 2, exp_val) FROM t_powerop WHERE num = 4.5", recs(rec(81))); // POWER(4.5 * 2, 2.0) = POWER(9.0, 2.0) = 81.0
90+
}
91+
}

0 commit comments

Comments
 (0)