This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 84deb1e236 `SQLBuilder.appendValue(Object)` should format temporal objects as SQL dates. 84deb1e236 is described below commit 84deb1e2364893293829b3323ff59928245c3e0e Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Mar 12 14:20:16 2025 +0100 `SQLBuilder.appendValue(Object)` should format temporal objects as SQL dates. --- .../apache/sis/metadata/sql/privy/SQLBuilder.java | 43 +++++++++- .../org/apache/sis/metadata/sql/privy/Syntax.java | 14 +++- .../org/apache/sis/temporal/LenientDateFormat.java | 6 +- .../sis/metadata/sql/privy/SQLBuilderTest.java | 97 ++++++++++++++++++++++ .../sis/metadata/sql/privy/SQLUtilitiesTest.java | 2 +- 5 files changed, 154 insertions(+), 8 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java index 505fbf68ff..cae7decee6 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/SQLBuilder.java @@ -18,6 +18,11 @@ package org.apache.sis.metadata.sql.privy; import java.sql.DatabaseMetaData; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; import org.apache.sis.util.CharSequences; @@ -57,7 +62,7 @@ public class SQLBuilder extends Syntax { /** * Creates a new {@code SQLBuilder} initialized from the given database metadata. * - * @param metadata the database metadata. + * @param metadata the database metadata, or {@code null} if unavailable. * @param quoteSchema whether the schema name should be written between quotes. * @throws SQLException if an error occurred while fetching the database metadata. */ @@ -241,6 +246,20 @@ public class SQLBuilder extends Syntax { * Appends a value in a {@code SELECT} or {@code INSERT} statement. * If the given value is a character string, then it is written between quotes. * + * <h4>Date and time</h4> + * The standard SQL date format for inserting or setting dates is {@code 'YYYY-MM-DD'}. + * This format is accepted by various SQL databases, including PostgreSQL and MySQL. + * The time format is {@code 'HH:MM:SS'}, optionally followed by a time zone offset + * in the {@code '+HH:MM} format. If the temporal object provides both a date and a time, + * these components are separated by a space instead of the ISO 8601 {@code 'T'} character. + * Example of a date/time with time zone: {@code '2025-03-12 14:30:00+01:00'}. + * + * <h4>When to use</h4> + * {@link java.sql.PreparedStatement} should be used instead of this method, + * for letting the <abbr>JDBC</abbr> driver performs appropriate conversion. + * This method is sometime useful for building a {@code WHERE} clause, + * when the number and type of conditions are not fixed in advance. + * * @param value the value to append, or {@code null}. * @return this builder, for method call chaining. */ @@ -249,6 +268,28 @@ public class SQLBuilder extends Syntax { buffer.append(value); } else if (value instanceof Boolean) { buffer.append((Boolean) value ? "TRUE" : "FALSE"); + } else if (value instanceof TemporalAccessor) { + final var t = (TemporalAccessor) value; + final LocalDate date = t.query(TemporalQueries.localDate()); + final LocalTime time = t.query(TemporalQueries.localTime()); + if (time == null && date == null) { + return appendValue(value.toString()); + } + buffer.append('\''); + if (date != null) { + buffer.append(date); // `toString()` defined as "uuuu-MM-dd" ('u' is year). + if (time != null) { + buffer.append(' '); + } + } + if (time != null) { + buffer.append(time); // `toString()` defined as "HH:mm[:ss]" optionally with fractions. + final ZoneOffset zone = t.query(TemporalQueries.offset()); + if (zone != null) { + buffer.append(zone); // `toString()` defined as "Z" or "±hh:mm" optionally with seconds. + } + } + buffer.append('\''); } else { return appendValue((value != null) ? value.toString() : (String) null); } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java index ef21416737..d4ab856b54 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Syntax.java @@ -56,14 +56,20 @@ public class Syntax { /** * Creates a new {@code Syntax} initialized from the given database metadata. * - * @param metadata the database metadata. + * @param metadata the database metadata, or {@code null} if unavailable. * @param quoteSchema whether the schema name should be written between quotes. * @throws SQLException if an error occurred while fetching the database metadata. */ public Syntax(final DatabaseMetaData metadata, final boolean quoteSchema) throws SQLException { - dialect = Dialect.guess(metadata); - quote = metadata.getIdentifierQuoteString(); - escape = metadata.getSearchStringEscape(); + if (metadata != null) { + dialect = Dialect.guess(metadata); + quote = metadata.getIdentifierQuoteString(); + escape = metadata.getSearchStringEscape(); + } else { + dialect = Dialect.ANSI; + quote = "\""; + escape = "\\"; + } this.quoteSchema = quoteSchema; } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java index 5a63f41d66..337b5762c0 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java @@ -73,13 +73,15 @@ public final class LenientDateFormat extends DateFormat { /** * The thread-safe instance to use for reading and formatting dates. * Only the year is mandatory, all other fields are optional at parsing time. - * However, all fields are written, including milliseconds at formatting time. + * However, all fields are written, including milliseconds at formatting time, + * unless that field is not available at all (which is not he same as available + * with value zero). * * @see #parseInstantUTC(CharSequence, int, int) */ public static final DateTimeFormatter FORMAT = new DateTimeFormatterBuilder() .parseLenient() // For allowing fields with one digit instead of two. - .parseCaseInsensitive() .appendValue(ChronoField.YEAR, 4, 5, SignStyle.NORMAL) // Proleptic year (use negative number if needed). + .parseCaseInsensitive() .appendValue(ChronoField.YEAR, 4, 10, SignStyle.NORMAL) // Proleptic year (use negative number if needed). .optionalStart().appendLiteral('-').appendValue(ChronoField.MONTH_OF_YEAR, 2) .optionalStart().appendLiteral('-').appendValue(ChronoField.DAY_OF_MONTH, 2) .optionalStart().appendLiteral('T').appendValue(ChronoField.HOUR_OF_DAY, 2) diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLBuilderTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLBuilderTest.java new file mode 100644 index 0000000000..59b2e68668 --- /dev/null +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLBuilderTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.sis.metadata.sql.privy; + +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.Year; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +// Test dependencies +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.apache.sis.test.TestCase; + + +/** + * Tests the {@link SQLBuilder} class. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class SQLBuilderTest extends TestCase { + /** + * The builder to use for the tests. + */ + private final SQLBuilder builder; + + /** + * Creates a new test case. + * + * @throws SQLException should never happen for this test. + */ + public SQLBuilderTest() throws SQLException { + builder = new SQLBuilder(null, false); + } + + /** + * Asserts that the builder content is equal to the expected value, then clears the buffer. + * + * @param expected the expected content. + */ + private void compareAndClear(final String expected) { + assertEquals(expected, builder.toString()); + assertSame(builder, builder.clear()); + } + + /** + * Tests the formatting of values of different types. + */ + @Test + public void testAppendValue() { + assertSame(builder, builder.appendValue(46)); + compareAndClear("46"); + + assertSame(builder, builder.appendValue("46")); + compareAndClear("'46'"); + + assertSame(builder, builder.appendValue(Year.of(2024))); + compareAndClear("'2024'"); + + assertSame(builder, builder.appendValue(LocalDate.of(2024, 10, 2))); + compareAndClear("'2024-10-02'"); + + assertSame(builder, builder.appendValue(LocalTime.of(9, 5))); + compareAndClear("'09:05'"); + + assertSame(builder, builder.appendValue(LocalTime.of(18, 32, 7))); + compareAndClear("'18:32:07'"); + + assertSame(builder, builder.appendValue(LocalDateTime.of(2024, 10, 2, 18, 32, 7))); + compareAndClear("'2024-10-02 18:32:07'"); + + assertSame(builder, builder.appendValue(OffsetDateTime.of(2024, 10, 2, 18, 32, 7, 0, ZoneOffset.ofHours(4)))); + compareAndClear("'2024-10-02 18:32:07+04:00'"); + + assertSame(builder, builder.appendValue(ZonedDateTime.of(2024, 10, 2, 18, 32, 7, 0, ZoneId.of("CET")))); + compareAndClear("'2024-10-02 18:32:07+02:00'"); + } +} diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java index ee7f93fe3e..192e3e787f 100644 --- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/sql/privy/SQLUtilitiesTest.java @@ -39,7 +39,7 @@ public final class SQLUtilitiesTest extends TestCase { */ @Test public void testToLikePattern() { - final StringBuilder buffer = new StringBuilder(30); + final var buffer = new StringBuilder(30); assertEquals("WGS84", toLikePattern(buffer, "WGS84")); assertEquals("WGS%84", toLikePattern(buffer, "WGS 84")); assertEquals("A%text%with%random%symbols%", toLikePattern(buffer, "A text !* with_random:/symbols;+"));