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;+"));

Reply via email to