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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [1.7.0] - 2025-05-??

### Added
- Supporting SQL dialect of SQLite.
- Supporting SQL dialects of SQLite and HSQLDB.

## [1.6.0] - 2025-05-16

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ The example assumes a connection to a MySQL database.
* PostgreSQL
* MS Access
* SQLite
* HSQLDB
* H2

### Supported SQL Features
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<ucanaccess.version>5.1.3</ucanaccess.version>
<mybatis.version>3.5.19</mybatis.version>
<sqlite-jdbc.version>3.46.1.2</sqlite-jdbc.version>
<hsqldb.version>2.7.4</hsqldb.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -227,6 +228,12 @@
<version>${sqlite-jdbc.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/io/github/torand/fastersql/dialect/Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ static Dialect fromConnection(Connection connection) {
return new AccessDialect();
} else if (productName.contains("sqlite")) {
return new SqliteDialect();
} else if (productName.contains("hsql")) {
return new HsqldbDialect();
} else {
throw new UnsupportedOperationException("Database with product name " + productName + " not supported");
}
Expand Down
117 changes: 117 additions & 0 deletions src/main/java/io/github/torand/fastersql/dialect/HsqldbDialect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2024-2025 Tore Eide Andersen
*
* 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.github.torand.fastersql.dialect;

import java.util.EnumSet;
import java.util.List;
import java.util.Optional;

import static io.github.torand.fastersql.dialect.Capability.CONCAT_OPERATOR;
import static io.github.torand.fastersql.dialect.Capability.CURRENT_TIME;
import static io.github.torand.fastersql.dialect.Capability.LIMIT_OFFSET;
import static io.github.torand.fastersql.dialect.Capability.NULL_ORDERING;
import static io.github.torand.fastersql.dialect.Capability.SELECT_FOR_UPDATE;
import static io.github.torand.fastersql.dialect.Capability.TRUNCATE_TABLE;

/**
* Defines the HyperSQL (HSQLDB) SQL dialect.
*
* <a href="https://hsqldb.org/doc/2.0/guide/sqlgeneral-chapt.html" />
*/
public class HsqldbDialect implements Dialect {
private static final EnumSet<Capability> SUPPORTED_CAPS = EnumSet.of(LIMIT_OFFSET, CONCAT_OPERATOR, CURRENT_TIME, NULL_ORDERING, SELECT_FOR_UPDATE, TRUNCATE_TABLE);

@Override
public String getProductName() {
return "HyperSQL/HSQLDB";
}

@Override
public boolean offsetBeforeLimit() {
return false;
}

@Override
public Optional<String> formatRowOffsetClause() {
return Optional.of("offset ?");
}

@Override
public Optional<String> formatRowLimitClause() {
return Optional.of("limit ?");
}

@Override
public Optional<String> formatRowNumLiteral() {
return Optional.of("rownum()");
}

@Override
public String formatToNumberFunction(String operand, int precision, int scale) {
return "to_number(" + operand + ")";
}

@Override
public String formatToCharFunction(String operand, String format) {
return "to_char(" + operand + ", " + format + ")";
}

@Override
public String formatSubstringFunction(String operand, int startPos, int length) {
return "substr(" + operand + ", " + startPos + ", " + length + ")";
}

@Override
public String formatConcatFunction(List<String> operands) {
// Note! The concat infix operator is used in output SQL
return "concat(%s)".formatted(String.join(", ", operands));
}

@Override
public String formatLengthFunction(String operand) {
return "char_length(" + operand + ")";
}

@Override
public String formatCeilFunction(String operand) {
return "ceil(" + operand + ")";
}

@Override
public String formatRoundFunction(String operand) {
return "round(" + operand + ")";
}

@Override
public String formatModuloFunction(String divisor, String dividend) {
return "mod(" + divisor + ", " + dividend + ")";
}

@Override
public String formatCurrentDateFunction() {
return "current_date";
}

@Override
public Optional<String> getConcatOperator() {
return Optional.of("||");
}

@Override
public boolean supports(Capability capability) {
return SUPPORTED_CAPS.contains(capability);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2024-2025 Tore Eide Andersen
*
* 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.github.torand.fastersql.statement.hsqldb;

import io.github.torand.fastersql.statement.PreparableStatement;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static io.github.torand.fastersql.datamodel.DataModel.PRODUCT;
import static io.github.torand.fastersql.function.singlerow.SingleRowFunctions.length;
import static io.github.torand.fastersql.statement.Statements.delete;
import static io.github.torand.fastersql.statement.Statements.select;

public class HsqldbDeleteStatementTest extends HsqldbTest {

@Test
void shouldRemoveDeletedRow() {
final UUID id = UUID.fromString("dba9f942-c24f-4b6a-89b6-881236ff5438"); // Apple iPad

PreparableStatement stmt =
delete().from(PRODUCT)
.where(PRODUCT.ID.eq(id));

statementTester()
.assertSql("""
delete from PRODUCT \
where ID = ?"""
)
.assertParams(id)
.assertAffectedRowCount(1)
.verify(stmt);

statementTester()
.assertRowCount(0)
.verify(
select(PRODUCT.ID)
.from(PRODUCT)
.where(PRODUCT.ID.eq(id))
);
}

@Test
void shouldHandleOptionalPredicates() {
final UUID id = UUID.fromString("7a4b3e96-afee-4284-8ccd-f7461bcd602b"); // Samsung Galaxy

PreparableStatement stmt =
delete().from(PRODUCT)
.where(PRODUCT.ID.eq(id))
.whereIf(true, () -> length(PRODUCT.NAME).gt(10));

statementTester()
.assertSql("""
delete from PRODUCT \
where ID = ? \
and char_length(NAME) > ?"""
)
.assertParams(id, 10)
.assertAffectedRowCount(1)
.verify(stmt);

statementTester()
.assertRowCount(0)
.verify(
select(PRODUCT.ID)
.from(PRODUCT)
.where(PRODUCT.ID.eq(id))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2024-2025 Tore Eide Andersen
*
* 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.github.torand.fastersql.statement.hsqldb;

import io.github.torand.fastersql.domainmodel.Product;
import io.github.torand.fastersql.domainmodel.ProductCategory;
import io.github.torand.fastersql.statement.PreparableStatement;
import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.UUID;

import static io.github.torand.fastersql.datamodel.DataModel.PRODUCT;
import static io.github.torand.fastersql.statement.Statements.insertBatch;
import static io.github.torand.fastersql.statement.Statements.select;
import static io.github.torand.fastersql.util.RowValueMatchers.isBigDecimal;
import static io.github.torand.fastersql.util.RowValueMatchers.isNull;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.is;

public class HsqldbInsertBatchStatementTest extends HsqldbTest {

@Test
void shouldRetrieveInsertedRows() {
final UUID id1 = UUID.randomUUID(), id2 = UUID.randomUUID(), id3 = UUID.randomUUID();

Collection<Product> products = asList(
new Product(id1, "IKEA Billy bookshelf", "TBD", ProductCategory.FURNITURE, 1234.56, 46),
new Product(id2, "Siemens IQ500 dishwasher", null, ProductCategory.APPLIANCE, 4567.89, 34),
new Product(id3, "HP Elitebook 830 laptop", "Power and portability", ProductCategory.ELECTRONICS, 9012.34, 9)
);

PreparableStatement stmt =
insertBatch(products).into(PRODUCT)
.value(PRODUCT.ID, Product::id)
.value(PRODUCT.NAME, Product::name)
.value(PRODUCT.DESCRIPTION, Product::description)
.value(PRODUCT.CATEGORY, Product::category)
.value(PRODUCT.PRICE, Product::price)
.value(PRODUCT.STOCK_COUNT, Product::stock_count);

statementTester()
.assertSql("""
insert into PRODUCT (ID, NAME, DESCRIPTION, CATEGORY, PRICE, STOCK_COUNT) \
values (?, ?, ?, ?, ?, ?), (?, ?, null, ?, ?, ?), (?, ?, ?, ?, ?, ?)"""
)
.assertAffectedRowCount(3)
.verify(stmt);

statementTester()
.assertRowCount(3)
.assertRow(1,
"PR_ID", is(id1.toString()),
"PR_NAME", is("IKEA Billy bookshelf"),
"PR_DESCRIPTION", is("TBD"),
"PR_CATEGORY", is("FURNITURE"),
"PR_PRICE", isBigDecimal(1234.56),
"PR_STOCK_COUNT", isBigDecimal(46)
)
.assertRow(2,
"PR_ID", is(id2.toString()),
"PR_NAME", is("Siemens IQ500 dishwasher"),
"PR_DESCRIPTION", isNull(),
"PR_CATEGORY", is("APPLIANCE"),
"PR_PRICE", isBigDecimal(4567.89),
"PR_STOCK_COUNT", isBigDecimal(34)
)
.assertRow(3,
"PR_ID", is(id3.toString()),
"PR_NAME", is("HP Elitebook 830 laptop"),
"PR_DESCRIPTION", is("Power and portability"),
"PR_CATEGORY", is("ELECTRONICS"),
"PR_PRICE", isBigDecimal(9012.34),
"PR_STOCK_COUNT", isBigDecimal(9)
)
.verify(
select(PRODUCT.ID, PRODUCT.NAME, PRODUCT.DESCRIPTION, PRODUCT.CATEGORY, PRODUCT.PRICE, PRODUCT.STOCK_COUNT)
.from(PRODUCT)
.where(PRODUCT.ID.in(id1, id2, id3))
.orderBy(PRODUCT.PRICE.asc())
);
}
}
Loading