Skip to content

Commit 124ea10

Browse files
committed
feat: add support for HSQLDB dialect
1 parent 41cdbbe commit 124ea10

File tree

13 files changed

+1417
-1
lines changed

13 files changed

+1417
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
## [1.7.0] - 2025-05-??
2727

2828
### Added
29-
- Supporting SQL dialect of SQLite.
29+
- Supporting SQL dialects of SQLite and HSQLDB.
3030

3131
## [1.6.0] - 2025-05-16
3232

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ The example assumes a connection to a MySQL database.
105105
* PostgreSQL
106106
* MS Access
107107
* SQLite
108+
* HSQLDB
108109
* H2
109110

110111
### Supported SQL Features

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
<ucanaccess.version>5.1.3</ucanaccess.version>
8888
<mybatis.version>3.5.19</mybatis.version>
8989
<sqlite-jdbc.version>3.46.1.2</sqlite-jdbc.version>
90+
<hsqldb.version>2.7.4</hsqldb.version>
9091
</properties>
9192

9293
<dependencies>
@@ -227,6 +228,12 @@
227228
<version>${sqlite-jdbc.version}</version>
228229
<scope>test</scope>
229230
</dependency>
231+
<dependency>
232+
<groupId>org.hsqldb</groupId>
233+
<artifactId>hsqldb</artifactId>
234+
<version>${hsqldb.version}</version>
235+
<scope>test</scope>
236+
</dependency>
230237
</dependencies>
231238

232239
<build>

