This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 8527c9cadc Add support for sequences in DuckDb #8522 (#8523)
8527c9cadc is described below

commit 8527c9cadc6291843065bec44367829edc7ea071
Author: Nicolas Adment <[email protected]>
AuthorDate: Tue Sep 22 10:04:16 2026 +0200

    Add support for sequences in DuckDb #8522 (#8523)
---
 .../hop/databases/duckdb/DuckDBDatabaseMeta.java   | 88 ++++++++++++++++++++++
 .../databases/duckdb/DuckDBDatabaseMetaTest.java   | 61 +++++++++++++--
 2 files changed, 143 insertions(+), 6 deletions(-)

diff --git 
a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
 
b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
index da64073901..7836315057 100644
--- 
a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
+++ 
b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMeta.java
@@ -17,7 +17,9 @@
 
 package org.apache.hop.databases.duckdb;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
@@ -257,4 +259,90 @@ public class DuckDBDatabaseMeta extends BaseDatabaseMeta 
implements IDatabase {
     setSupportsBooleanDataType(true);
     setSupportsTimestampDataType(true);
   }
+
+  @Override
+  public boolean isSupportsSequences() {
+    return true;
+  }
+
+  @Override
+  public boolean isSupportsSequenceNoMaxValueOption() {
+    return true;
+  }
+
+  /** DuckDB's parser only knows the two word form; the default NOMAXVALUE is 
a syntax error. */
+  @Override
+  public String getSequenceNoMaxValueOption() {
+    return "NO MAXVALUE";
+  }
+
+  /** Sequences live in the duckdb_sequences() catalog function; 
information_schema has no view. */
+  @Override
+  public String getSqlListOfSequences() {
+    return "SELECT sequence_name FROM duckdb_sequences() ORDER BY schema_name, 
sequence_name";
+  }
+
+  @Override
+  public String getSqlNextSequenceValue(String sequenceName) {
+    return "SELECT nextval('" + sequenceName + "')";
+  }
+
+  @Override
+  public String getSqlCurrentSequenceValue(String sequenceName) {
+    return "SELECT currval('" + sequenceName + "')";
+  }
+
+  @Override
+  public String getSqlSequenceExists(String sequenceName) {
+    // The name arrives the way getQuotedSchemaTableCombination built it, so 
it can carry a schema
+    // and, since a DuckDB schema is listed as catalog.schema, a catalog 
before that.
+    List<String> parts = splitQualifiedName(sequenceName);
+    StringBuilder sql =
+        new StringBuilder("SELECT sequence_name FROM duckdb_sequences() WHERE 
")
+            // Identifiers are case-insensitive in DuckDB, quoted ones 
included.
+            .append("lower(sequence_name) = ")
+            .append(quoteSqlString(lower(parts.getLast())));
+    if (parts.size() > 1) {
+      sql.append(" AND lower(schema_name) = ")
+          .append(quoteSqlString(lower(parts.get(parts.size() - 2))));
+    }
+    if (parts.size() > 2) {
+      sql.append(" AND lower(database_name) = ")
+          .append(quoteSqlString(lower(parts.get(parts.size() - 3))));
+    }
+    return sql.toString();
+  }
+
+  /**
+   * Splits a qualified name into its parts, on the dots outside a quoted 
identifier, and gives
+   * every part back the way the catalog holds it: unquoted.
+   */
+  private static List<String> splitQualifiedName(String name) {
+    List<String> parts = new ArrayList<>();
+    StringBuilder part = new StringBuilder();
+    boolean quoted = false;
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if (c == '"') {
+        // Two quotes within a quoted identifier stand for one quote in the 
name itself.
+        if (quoted && i + 1 < name.length() && name.charAt(i + 1) == '"') {
+          part.append('"');
+          i++;
+        } else {
+          quoted = !quoted;
+        }
+      } else if (c == '.' && !quoted) {
+        parts.add(part.toString().trim());
+        part.setLength(0);
+      } else {
+        part.append(c);
+      }
+    }
+    parts.add(part.toString().trim());
+    return parts;
+  }
+
+  private static String lower(String identifier) {
+    return identifier.toLowerCase(Locale.ROOT);
+  }
 }
diff --git 
a/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMetaTest.java
 
