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 2a2ba3f78c DuckDB fixes, fixes #3743 and fixes #3744 (#8056)
2a2ba3f78c is described below
commit 2a2ba3f78c5d7b315c956967ac859fa430802058
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Aug 21 18:31:48 2026 +0200
DuckDB fixes, fixes #3743 and fixes #3744 (#8056)
---
.../duckdb/0002-read-time-columns.hpl | 322 +++++++++++++++++
integration-tests/duckdb/0003-write-time.hpl | 169 +++++++++
.../duckdb/0004-read-written-time.hpl | 323 +++++++++++++++++
.../duckdb/0005-read-special-types.hpl | 402 +++++++++++++++++++++
.../duckdb/0006-write-special-types.hpl | 200 ++++++++++
.../duckdb/0007-read-written-special-types.hpl | 402 +++++++++++++++++++++
integration-tests/duckdb/0008-column-types.hpl | 238 ++++++++++++
.../duckdb/main-0002-read-time-columns.hwf | 115 ++++++
.../duckdb/main-0003-write-read-time.hwf | 150 ++++++++
.../duckdb/main-0004-read-special-types.hwf | 115 ++++++
.../duckdb/main-0005-write-read-special-types.hwf | 150 ++++++++
.../duckdb/main-0006-column-types.hwf | 92 +++++
.../scripts/create-special-types-written.sql | 29 ++
.../duckdb/scripts/create-special-types.sql | 56 +++
.../duckdb/scripts/create-time-samples.sql | 46 +++
.../duckdb/scripts/create-time-written.sql | 27 ++
lib-jdbc/pom.xml | 2 +-
plugins/databases/duckdb/pom.xml | 21 ++
.../hop/databases/duckdb/DuckDBDatabaseMeta.java | 10 +-
.../hop/databases/duckdb/DuckDbTimeValues.java | 99 +++++
.../databases/duckdb/DuckDBDatabaseMetaTest.java | 121 +++++++
.../hop/databases/duckdb/DuckDbTimeValuesTest.java | 137 +++++++
22 files changed, 3224 insertions(+), 2 deletions(-)
diff --git a/integration-tests/duckdb/0002-read-time-columns.hpl
b/integration-tests/duckdb/0002-read-time-columns.hpl
new file mode 100644
index 0000000000..31389eeb72
--- /dev/null
+++ b/integration-tests/duckdb/0002-read-time-columns.hpl
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0002-read-time-columns</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads a DuckDB TIME column and compares every value with the
expected rendering stored next to it in the same row. Issue #3744.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>read columns</from>
+ <to>values to text</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values to text</from>
+ <to>null marker</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>null marker</from>
+ <to>values match?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>count rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count rows</from>
+ <to>all rows read?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>success</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>read columns</name>
+ <type>TableInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>duckdb</connection>
+ <execute_each_row>N</execute_each_row>
+ <limit>0</limit>
+ <sql>SELECT id
+, t_time
+, x_time
+FROM main.time_samples
+ORDER BY id</sql>
+ <variables_active>N</variables_active>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values to text</name>
+ <type>SelectValues</type>
+ <description>Renders what Hop read back to text so it can be compared with
the expected value stored alongside it.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>t_time</name>
+ <rename>t_time</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask>HH:mm:ss.SSS</conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>null marker</name>
+ <type>IfNull</type>
+ <description>The expected columns spell a NULL out as &lt;null&gt;
so the null row takes part in the comparison.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <replaceAllByValue/>
+ <replaceAllMask/>
+ <selectFields>Y</selectFields>
+ <selectValuesType>N</selectValuesType>
+ <setEmptyStringAll>N</setEmptyStringAll>
+ <valuetypes>
+ </valuetypes>
+ <fields>
+ <field>
+ <name>t_time</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values match?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>count rows</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <conditions>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>t_time</leftvalue>
+ <function>=</function>
+ <rightvalue>x_time</rightvalue>
+ </condition>
+ </conditions>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count rows</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <add_linenr>N</add_linenr>
+ <all_rows>N</all_rows>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <aggregate>nr_rows</aggregate>
+ <subject>id</subject>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <give_back_row>N</give_back_row>
+ <group>
+</group>
+ <ignore_aggregate>N</ignore_aggregate>
+ <linenr_fieldname/>
+ <prefix>grp</prefix>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>all rows read?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>success</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>nr_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>5</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>976</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>success</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1152</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>A TIME column was not read back as the value DuckDB holds. See
issue #3744.</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0003-write-time.hpl
b/integration-tests/duckdb/0003-write-time.hpl
new file mode 100644
index 0000000000..c1bb43eebc
--- /dev/null
+++ b/integration-tests/duckdb/0003-write-time.hpl
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0003-write-time</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Writes times Hop built itself into a DuckDB TIME column.
Issue #3744.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>rows to write</from>
+ <to>write rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>rows to write</name>
+ <type>DataGrid</type>
+ <description>Values Hop writes into DuckDB, each with the text DuckDB
itself should end up storing.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>1</item>
+ <item>00:00:00.000</item>
+ <item>00:00:00.000</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>09:08:07.000</item>
+ <item>09:08:07.000</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>11:22:33.456</item>
+ <item>11:22:33.456</item>
+ </line>
+ <line>
+ <item>4</item>
+ <item>23:59:59.999</item>
+ <item>23:59:59.999</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <type>Integer</type>
+ <format>####0;-####0</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>w_time</name>
+ <type>Date</type>
+ <format>HH:mm:ss.SSS</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>x_time</name>
+ <type>String</type>
+ <format></format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>write rows</name>
+ <type>TableOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <commit>1000</commit>
+ <connection>duckdb</connection>
+ <fields>
+ <field>
+ <column_name>id</column_name>
+ <stream_name>id</stream_name>
+ </field>
+ <field>
+ <column_name>w_time</column_name>
+ <stream_name>w_time</stream_name>
+ </field>
+ <field>
+ <column_name>x_time</column_name>
+ <stream_name>x_time</stream_name>
+ </field>
+ </fields>
+ <ignore_errors>N</ignore_errors>
+ <only_when_have_rows>N</only_when_have_rows>
+ <partitioning_daily>N</partitioning_daily>
+ <partitioning_enabled>N</partitioning_enabled>
+ <partitioning_monthly>Y</partitioning_monthly>
+ <return_keys>N</return_keys>
+ <specify_fields>Y</specify_fields>
+ <table>time_written</table>
+ <tablename_in_field>N</tablename_in_field>
+ <tablename_in_table>Y</tablename_in_table>
+ <truncate>N</truncate>
+ <use_batch>Y</use_batch>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0004-read-written-time.hpl
b/integration-tests/duckdb/0004-read-written-time.hpl
new file mode 100644
index 0000000000..d95f76dc4f
--- /dev/null
+++ b/integration-tests/duckdb/0004-read-written-time.hpl
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0004-read-written-time</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads back the TIME values Hop wrote and compares them with
the text written beside them, through Hop and through DuckDB's own rendering.
Issue #3744.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>read columns</from>
+ <to>values to text</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values to text</from>
+ <to>null marker</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>null marker</from>
+ <to>values match?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>count rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count rows</from>
+ <to>all rows read?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>success</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>read columns</name>
+ <type>TableInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>duckdb</connection>
+ <execute_each_row>N</execute_each_row>
+ <limit>0</limit>
+ <sql>SELECT id
+, w_time
+, x_time
+, CAST(w_time AS VARCHAR) AS d_time
+FROM main.time_written
+ORDER BY id</sql>
+ <variables_active>N</variables_active>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values to text</name>
+ <type>SelectValues</type>
+ <description>Renders what Hop read back to text so it can be compared with
the expected value stored alongside it.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>w_time</name>
+ <rename>w_time</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask>HH:mm:ss.SSS</conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>null marker</name>
+ <type>IfNull</type>
+ <description>The expected columns spell a NULL out as &lt;null&gt;
so the null row takes part in the comparison.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <replaceAllByValue/>
+ <replaceAllMask/>
+ <selectFields>Y</selectFields>
+ <selectValuesType>N</selectValuesType>
+ <setEmptyStringAll>N</setEmptyStringAll>
+ <valuetypes>
+ </valuetypes>
+ <fields>
+ <field>
+ <name>w_time</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values match?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>count rows</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <conditions>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>w_time</leftvalue>
+ <function>=</function>
+ <rightvalue>x_time</rightvalue>
+ </condition>
+ </conditions>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count rows</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <add_linenr>N</add_linenr>
+ <all_rows>N</all_rows>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <aggregate>nr_rows</aggregate>
+ <subject>id</subject>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <give_back_row>N</give_back_row>
+ <group>
+</group>
+ <ignore_aggregate>N</ignore_aggregate>
+ <linenr_fieldname/>
+ <prefix>grp</prefix>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>all rows read?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>success</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>nr_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>4</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>976</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>success</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1152</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>A TIME Hop wrote did not come back as the value it wrote. See
issue #3744.</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0005-read-special-types.hpl
b/integration-tests/duckdb/0005-read-special-types.hpl
new file mode 100644
index 0000000000..e534b2e2a8
--- /dev/null
+++ b/integration-tests/duckdb/0005-read-special-types.hpl
@@ -0,0 +1,402 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0005-read-special-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads DuckDB JSON and UUID columns and compares every value
with the expected rendering stored next to it in the same row.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>read columns</from>
+ <to>values to text</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values to text</from>
+ <to>compact json</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>compact json</from>
+ <to>null marker</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>null marker</from>
+ <to>values match?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>count rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count rows</from>
+ <to>all rows read?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>success</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>read columns</name>
+ <type>TableInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>duckdb</connection>
+ <execute_each_row>N</execute_each_row>
+ <limit>0</limit>
+ <sql>SELECT id
+, v_json
+, v_uuid
+, x_json
+, x_uuid
+FROM main.special_types
+ORDER BY id</sql>
+ <variables_active>N</variables_active>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values to text</name>
+ <type>SelectValues</type>
+ <description>Renders what Hop read back to text so it can be compared with
the expected value stored alongside it.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>v_json</name>
+ <rename>v_json</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask></conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ <meta>
+ <name>v_uuid</name>
+ <rename>v_uuid</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask></conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>compact json</name>
+ <type>ReplaceString</type>
+ <description>Hop renders a JSON value pretty printed by default, DuckDB
stores it compact. Whitespace is dropped from both sides so the comparison is
about the value rather than about either one's layout. No fixture carries
whitespace inside a JSON string, so nothing meaningful is removed.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <in_stream_name>v_json</in_stream_name>
+ <out_stream_name/>
+ <use_regex>Y</use_regex>
+ <replace_string>\s</replace_string>
+ <replace_by_string/>
+ <set_empty_string>N</set_empty_string>
+ <replace_field_by_string/>
+ <whole_word>N</whole_word>
+ <case_sensitive>N</case_sensitive>
+ <is_unicode>N</is_unicode>
+ </field>
+ <field>
+ <in_stream_name>x_json</in_stream_name>
+ <out_stream_name/>
+ <use_regex>Y</use_regex>
+ <replace_string>\s</replace_string>
+ <replace_by_string/>
+ <set_empty_string>N</set_empty_string>
+ <replace_field_by_string/>
+ <whole_word>N</whole_word>
+ <case_sensitive>N</case_sensitive>
+ <is_unicode>N</is_unicode>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>360</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>null marker</name>
+ <type>IfNull</type>
+ <description>The expected columns spell a NULL out as &lt;null&gt;
so the null row takes part in the comparison.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <replaceAllByValue/>
+ <replaceAllMask/>
+ <selectFields>Y</selectFields>
+ <selectValuesType>N</selectValuesType>
+ <setEmptyStringAll>N</setEmptyStringAll>
+ <valuetypes>
+ </valuetypes>
+ <fields>
+ <field>
+ <name>v_json</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>v_uuid</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values match?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>count rows</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <conditions>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>v_json</leftvalue>
+ <function>=</function>
+ <rightvalue>x_json</rightvalue>
+ </condition>
+ <condition>
+ <negated>N</negated>
+ <operator>AND</operator>
+ <leftvalue>v_uuid</leftvalue>
+ <function>=</function>
+ <rightvalue>x_uuid</rightvalue>
+ </condition>
+ </conditions>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count rows</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <add_linenr>N</add_linenr>
+ <all_rows>N</all_rows>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <aggregate>nr_rows</aggregate>
+ <subject>id</subject>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <give_back_row>N</give_back_row>
+ <group>
+</group>
+ <ignore_aggregate>N</ignore_aggregate>
+ <linenr_fieldname/>
+ <prefix>grp</prefix>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>all rows read?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>success</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>nr_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>4</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>976</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>success</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1152</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>A JSON or UUID column was not read back as the value DuckDB
holds.</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0006-write-special-types.hpl
b/integration-tests/duckdb/0006-write-special-types.hpl
new file mode 100644
index 0000000000..43ed5fef8a
--- /dev/null
+++ b/integration-tests/duckdb/0006-write-special-types.hpl
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0006-write-special-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Writes JSON and UUID values Hop built itself into DuckDB
columns of those types.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>rows to write</from>
+ <to>write rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>rows to write</name>
+ <type>DataGrid</type>
+ <description>Values Hop writes into DuckDB, each with the text DuckDB
itself should end up storing.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <data>
+ <line>
+ <item>1</item>
+ <item>{"a":1}</item>
+ <item>123e4567-e89b-12d3-a456-426614174000</item>
+ <item>{"a":1}</item>
+ <item>123e4567-e89b-12d3-a456-426614174000</item>
+ </line>
+ <line>
+ <item>2</item>
+ <item>{"a":1,"b":[2,3]}</item>
+ <item>00000000-0000-0000-0000-000000000000</item>
+ <item>{"a":1,"b":[2,3]}</item>
+ <item>00000000-0000-0000-0000-000000000000</item>
+ </line>
+ <line>
+ <item>3</item>
+ <item>[1,2,3]</item>
+ <item>ffffffff-ffff-ffff-ffff-ffffffffffff</item>
+ <item>[1,2,3]</item>
+ <item>ffffffff-ffff-ffff-ffff-ffffffffffff</item>
+ </line>
+ </data>
+ <fields>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>id</name>
+ <type>Integer</type>
+ <format>####0;-####0</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>w_json</name>
+ <type>JSON</type>
+ <format></format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>w_uuid</name>
+ <type>UUID</type>
+ <format></format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>x_json</name>
+ <type>String</type>
+ <format></format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ <field>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ <name>x_uuid</name>
+ <type>String</type>
+ <format></format>
+ <currency/>
+ <decimal/>
+ <group/>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>write rows</name>
+ <type>TableOutput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <commit>1000</commit>
+ <connection>duckdb</connection>
+ <fields>
+ <field>
+ <column_name>id</column_name>
+ <stream_name>id</stream_name>
+ </field>
+ <field>
+ <column_name>w_json</column_name>
+ <stream_name>w_json</stream_name>
+ </field>
+ <field>
+ <column_name>w_uuid</column_name>
+ <stream_name>w_uuid</stream_name>
+ </field>
+ <field>
+ <column_name>x_json</column_name>
+ <stream_name>x_json</stream_name>
+ </field>
+ <field>
+ <column_name>x_uuid</column_name>
+ <stream_name>x_uuid</stream_name>
+ </field>
+ </fields>
+ <ignore_errors>N</ignore_errors>
+ <only_when_have_rows>N</only_when_have_rows>
+ <partitioning_daily>N</partitioning_daily>
+ <partitioning_enabled>N</partitioning_enabled>
+ <partitioning_monthly>Y</partitioning_monthly>
+ <return_keys>N</return_keys>
+ <specify_fields>Y</specify_fields>
+ <table>special_types_written</table>
+ <tablename_in_field>N</tablename_in_field>
+ <tablename_in_table>Y</tablename_in_table>
+ <truncate>N</truncate>
+ <use_batch>Y</use_batch>
+ <attributes/>
+ <GUI>
+ <xloc>320</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0007-read-written-special-types.hpl
b/integration-tests/duckdb/0007-read-written-special-types.hpl
new file mode 100644
index 0000000000..db386587e1
--- /dev/null
+++ b/integration-tests/duckdb/0007-read-written-special-types.hpl
@@ -0,0 +1,402 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0007-read-written-special-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads back the JSON and UUID values Hop wrote and compares
them with the text written beside them.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>read columns</from>
+ <to>values to text</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values to text</from>
+ <to>compact json</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>compact json</from>
+ <to>null marker</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>null marker</from>
+ <to>values match?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>count rows</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>values match?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>count rows</from>
+ <to>all rows read?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>success</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>all rows read?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>read columns</name>
+ <type>TableInput</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>duckdb</connection>
+ <execute_each_row>N</execute_each_row>
+ <limit>0</limit>
+ <sql>SELECT id
+, w_json
+, w_uuid
+, x_json
+, x_uuid
+FROM main.special_types_written
+ORDER BY id</sql>
+ <variables_active>N</variables_active>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values to text</name>
+ <type>SelectValues</type>
+ <description>Renders what Hop read back to text so it can be compared with
the expected value stored alongside it.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <select_unspecified>N</select_unspecified>
+ <meta>
+ <name>w_json</name>
+ <rename>w_json</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask></conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ <meta>
+ <name>w_uuid</name>
+ <rename>w_uuid</rename>
+ <type>String</type>
+ <length>-2</length>
+ <precision>-2</precision>
+ <conversion_mask></conversion_mask>
+ <date_format_lenient>false</date_format_lenient>
+ <date_format_locale/>
+ <date_format_timezone/>
+ <lenient_string_to_number>false</lenient_string_to_number>
+ <encoding/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ <storage_type/>
+ </meta>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>compact json</name>
+ <type>ReplaceString</type>
+ <description>Hop renders a JSON value pretty printed by default, DuckDB
stores it compact. Whitespace is dropped from both sides so the comparison is
about the value rather than about either one's layout. No fixture carries
whitespace inside a JSON string, so nothing meaningful is removed.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <in_stream_name>w_json</in_stream_name>
+ <out_stream_name/>
+ <use_regex>Y</use_regex>
+ <replace_string>\s</replace_string>
+ <replace_by_string/>
+ <set_empty_string>N</set_empty_string>
+ <replace_field_by_string/>
+ <whole_word>N</whole_word>
+ <case_sensitive>N</case_sensitive>
+ <is_unicode>N</is_unicode>
+ </field>
+ <field>
+ <in_stream_name>x_json</in_stream_name>
+ <out_stream_name/>
+ <use_regex>Y</use_regex>
+ <replace_string>\s</replace_string>
+ <replace_by_string/>
+ <set_empty_string>N</set_empty_string>
+ <replace_field_by_string/>
+ <whole_word>N</whole_word>
+ <case_sensitive>N</case_sensitive>
+ <is_unicode>N</is_unicode>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>360</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>null marker</name>
+ <type>IfNull</type>
+ <description>The expected columns spell a NULL out as &lt;null&gt;
so the null row takes part in the comparison.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <replaceAllByValue/>
+ <replaceAllMask/>
+ <selectFields>Y</selectFields>
+ <selectValuesType>N</selectValuesType>
+ <setEmptyStringAll>N</setEmptyStringAll>
+ <valuetypes>
+ </valuetypes>
+ <fields>
+ <field>
+ <name>w_json</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>w_uuid</name>
+ <value><null></value>
+ <mask/>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>values match?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>count rows</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <conditions>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>w_json</leftvalue>
+ <function>=</function>
+ <rightvalue>x_json</rightvalue>
+ </condition>
+ <condition>
+ <negated>N</negated>
+ <operator>AND</operator>
+ <leftvalue>w_uuid</leftvalue>
+ <function>=</function>
+ <rightvalue>x_uuid</rightvalue>
+ </condition>
+ </conditions>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>count rows</name>
+ <type>GroupBy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <add_linenr>N</add_linenr>
+ <all_rows>N</all_rows>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <aggregate>nr_rows</aggregate>
+ <subject>id</subject>
+ <type>COUNT_ANY</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <give_back_row>N</give_back_row>
+ <group>
+</group>
+ <ignore_aggregate>N</ignore_aggregate>
+ <linenr_fieldname/>
+ <prefix>grp</prefix>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>all rows read?</name>
+ <type>FilterRows</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>success</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>nr_rows</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>Integer</type>
+ <text>3</text>
+ <length>-1</length>
+ <precision>0</precision>
+ <isnull>N</isnull>
+ <mask>####0;-####0</mask>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>976</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>success</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>1152</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>A JSON or UUID value Hop wrote did not come back as the value it
wrote.</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/0008-column-types.hpl
b/integration-tests/duckdb/0008-column-types.hpl
new file mode 100644
index 0000000000..fb47c89825
--- /dev/null
+++ b/integration-tests/duckdb/0008-column-types.hpl
@@ -0,0 +1,238 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+ <info>
+ <name>0008-column-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Checks that DuckDB's TIME, JSON and UUID columns arrive as
the Hop type that matches them rather than falling back to a string. Comparing
values alone would not notice: DuckDB renders all three as text a string column
would carry just as well.</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>read one of each</from>
+ <to>column types</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>column types</from>
+ <to>one string of types</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>one string of types</from>
+ <to>types as expected?</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>types as expected?</from>
+ <to>success</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>types as expected?</from>
+ <to>Abort</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>read one of each</name>
+ <type>TableInput</type>
+ <description>One column of each type, cast in the query so the check needs
no fixture table.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <connection>duckdb</connection>
+ <execute_each_row>N</execute_each_row>
+ <limit>0</limit>
+ <sql>SELECT CAST('11:22:33.456' AS TIME) AS c_time
+, CAST('{"a":1}' AS JSON) AS c_json
+, CAST('123e4567-e89b-12d3-a456-426614174000' AS UUID) AS c_uuid</sql>
+ <variables_active>N</variables_active>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>column types</name>
+ <type>TransformMetaStructure</type>
+ <description>One row per column, carrying the Hop type it was read
as.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <includePositionField>N</includePositionField>
+ <positionFieldname>position</positionFieldname>
+ <includeFieldnameField>Y</includeFieldnameField>
+ <fieldFieldname>fieldname</fieldFieldname>
+ <includeCommentsField>N</includeCommentsField>
+ <commentsFieldname>comments</commentsFieldname>
+ <includeTypeField>Y</includeTypeField>
+ <typeFieldname>type</typeFieldname>
+ <includeLengthField>N</includeLengthField>
+ <lengthFieldname>length</lengthFieldname>
+ <includePrecisionField>N</includePrecisionField>
+ <precisionFieldname>precision</precisionFieldname>
+ <includeMaskField>N</includeMaskField>
+ <maskFieldname>mask</maskFieldname>
+ <includeOriginField>N</includeOriginField>
+ <originFieldname>origin</originFieldname>
+ <outputRowcount>N</outputRowcount>
+ <rowcountField>rowcount</rowcountField>
+ <attributes/>
+ <GUI>
+ <xloc>272</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>one string of types</name>
+ <type>GroupBy</type>
+ <description>The types in query column order, so one comparison covers all
three.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <add_linenr>N</add_linenr>
+ <all_rows>N</all_rows>
+ <directory>${java.io.tmpdir}</directory>
+ <fields>
+ <field>
+ <aggregate>types</aggregate>
+ <subject>type</subject>
+ <type>CONCAT_COMMA</type>
+ <valuefield/>
+ </field>
+ </fields>
+ <give_back_row>N</give_back_row>
+ <group>
+</group>
+ <ignore_aggregate>N</ignore_aggregate>
+ <linenr_fieldname/>
+ <prefix>grp</prefix>
+ <attributes/>
+ <GUI>
+ <xloc>448</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>types as expected?</name>
+ <type>FilterRows</type>
+ <description>A Hop date for TIME, and the value types Hop has for JSON and
UUID.</description>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <send_true_to>success</send_true_to>
+ <send_false_to>Abort</send_false_to>
+ <compare>
+ <condition>
+ <negated>N</negated>
+ <operator>-</operator>
+ <leftvalue>types</leftvalue>
+ <function>=</function>
+ <rightvalue/>
+ <value>
+ <name>constant</name>
+ <type>String</type>
+ <text>Date, JSON, UUID</text>
+ <length>-1</length>
+ <precision>-1</precision>
+ <isnull>N</isnull>
+ <mask/>
+ </value>
+ </condition>
+ </compare>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>success</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>800</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Abort</name>
+ <type>Abort</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <abort_option>ABORT_WITH_ERROR</abort_option>
+ <always_log_rows>Y</always_log_rows>
+ <message>A DuckDB TIME, JSON or UUID column was not read as the Hop type
that matches it. See issue #3744 for the TIME half.</message>
+ <row_threshold>0</row_threshold>
+ <attributes/>
+ <GUI>
+ <xloc>624</xloc>
+ <yloc>192</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git a/integration-tests/duckdb/main-0002-read-time-columns.hwf
b/integration-tests/duckdb/main-0002-read-time-columns.hwf
new file mode 100644
index 0000000000..a8023fbb08
--- /dev/null
+++ b/integration-tests/duckdb/main-0002-read-time-columns.hwf
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0002-read-time-columns</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads a DuckDB TIME column, which could not be read at all
before. Issue #3744.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create time_samples</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/create-time-samples.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>duckdb</connection>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0002-read-time-columns.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0002-read-time-columns.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create time_samples</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>create time_samples</from>
+ <to>0002-read-time-columns.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/duckdb/main-0003-write-read-time.hwf
b/integration-tests/duckdb/main-0003-write-read-time.hwf
new file mode 100644
index 0000000000..6a0b6d6c0e
--- /dev/null
+++ b/integration-tests/duckdb/main-0003-write-read-time.hwf
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0003-write-read-time</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Writes times into a DuckDB TIME column and reads them back.
Issue #3744.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create time_written</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/create-time-written.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>duckdb</connection>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0003-write-time.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0003-write-time.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0004-read-written-time.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0004-read-written-time.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>592</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create time_written</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>create time_written</from>
+ <to>0003-write-time.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0003-write-time.hpl</from>
+ <to>0004-read-written-time.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/duckdb/main-0004-read-special-types.hwf
b/integration-tests/duckdb/main-0004-read-special-types.hwf
new file mode 100644
index 0000000000..a857367563
--- /dev/null
+++ b/integration-tests/duckdb/main-0004-read-special-types.hwf
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0004-read-special-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Reads DuckDB's own JSON and UUID column types.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create special_types</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/create-special-types.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>duckdb</connection>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0005-read-special-types.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0005-read-special-types.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create special_types</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>create special_types</from>
+ <to>0005-read-special-types.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/duckdb/main-0005-write-read-special-types.hwf
b/integration-tests/duckdb/main-0005-write-read-special-types.hwf
new file mode 100644
index 0000000000..f0b923d39c
--- /dev/null
+++ b/integration-tests/duckdb/main-0005-write-read-special-types.hwf
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0005-write-read-special-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Writes JSON and UUID values into DuckDB columns of those types
and reads them back.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>create special_types_written</name>
+ <description/>
+ <type>SQL</type>
+ <attributes/>
+ <sql/>
+ <useVariableSubstitution>F</useVariableSubstitution>
+ <sqlfromfile>T</sqlfromfile>
+
<sqlfilename>${PROJECT_HOME}/scripts/create-special-types-written.sql</sqlfilename>
+ <sendOneStatement>F</sendOneStatement>
+ <connection>duckdb</connection>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0006-write-special-types.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0006-write-special-types.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>400</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0007-read-written-special-types.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0007-read-written-special-types.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>592</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>create special_types_written</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ <hop>
+ <from>create special_types_written</from>
+ <to>0006-write-special-types.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ <hop>
+ <from>0006-write-special-types.hpl</from>
+ <to>0007-read-written-special-types.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>N</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/duckdb/main-0006-column-types.hwf
b/integration-tests/duckdb/main-0006-column-types.hwf
new file mode 100644
index 0000000000..31c37c05d6
--- /dev/null
+++ b/integration-tests/duckdb/main-0006-column-types.hwf
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+ <name>main-0006-column-types</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Checks that DuckDB's TIME, JSON and UUID columns arrive as the
Hop types that match them.</description>
+ <extended_description/>
+ <workflow_version/>
+ <created_user>-</created_user>
+ <created_date>2026/08/21 10:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/08/21 10:00:00.000</modified_date>
+ <parameters>
+ </parameters>
+ <actions>
+ <action>
+ <name>Start</name>
+ <description/>
+ <type>SPECIAL</type>
+ <attributes/>
+ <DayOfMonth>1</DayOfMonth>
+ <hour>12</hour>
+ <intervalMinutes>60</intervalMinutes>
+ <intervalSeconds>0</intervalSeconds>
+ <minutes>0</minutes>
+ <repeat>N</repeat>
+ <schedulerType>0</schedulerType>
+ <weekDay>1</weekDay>
+ <parallel>N</parallel>
+ <xloc>50</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ <action>
+ <name>0008-column-types.hpl</name>
+ <description/>
+ <type>PIPELINE</type>
+ <attributes/>
+ <filename>${PROJECT_HOME}/0008-column-types.hpl</filename>
+ <params_from_previous>N</params_from_previous>
+ <exec_per_row>N</exec_per_row>
+ <clear_rows>N</clear_rows>
+ <clear_files>N</clear_files>
+ <set_logfile>N</set_logfile>
+ <logfile/>
+ <logext/>
+ <add_date>N</add_date>
+ <add_time>N</add_time>
+ <loglevel>Basic</loglevel>
+ <set_append_logfile>N</set_append_logfile>
+ <wait_until_finished>Y</wait_until_finished>
+ <create_parent_folder>N</create_parent_folder>
+ <run_configuration>local</run_configuration>
+ <parameters>
+ <pass_all_parameters>Y</pass_all_parameters>
+ </parameters>
+ <parallel>N</parallel>
+ <xloc>208</xloc>
+ <yloc>50</yloc>
+ <attributes_hac/>
+ </action>
+ </actions>
+ <hops>
+ <hop>
+ <from>Start</from>
+ <to>0008-column-types.hpl</to>
+ <enabled>Y</enabled>
+ <evaluation>Y</evaluation>
+ <unconditional>Y</unconditional>
+ </hop>
+ </hops>
+ <notepads>
+ </notepads>
+ <attributes/>
+</workflow>
diff --git a/integration-tests/duckdb/scripts/create-special-types-written.sql
b/integration-tests/duckdb/scripts/create-special-types-written.sql
new file mode 100644
index 0000000000..8328219eb4
--- /dev/null
+++ b/integration-tests/duckdb/scripts/create-special-types-written.sql
@@ -0,0 +1,29 @@
+/*
+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.
+*/
+
+/* The table 0006-write-special-types.hpl writes into. */
+
+DROP TABLE IF EXISTS main.special_types_written;
+
+CREATE TABLE main.special_types_written
+(
+ id INTEGER
+, w_json JSON
+, w_uuid UUID
+, x_json VARCHAR(200)
+, x_uuid VARCHAR(50)
+);
diff --git a/integration-tests/duckdb/scripts/create-special-types.sql
b/integration-tests/duckdb/scripts/create-special-types.sql
new file mode 100644
index 0000000000..0049bacdf7
--- /dev/null
+++ b/integration-tests/duckdb/scripts/create-special-types.sql
@@ -0,0 +1,56 @@
+/*
+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.
+*/
+
+/*
+DuckDB has a JSON and a UUID type of its own, and the driver reports both as
Types.OTHER with only
+the type name to tell them apart. Hop has a value type for each, so a column
of either has to come
+back as that type rather than as an anonymous string.
+
+Every row carries the expected rendering of its own columns as plain text, so
the pipeline compares
+what Hop read with what DuckDB holds without hard coding a value in a
transform.
+*/
+
+DROP TABLE IF EXISTS main.special_types;
+
+CREATE TABLE main.special_types
+(
+ id INTEGER
+, label VARCHAR(50)
+, v_json JSON
+, v_uuid UUID
+, x_json VARCHAR(200)
+, x_uuid VARCHAR(50)
+);
+
+/* A type that reads a null wrong is as broken as one that reads a value
wrong. */
+INSERT INTO main.special_types VALUES
+ (1, 'all null', NULL, NULL, '<null>', '<null>');
+
+INSERT INTO main.special_types VALUES
+ (2, 'object'
+ , '{"a":1}', '123e4567-e89b-12d3-a456-426614174000'
+ , '{"a":1}', '123e4567-e89b-12d3-a456-426614174000');
+
+INSERT INTO main.special_types VALUES
+ (3, 'nested object'
+ , '{"a":1,"b":[2,3]}', '00000000-0000-0000-0000-000000000000'
+ , '{"a":1,"b":[2,3]}', '00000000-0000-0000-0000-000000000000');
+
+INSERT INTO main.special_types VALUES
+ (4, 'array'
+ , '[1,2,3]', 'ffffffff-ffff-ffff-ffff-ffffffffffff'
+ , '[1,2,3]', 'ffffffff-ffff-ffff-ffff-ffffffffffff');
diff --git a/integration-tests/duckdb/scripts/create-time-samples.sql
b/integration-tests/duckdb/scripts/create-time-samples.sql
new file mode 100644
index 0000000000..003bf70d2e
--- /dev/null
+++ b/integration-tests/duckdb/scripts/create-time-samples.sql
@@ -0,0 +1,46 @@
+/*
+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.
+*/
+
+/*
+DuckDB's TIME carries a time of day and no date, which is the type issue #3744
could not read:
+the driver cannot produce a Timestamp for it, and reading a date through
getTimestamp is what Hop
+does for every date column.
+
+Every row carries the expected rendering of its own TIME column as plain text,
so the pipeline
+compares what Hop read with what DuckDB holds without hard coding a value in a
transform.
+
+Hop holds a date to the millisecond, so the fixtures stop there. DuckDB's TIME
goes to the
+microsecond; a value with more precision than Hop can hold would be testing
rounding rather than
+reading.
+*/
+
+DROP TABLE IF EXISTS main.time_samples;
+
+CREATE TABLE main.time_samples
+(
+ id INTEGER
+, label VARCHAR(50)
+, t_time TIME
+, x_time VARCHAR(20)
+);
+
+/* A type that reads a null wrong is as broken as one that reads a value
wrong. */
+INSERT INTO main.time_samples VALUES (1, 'all null', NULL, '<null>');
+INSERT INTO main.time_samples VALUES (2, 'midnight', TIME '00:00:00',
'00:00:00.000');
+INSERT INTO main.time_samples VALUES (3, 'whole second', TIME '09:08:07',
'09:08:07.000');
+INSERT INTO main.time_samples VALUES (4, 'milliseconds', TIME '11:22:33.456',
'11:22:33.456');
+INSERT INTO main.time_samples VALUES (5, 'last millisecond of the day', TIME
'23:59:59.999', '23:59:59.999');
diff --git a/integration-tests/duckdb/scripts/create-time-written.sql
b/integration-tests/duckdb/scripts/create-time-written.sql
new file mode 100644
index 0000000000..cd829767e6
--- /dev/null
+++ b/integration-tests/duckdb/scripts/create-time-written.sql
@@ -0,0 +1,27 @@
+/*
+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.
+*/
+
+/* The table 0003-write-time.hpl writes into, so that the write side of a TIME
is covered too. */
+
+DROP TABLE IF EXISTS main.time_written;
+
+CREATE TABLE main.time_written
+(
+ id INTEGER
+, w_time TIME
+, x_time VARCHAR(20)
+);
diff --git a/lib-jdbc/pom.xml b/lib-jdbc/pom.xml
index e419b406ad..50db4afb82 100644
--- a/lib-jdbc/pom.xml
+++ b/lib-jdbc/pom.xml
@@ -32,7 +32,7 @@
<clickhouse.jdbc.version>0.9.8</clickhouse.jdbc.version>
<cratedb-jdbc.version>2.7.0</cratedb-jdbc.version>
<derby.version>10.17.1.0</derby.version>
- <duckdb.version>1.5.3.0</duckdb.version>
+ <duckdb.version>1.5.5.1</duckdb.version>
<h2.version>2.4.240</h2.version>
<hsqldb.version>2.7.4</hsqldb.version>
<jt400.version>21.0.7</jt400.version>
diff --git a/plugins/databases/duckdb/pom.xml b/plugins/databases/duckdb/pom.xml
index f1743aadf9..7790c896cf 100644
--- a/plugins/databases/duckdb/pom.xml
+++ b/plugins/databases/duckdb/pom.xml
@@ -27,4 +27,25 @@
<artifactId>hop-databases-duckdb</artifactId>
<packaging>jar</packaging>
<name>Hop Plugins Databases DuckDB</name>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-libs-jdbc</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <!-- The driver is downloaded on demand at runtime, so it is only
needed to test against. -->
+ <dependency>
+ <groupId>org.duckdb</groupId>
+ <artifactId>duckdb_jdbc</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
</project>
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 dabbc2b40c..8000a4bbbb 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
@@ -49,6 +49,8 @@ public class DuckDBDatabaseMeta extends BaseDatabaseMeta
implements IDatabase {
private static final List<IDatabaseTypeRule> TYPE_RULES =
DatabaseTypes.rules()
+ // A column that carries only a time cannot be read as a timestamp
on DuckDB.
+ .bind(IValueMeta.TYPE_DATE, DuckDbTimeValues::isTimeColumn,
DuckDbTimeValues.BINDING)
// As of DuckDB JDBC 0.10.0 the Calendar overloads of setDate and
setTimestamp are not
// implemented, so a configured time zone cannot be passed to the
driver.
.bind(IValueMeta.TYPE_DATE,
JdbcDateValues.WITHOUT_CALENDAR_OVERLOADS)
@@ -234,9 +236,15 @@ public class DuckDBDatabaseMeta extends BaseDatabaseMeta
implements IDatabase {
return false;
}
+ @Override
+ public String getSqlListOfSchemas() {
+ return "SELECT CONCAT(catalog_name, '.', schema_name) AS name FROM
information_schema.schemata"
+ + " ORDER BY catalog_name, schema_name";
+ }
+
@Override
public String[] getTableTypes() {
- return new String[] {"BASE TABLE", "LOCAL TEMPORARY"};
+ return new String[] {"TABLE", "BASE TABLE", "LOCAL TEMPORARY"};
}
@Override
diff --git
a/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDbTimeValues.java
b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDbTimeValues.java
new file mode 100644
index 0000000000..3d266c79a7
--- /dev/null
+++
b/plugins/databases/duckdb/src/main/java/org/apache/hop/databases/duckdb/DuckDbTimeValues.java
@@ -0,0 +1,99 @@
+/*
+ * 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.hop.databases.duckdb;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.util.Date;
+import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.core.database.types.IValueBinding;
+import org.apache.hop.core.database.types.JdbcDateValues;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+
+/**
+ * Reading a DuckDB {@code TIME} column.
+ *
+ * <p>The DuckDB driver cannot produce a {@code Timestamp} for a column that
carries only a time:
+ * {@code getTimestamp} fails on it, which is how Hop reads every date column.
So such a column is
+ * read through {@code getObject}, which hands out the {@code LocalTime} the
driver already holds,
+ * and the time is placed on the epoch day the way {@link java.sql.Time} does.
+ *
+ * <p>DuckDB's {@code TIMETZ} has the same problem, but it is reported as
{@link
+ * java.sql.Types#TIME_WITH_TIMEZONE}, which no Hop mapping claims, so it
never reaches a date in
+ * the first place. Whoever maps that type has to widen {@link #isTimeColumn}
along with it, or the
+ * column lands on the default read and fails the way this one used to. The
{@code OffsetTime}
+ * branch below is already here for that.
+ *
+ * <p>See issue #3744.
+ */
+public final class DuckDbTimeValues {
+
+ /** The date JDBC gives a value that carries only a time. */
+ private static final LocalDate EPOCH = LocalDate.EPOCH;
+
+ private DuckDbTimeValues() {
+ // Utility class.
+ }
+
+ /** Whether this value was read from a column that carries a time and no
date. */
+ public static boolean isTimeColumn(IDatabase database, IValueMeta valueMeta)
{
+ return valueMeta.getOriginalColumnType() == Types.TIME;
+ }
+
+ /** Reads and writes a time-only column: the read is DuckDB's, the write is
the ordinary one. */
+ public static final IValueBinding BINDING =
+ new IValueBinding() {
+ @Override
+ public Object read(IDatabase database, IValueMeta valueMeta, ResultSet
resultSet, int index)
+ throws SQLException {
+ Object stored = resultSet.getObject(index);
+ if (stored == null || resultSet.wasNull()) {
+ return null;
+ }
+ if (stored instanceof OffsetTime offsetTime) {
+ return Date.from(offsetTime.atDate(EPOCH).toInstant());
+ }
+ if (stored instanceof LocalTime localTime) {
+ return
Date.from(localTime.atDate(EPOCH).atZone(ZoneId.systemDefault()).toInstant());
+ }
+ if (stored instanceof Date date) {
+ return date;
+ }
+ // Whatever else the driver decides to hand out still has to become
a date.
+ return resultSet.getTime(index);
+ }
+
+ @Override
+ public void write(
+ IDatabase database,
+ IValueMeta valueMeta,
+ PreparedStatement preparedStatement,
+ int index,
+ Object value)
+ throws SQLException, HopValueException {
+ JdbcDateValues.write(database, valueMeta, preparedStatement, index,
value, false);
+ }
+ };
+}
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
new file mode 100644
index 0000000000..847d04d9fa
--- /dev/null
+++
b/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDBDatabaseMetaTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.hop.databases.duckdb;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.logging.LoggingObjectType;
+import org.apache.hop.core.logging.SimpleLoggingObject;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The database explorer and every "browse tables" button against the real
DuckDB driver. See issue
+ * #3743, where neither tables nor schemas showed up.
+ */
+class DuckDBDatabaseMetaTest {
+
+ @BeforeAll
+ static void setUpClass() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ /**
+ * A named in-memory database, which is the only in-memory form DuckDB
shares between connections
+ * within a JVM. The plain {@code jdbc:duckdb:} gives every connection a
private one.
+ */
+ private Database database(String name) {
+ DatabaseMeta databaseMeta = new DatabaseMeta();
+ databaseMeta.setIDatabase(new DuckDBDatabaseMeta());
+ databaseMeta.setName(name);
+ databaseMeta.setDBName(":memory:" + name);
+ databaseMeta.setAccessType(DatabaseMeta.TYPE_ACCESS_NATIVE);
+ return new Database(
+ new SimpleLoggingObject(name, LoggingObjectType.GENERAL, null),
+ new Variables(),
+ databaseMeta);
+ }
+
+ /**
+ * DuckDB JDBC 1.5 renamed the type it reports for an ordinary table from
"BASE TABLE" to "TABLE",
+ * and Hop asked for "BASE TABLE" only, so every table listing came back
empty.
+ */
+ @Test
+ void tablesAreListedWhicheverNameTheDriverGivesTheirType() throws Exception {
+ Database db = database("tables");
+ db.connect();
+ try {
+ 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");
+
+ 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);
+
+ assertTrue(
+ db.getTableMap().values().stream().anyMatch(names ->
names.contains("ORDINARY")),
+ "the table map the explorer builds is populated too: " +
db.getTableMap());
+
+ List<String> views = Arrays.asList(db.getViews(false));
+ assertTrue(views.contains("A_VIEW"), "a view is still a view: " + views);
+ } finally {
+ db.disconnect();
+ }
+ }
+
+ /**
+ * "main" exists in every DuckDB catalog, so the plain JDBC schema list is
three identical names.
+ * Qualifying them is what issue #3666 fixed and what a later refactor
dropped again.
+ */
+ @Test
+ void schemasAreQualifiedByTheirCatalog() throws Exception {
+ String sql = new DuckDBDatabaseMeta().getSqlListOfSchemas();
+ assertNotNull(sql, "DuckDB has to supply its own schema list");
+
+ List<String> schemas = new ArrayList<>();
+ try (Connection connection = DriverManager.getConnection("jdbc:duckdb:");
+ Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(sql)) {
+ while (resultSet.next()) {
+ schemas.add(resultSet.getString("name"));
+ }
+ }
+
+ assertTrue(schemas.contains("memory.main"), "the database's own schema: "
+ schemas);
+ assertTrue(schemas.contains("system.main"), "and the system one beside it:
" + schemas);
+ assertEquals(
+ schemas.size(),
+ schemas.stream().distinct().count(),
+ "no two schemas share a name once qualified: " + schemas);
+ }
+}
diff --git
a/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDbTimeValuesTest.java
b/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDbTimeValuesTest.java
new file mode 100644
index 0000000000..ea131f7d0f
--- /dev/null
+++
b/plugins/databases/duckdb/src/test/java/org/apache/hop/databases/duckdb/DuckDbTimeValuesTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Types;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.Date;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.RowMetaAndData;
+import org.apache.hop.core.database.Database;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.logging.LoggingObjectType;
+import org.apache.hop.core.logging.SimpleLoggingObject;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Reading DuckDB temporal columns through the engine. See issue #3744, where
a TIME column could
+ * not be read at all because the driver cannot make a timestamp out of it.
+ */
+class DuckDbTimeValuesTest {
+
+ @BeforeAll
+ static void setUpClass() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ private Database database(String name) {
+ DatabaseMeta databaseMeta = new DatabaseMeta();
+ databaseMeta.setIDatabase(new DuckDBDatabaseMeta());
+ databaseMeta.setName(name);
+ databaseMeta.setDBName(":memory:" + name);
+ databaseMeta.setAccessType(DatabaseMeta.TYPE_ACCESS_NATIVE);
+ return new Database(
+ new SimpleLoggingObject(name, LoggingObjectType.GENERAL, null),
+ new Variables(),
+ databaseMeta);
+ }
+
+ private static Date onTheEpochDay(LocalTime time) {
+ return
Date.from(time.atDate(LocalDate.EPOCH).atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ private static Date at(LocalDateTime dateTime) {
+ return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
+ }
+
+ /** The report: reading a TIME column failed outright. */
+ @Test
+ void aTimeColumnIsReadAsATimeOnTheEpochDay() throws Exception {
+ Database db = database("time");
+ db.connect();
+ try {
+ db.execStatement("CREATE TABLE run_time AS SELECT CAST('11:22:33.456' AS
TIME) AS t");
+
+ RowMetaAndData row = db.getOneRow("SELECT t FROM run_time");
+ assertEquals(IValueMeta.TYPE_DATE,
row.getRowMeta().getValueMeta(0).getType());
+ assertEquals(
+ onTheEpochDay(LocalTime.of(11, 22, 33, 456_000_000)),
+ row.getDate(0, null),
+ "the time, milliseconds and all, on the day JDBC puts a time-only
value");
+ } finally {
+ db.disconnect();
+ }
+ }
+
+ /** The binding is chosen per column, so the ordinary date types must keep
their own handling. */
+ @Test
+ void datesAndTimestampsAreUnaffected() throws Exception {
+ Database db = database("dates");
+ db.connect();
+ try {
+ db.execStatement(
+ "CREATE TABLE moments AS SELECT CAST('2024-03-04 05:06:07.891' AS
TIMESTAMP) AS ts,"
+ + " CAST('2024-03-04' AS DATE) AS d");
+
+ RowMetaAndData row = db.getOneRow("SELECT ts, d FROM moments");
+ assertEquals(at(LocalDateTime.of(2024, 3, 4, 5, 6, 7, 891_000_000)),
row.getDate(0, null));
+ assertEquals(at(LocalDate.of(2024, 3, 4).atStartOfDay()), row.getDate(1,
null));
+ } finally {
+ db.disconnect();
+ }
+ }
+
+ @Test
+ void aNullTimeStaysNull() throws Exception {
+ Database db = database("nulltime");
+ db.connect();
+ try {
+ db.execStatement("CREATE TABLE absent AS SELECT CAST(NULL AS TIME) AS
t");
+ assertNull(db.getOneRow("SELECT t FROM absent").getDate(0, null));
+ } finally {
+ db.disconnect();
+ }
+ }
+
+ @Test
+ void onlyATimeColumnTakesTheTimeBinding() {
+ IValueMeta fromTime = new ValueMetaDate("t");
+ fromTime.setOriginalColumnType(Types.TIME);
+ assertTrue(DuckDbTimeValues.isTimeColumn(null, fromTime));
+
+ IValueMeta fromTimestamp = new ValueMetaDate("ts");
+ fromTimestamp.setOriginalColumnType(Types.TIMESTAMP);
+ assertFalse(DuckDbTimeValues.isTimeColumn(null, fromTimestamp));
+
+ IValueMeta fromDate = new ValueMetaDate("d");
+ fromDate.setOriginalColumnType(Types.DATE);
+ assertFalse(DuckDbTimeValues.isTimeColumn(null, fromDate));
+ }
+}