src/main/java/io/github/torand/fastersql/dialect/Dialect.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ static Dialect fromConnection(Connection connection) {
160160
return new AccessDialect();
161161
} else if (productName.contains("sqlite")) {
162162
return new SqliteDialect();
163+
} else if (productName.contains("hsql")) {
164+
return new HsqldbDialect();
163165
} else {
164166
throw new UnsupportedOperationException("Database with product name " + productName + " not supported");
165167
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2024-2025 Tore Eide Andersen
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.torand.fastersql.dialect;
17+
18+
import java.util.EnumSet;
19+
import java.util.List;
20+
import java.util.Optional;
21+
22+
import static io.github.torand.fastersql.dialect.Capability.CONCAT_OPERATOR;
23+
import static io.github.torand.fastersql.dialect.Capability.CURRENT_TIME;
24+
import static io.github.torand.fastersql.dialect.Capability.LIMIT_OFFSET;
25+
import static io.github.torand.fastersql.dialect.Capability.NULL_ORDERING;
26+
import static io.github.torand.fastersql.dialect.Capability.SELECT_FOR_UPDATE;
27+
import static io.github.torand.fastersql.dialect.Capability.TRUNCATE_TABLE;
28+
29+
/**
30+
* Defines the HyperSQL (HSQLDB) SQL dialect.
31+
*
32+
* <a href="https://hsqldb.org/doc/2.0/guide/sqlgeneral-chapt.html" />
33+
*/
34+
public class HsqldbDialect implements Dialect {
35+
private static final EnumSet<Capability> SUPPORTED_CAPS = EnumSet.of(LIMIT_OFFSET, CONCAT_OPERATOR, CURRENT_TIME, NULL_ORDERING, SELECT_FOR_UPDATE, TRUNCATE_TABLE);
36+
37+
@Override
38+
public String getProductName() {
39+
return "HyperSQL/HSQLDB";
40+
}
41+
42+
@Override
43+
public boolean offsetBeforeLimit() {
44+
return false;
45+
}
46+
47+
@Override
48+
public Optional<String> formatRowOffsetClause() {
49+
return Optional.of("offset ?");
50+
}
51+
52+
@Override
53+
public Optional<String> formatRowLimitClause() {
54+
return Optional.of("limit ?");
55+
}
56+
57+
@Override
58+
public Optional<String> formatRowNumLiteral() {
59+
return Optional.of("rownum()");
60+
}
61+
62+
@Override
63+
public String formatToNumberFunction(String operand, int precision, int scale) {
64+
return "to_number(" + operand + ")";
65+
}
66+
67+
@Override
68+
public String formatToCharFunction(String operand, String format) {
69+
return "to_char(" + operand + ", " + format + ")";
70+
}
71+
72+
@Override
73+
public String formatSubstringFunction(String operand, int startPos, int length) {
74+
return "substr(" + operand + ", " + startPos + ", " + length + ")";
75+
}
76+
77+
@Override
78+
public String formatConcatFunction(List<String> operands) {
79+
// Note! The concat infix operator is used in output SQL
80+
return "concat(%s)".formatted(String.join(", ", operands));
81+
}
82+
83+
@Override
84+
public String formatLengthFunction(String operand) {
85+
return "char_length(" + operand + ")";
86+
}
87+
88+
@Override
89+
public String formatCeilFunction(String operand) {
90+
return "ceil(" + operand + ")";
91+
}
92+
93+
@Override
94+
public String formatRoundFunction(String operand) {
95+
return "round(" + operand + ")";
96+
}
97+
98+
@Override
99+
public String formatModuloFunction(String divisor, String dividend) {
100+
return "mod(" + divisor + ", " + dividend + ")";
101+
}
102+
103+
@Override
104+
public String formatCurrentDateFunction() {
105+
return "current_date";
106+
}
107+
108+
@Override
109+
public Optional<String> getConcatOperator() {
110+
return Optional.of("||");
111+
}
112+
113+
@Override
114+
public boolean supports(Capability capability) {
115+
return SUPPORTED_CAPS.contains(capability);
116+
}
117+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (c) 2024-2025 Tore Eide Andersen
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.torand.fastersql.statement.hsqldb;
17+
18+
import io.github.torand.fastersql.statement.PreparableStatement;
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.util.UUID;
22+
23+
import static io.github.torand.fastersql.datamodel.DataModel.PRODUCT;
24+
import static io.github.torand.fastersql.function.singlerow.SingleRowFunctions.length;
25+
import static io.github.torand.fastersql.statement.Statements.delete;
26+
import static io.github.torand.fastersql.statement.Statements.select;
27+
28+
public class HsqldbDeleteStatementTest extends HsqldbTest {
29+
30+
@Test
31+
void shouldRemoveDeletedRow() {
32+
final UUID id = UUID.fromString("dba9f942-c24f-4b6a-89b6-881236ff5438"); // Apple iPad
33+
34+
PreparableStatement stmt =
35+
delete().from(PRODUCT)
36+
.where(PRODUCT.ID.eq(id));
37+
38+
statementTester()
39+
.assertSql("""
40+
delete from PRODUCT \
41+
where ID = ?"""
42+
)
43+
.assertParams(id)
44+
.assertAffectedRowCount(1)
45+
.verify(stmt);
46+
47+
statementTester()
48+
.assertRowCount(0)
49+
.verify(
50+
select(PRODUCT.ID)
51+
.from(PRODUCT)
52+
.where(PRODUCT.ID.eq(id))
53+
);
54+
}
55+
56+
@Test
57+
void shouldHandleOptionalPredicates() {
58+
final UUID id = UUID.fromString("7a4b3e96-afee-4284-8ccd-f7461bcd602b"); // Samsung Galaxy
59+
60+
PreparableStatement stmt =
61+
delete().from(PRODUCT)
62+
.where(PRODUCT.ID.eq(id))
63+
.whereIf(true, () -> length(PRODUCT.NAME).gt(10));
64+
65+
statementTester()
66+
.assertSql("""
67+
delete from PRODUCT \
68+
where ID = ? \
69+
and char_length(NAME) > ?"""
70+
)
71+
.assertParams(id, 10)
72+
.assertAffectedRowCount(1)
73+
.verify(stmt);
74+
75+
statementTester()
76+
.assertRowCount(0)
77+
.verify(
78+
select(PRODUCT.ID)
79+
.from(PRODUCT)
80+
.where(PRODUCT.ID.eq(id))
81+
);
82+
}
83+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright (c) 2024-2025 Tore Eide Andersen
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.torand.fastersql.statement.hsqldb;
17+
18+
import io.github.torand.fastersql.domainmodel.Product;
19+
import io.github.torand.fastersql.domainmodel.ProductCategory;
20+
import io.github.torand.fastersql.statement.PreparableStatement;
21+
import org.junit.jupiter.api.Test;
22+
23+
import java.util.Collection;
24+
import java.util.UUID;
25+
26+
import static io.github.torand.fastersql.datamodel.DataModel.PRODUCT;
27+
import static io.github.torand.fastersql.statement.Statements.insertBatch;
28+
import static io.github.torand.fastersql.statement.Statements.select;
29+
import static io.github.torand.fastersql.util.RowValueMatchers.isBigDecimal;
30+
import static io.github.torand.fastersql.util.RowValueMatchers.isNull;
31+
import static java.util.Arrays.asList;
32+
import static org.hamcrest.Matchers.is;
33+
34+
public class HsqldbInsertBatchStatementTest extends HsqldbTest {
35+
36+
@Test
37+
void shouldRetrieveInsertedRows() {
38+
final UUID id1 = UUID.randomUUID(), id2 = UUID.randomUUID(), id3 = UUID.randomUUID();
39+
40+
Collection<Product> products = asList(
41+
new Product(id1, "IKEA Billy bookshelf", "TBD", ProductCategory.FURNITURE, 1234.56, 46),
42+
new Product(id2, "Siemens IQ500 dishwasher", null, ProductCategory.APPLIANCE, 4567.89, 34),
43+
new Product(id3, "HP Elitebook 830 laptop", "Power and portability", ProductCategory.ELECTRONICS, 9012.34, 9)
44+
);
45+
46+
PreparableStatement stmt =
47+
insertBatch(products).into(PRODUCT)
48+
.value(PRODUCT.ID, Product::id)
49+
.value(PRODUCT.NAME, Product::name)
50+
.value(PRODUCT.DESCRIPTION, Product::description)
51+
.value(PRODUCT.CATEGORY, Product::category)
52+
.value(PRODUCT.PRICE, Product::price)
53+
.value(PRODUCT.STOCK_COUNT, Product::stock_count);
54+
55+
statementTester()
56+
.assertSql("""
57+
insert into PRODUCT (ID, NAME, DESCRIPTION, CATEGORY, PRICE, STOCK_COUNT) \
58+
values (?, ?, ?, ?, ?, ?), (?, ?, null, ?, ?, ?), (?, ?, ?, ?, ?, ?)"""
59+
)
60+
.assertAffectedRowCount(3)
61+
.verify(stmt);
62+
63+
statementTester()
64+
.assertRowCount(3)
65+
.assertRow(1,
66+
"PR_ID", is(id1.toString()),
67+
"PR_NAME", is("IKEA Billy bookshelf"),
68+
"PR_DESCRIPTION", is("TBD"),
69+
"PR_CATEGORY", is("FURNITURE"),
70+
"PR_PRICE", isBigDecimal(1234.56),
71+
"PR_STOCK_COUNT", isBigDecimal(46)
72+
)
73+
.assertRow(2,
74+
"PR_ID", is(id2.toString()),
75+
"PR_NAME", is("Siemens IQ500 dishwasher"),
76+
"PR_DESCRIPTION", isNull(),
77+
"PR_CATEGORY", is("APPLIANCE"),
78+
"PR_PRICE", isBigDecimal(4567.89),
79+
"PR_STOCK_COUNT", isBigDecimal(34)
80+
)
81+
.assertRow(3,
82+
"PR_ID", is(id3.toString()),
83+
"PR_NAME", is("HP Elitebook 830 laptop"),
84+
"PR_DESCRIPTION", is("Power and portability"),
85+
"PR_CATEGORY", is("ELECTRONICS"),
86+
"PR_PRICE", isBigDecimal(9012.34),
87+
"PR_STOCK_COUNT", isBigDecimal(9)
88+
)
89+
.verify(
90+
select(PRODUCT.ID, PRODUCT.NAME, PRODUCT.DESCRIPTION, PRODUCT.CATEGORY, PRODUCT.PRICE, PRODUCT.STOCK_COUNT)
91+
.from(PRODUCT)
92+
.where(PRODUCT.ID.in(id1, id2, id3))
93+
.orderBy(PRODUCT.PRICE.asc())
94+
);
95+
}
96+
}

0 commit comments

Comments
 (0)