b/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMetaTest.java
index 847d04d9fa..a01c32078e 100644
--- 
a/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMetaTest.java
+++ 
b/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMetaTest.java
@@ -18,6 +18,7 @@
 package org.apache.hop.databases.duckdb;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -70,9 +71,8 @@ class DuckDBDatabaseMetaTest {
    */
   @Test
   void tablesAreListedWhicheverNameTheDriverGivesTheirType() throws Exception {
-    Database db = database("tables");
-    db.connect();
-    try {
+    try (Database db = database("tables")) {
+      db.connect();
       db.execStatement("CREATE TABLE ORDINARY(a INT)");
       db.execStatement("CREATE TEMP TABLE TEMPORARY_ONE(a INT)");
       db.execStatement("CREATE VIEW A_VIEW AS SELECT * FROM ORDINARY");
@@ -80,7 +80,7 @@ class DuckDBDatabaseMetaTest {
       List<String> tables = Arrays.asList(db.getTablenames());
       assertTrue(tables.contains("ORDINARY"), "an ordinary table is a table: " 
+ tables);
       assertTrue(tables.contains("TEMPORARY_ONE"), "a temporary table is a 
table: " + tables);
-      assertTrue(!tables.contains("A_VIEW"), "a view is not a table: " + 
tables);
+      assertFalse(tables.contains("A_VIEW"), "a view is not a table: " + 
tables);
 
       assertTrue(
           db.getTableMap().values().stream().anyMatch(names -> 
names.contains("ORDINARY")),
@@ -88,8 +88,6 @@ class DuckDBDatabaseMetaTest {
 
       List<String> views = Arrays.asList(db.getViews(false));
       assertTrue(views.contains("A_VIEW"), "a view is still a view: " + views);
-    } finally {
-      db.disconnect();
     }
   }
 
@@ -118,4 +116,55 @@ class DuckDBDatabaseMetaTest {
         schemas.stream().distinct().count(),
         "no two schemas share a name once qualified: " + schemas);
   }
+
+  /** DuckDB has CREATE SEQUENCE, nextval() and currval() */
+  @Test
+  void sequencesAreCreatedListedAndRead() throws Exception {
+    try (Database db = database("sequences")) {
+      db.connect();
+      db.execStatement(db.getCreateSequenceStatement(null, "SEQ_ONE", 1L, 1L, 
999L, false));
+
+      assertTrue(db.checkSequenceExists("SEQ_ONE"), "the sequence just created 
is found");
+      assertFalse(
+          db.checkSequenceExists("SEQ_MISSING"), "one that was never created 
is not invented");
+      assertTrue(
+          Arrays.asList(db.getSequences()).contains("SEQ_ONE"),
+          "the picker lists it: " + Arrays.toString(db.getSequences()));
+
+      assertEquals(Long.valueOf(1L), db.getNextSequenceValue("SEQ_ONE", "id"));
+      assertEquals(Long.valueOf(2L), db.getNextSequenceValue("SEQ_ONE", "id"));
+    }
+  }
+
+  /** A maximum of -1 stands for an unbounded sequence, spelled NO MAXVALUE on 
DuckDB. */
+  @Test
+  void aSequenceWithoutAMaximumIsCreated() throws Exception {
+    try (Database db = database("unbounded")) {
+      db.connect();
+      String sql = db.getCreateSequenceStatement(null, "SEQ_UNBOUNDED", "1", 
"1", "-1", false);
+      assertTrue(sql.contains("NO MAXVALUE"), "NOMAXVALUE is a syntax error on 
DuckDB: " + sql);
+
+      db.execStatement(sql);
+      assertEquals(Long.valueOf(1L), db.getNextSequenceValue("SEQ_UNBOUNDED", 
"id"));
+    }
+  }
+
+  /** A schema tells two sequences of the same name apart, catalog qualified 
schemas included. */
+  @Test
+  void sequencesAreLookedUpWithinTheirSchema() throws Exception {
+    try (Database db = database("schemas")) {
+      db.connect();
+      db.execStatement("CREATE SCHEMA SIDE");
+      db.execStatement(db.getCreateSequenceStatement("SIDE", "SEQ_TWO", 5L, 
1L, 999L, false));
+
+      assertTrue(db.checkSequenceExists("SIDE", "SEQ_TWO"), "found in the 
schema holding it");
+      assertFalse(
+          db.checkSequenceExists("main", "SEQ_TWO"), "and not in the one that 
does not hold it");
+      assertTrue(
+          db.checkSequenceExists("memory.SIDE", "SEQ_TWO"),
+          "the schema list qualifies schemas by their catalog, so a lookup has 
to take that form");
+
+      assertEquals(Long.valueOf(5L), db.getNextSequenceValue("SIDE", 
"SEQ_TWO", "id"));
+    }
+  }
 }

Reply via email to