diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Changelog
+
+## 0.1.4.1
+- Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,109 @@
+-- Setup.hs
+
+import Control.Exception (bracket)
+import Control.Monad (filterM, forM_)
+import Data.List (nub)
+import Data.Maybe (catMaybes, isJust, listToMaybe)
+import Distribution.PackageDescription (
+  GenericPackageDescription (..),
+  PackageDescription (..),
+  extraLibDirs,
+  libBuildInfo,
+  library,
+  package,
+ )
+import Distribution.Simple
+import Distribution.Simple.Compiler (compilerInfo)
+import Distribution.Simple.InstallDirs (absoluteInstallDirs, dynlibdir, libdir)
+import Distribution.Simple.LocalBuildInfo (LocalBuildInfo (..), localUnitId)
+import Distribution.Simple.Setup (configExtraLibDirs, configVerbosity, copyDest, defaultCopyFlags, fromFlag)
+import Distribution.Simple.Utils (copyFileVerbose, createDirectoryIfMissingVerbose, createTempDirectory, info)
+import Distribution.Verbosity (Verbosity)
+import System.Directory (
+  doesDirectoryExist,
+  doesFileExist,
+  findFile,
+  getCurrentDirectory,
+  getTemporaryDirectory,
+  removeDirectoryRecursive,
+ )
+import System.Environment (lookupEnv)
+import System.FilePath ((</>), splitSearchPath)
+import System.Process (callProcess)
+
+main :: IO ()
+main =
+  defaultMainWithHooks
+    simpleUserHooks
+      { preConf = \args flags -> do
+          ensureDuckDBAvailable (fromFlag (configVerbosity flags))
+          preConf simpleUserHooks args flags
+      }
+
+systemLibraryCandidates :: [FilePath]
+systemLibraryCandidates =
+  [ "libduckdb.so"
+  , "libduckdb.so.1"
+  , "libduckdb.so.1.4"
+  , "libduckdb.so.1.4.0"
+  , "libduckdb.so.1.4.1"
+  ]
+
+
+ensureDuckDBAvailable :: Verbosity -> IO ()
+ensureDuckDBAvailable verb = do
+  available <- systemDuckDBAvailable
+  if available
+    then info verb "libduckdb found"
+    else error "libduckdb not found, please install libduckdb (available at https://duckdb.org/install/)"
+
+
+systemDuckDBAvailable :: IO Bool
+systemDuckDBAvailable = do
+  header <- findDuckDBHeader
+  lib <- findDuckDBLibrary
+  pure (isJust header && isJust lib)
+
+findDuckDBHeader :: IO (Maybe FilePath)
+findDuckDBHeader = do
+  paths <- headerSearchPaths
+  findFile paths "duckdb.h"
+
+findDuckDBLibrary :: IO (Maybe FilePath)
+findDuckDBLibrary = do
+  paths <- librarySearchPaths
+  listToMaybe . catMaybes <$> mapM (findFile paths) systemLibraryCandidates
+
+headerSearchPaths :: IO [FilePath]
+headerSearchPaths = do
+  envPaths <- gatherEnvPaths ["C_INCLUDE_PATH", "CPATH", "INCLUDE"]
+  let defaults =
+        [ "/usr/include"
+        , "/usr/local/include"
+        , "/usr/include/x86_64-linux-gnu"
+        , "/usr/include/aarch64-linux-gnu"
+        , "/opt/homebrew/include"
+        , "/opt/local/include"
+        ]
+  pure (nub (envPaths ++ defaults))
+
+librarySearchPaths :: IO [FilePath]
+librarySearchPaths = do
+  envPaths <- gatherEnvPaths ["LIBRARY_PATH", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "LIB"]
+  let defaults =
+        [ "/usr/lib"
+        , "/usr/local/lib"
+        , "/usr/lib64"
+        , "/usr/local/lib64"
+        , "/usr/lib/x86_64-linux-gnu"
+        , "/usr/lib/aarch64-linux-gnu"
+        , "/opt/homebrew/lib"
+        , "/opt/local/lib"
+        ]
+  pure (nub (envPaths ++ defaults))
+
+gatherEnvPaths :: [String] -> IO [FilePath]
+gatherEnvPaths names = do
+  vals <- mapM lookupEnv names
+  let paths = concatMap splitSearchPath (catMaybes vals)
+  pure (filter (not . null) paths)
diff --git a/cbits/duckdb.h b/cbits/duckdb.h
new file mode 100644
--- /dev/null
+++ b/cbits/duckdb.h
@@ -0,0 +1,5168 @@
+//===----------------------------------------------------------------------===//
+//
+//                         DuckDB
+//
+// duckdb.h
+//
+//
+//===----------------------------------------------------------------------===//
+//
+// !!!!!!!
+// WARNING: this file is autogenerated by scripts/generate_c_api.py, manual changes will be overwritten
+// !!!!!!!
+
+#pragma once
+
+#ifndef DUCKDB_C_API
+#ifdef _WIN32
+#ifdef DUCKDB_STATIC_BUILD
+#define DUCKDB_C_API
+#else
+#if defined(DUCKDB_BUILD_LIBRARY) && !defined(DUCKDB_BUILD_LOADABLE_EXTENSION)
+#define DUCKDB_C_API __declspec(dllexport)
+#else
+#define DUCKDB_C_API __declspec(dllimport)
+#endif
+#endif
+#else
+#define DUCKDB_C_API
+#endif
+#endif
+
+//! duplicate of duckdb/main/winapi.hpp
+#ifndef DUCKDB_EXTENSION_API
+#ifdef _WIN32
+#ifdef DUCKDB_STATIC_BUILD
+#define DUCKDB_EXTENSION_API
+#else
+#define DUCKDB_EXTENSION_API __declspec(dllexport)
+#endif
+#else
+#define DUCKDB_EXTENSION_API __attribute__((visibility("default")))
+#endif
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//===--------------------------------------------------------------------===//
+// Enums
+//===--------------------------------------------------------------------===//
+
+//! WARNING: The numbers of these enums should not be changed, as changing the numbers breaks ABI compatibility.
+//! Always add enums at the END of the enum
+
+//! An enum over DuckDB's internal types.
+typedef enum DUCKDB_TYPE {
+	DUCKDB_TYPE_INVALID = 0,
+	// bool
+	DUCKDB_TYPE_BOOLEAN = 1,
+	// int8_t
+	DUCKDB_TYPE_TINYINT = 2,
+	// int16_t
+	DUCKDB_TYPE_SMALLINT = 3,
+	// int32_t
+	DUCKDB_TYPE_INTEGER = 4,
+	// int64_t
+	DUCKDB_TYPE_BIGINT = 5,
+	// uint8_t
+	DUCKDB_TYPE_UTINYINT = 6,
+	// uint16_t
+	DUCKDB_TYPE_USMALLINT = 7,
+	// uint32_t
+	DUCKDB_TYPE_UINTEGER = 8,
+	// uint64_t
+	DUCKDB_TYPE_UBIGINT = 9,
+	// float
+	DUCKDB_TYPE_FLOAT = 10,
+	// double
+	DUCKDB_TYPE_DOUBLE = 11,
+	// duckdb_timestamp (microseconds)
+	DUCKDB_TYPE_TIMESTAMP = 12,
+	// duckdb_date
+	DUCKDB_TYPE_DATE = 13,
+	// duckdb_time
+	DUCKDB_TYPE_TIME = 14,
+	// duckdb_interval
+	DUCKDB_TYPE_INTERVAL = 15,
+	// duckdb_hugeint
+	DUCKDB_TYPE_HUGEINT = 16,
+	// duckdb_uhugeint
+	DUCKDB_TYPE_UHUGEINT = 32,
+	// const char*
+	DUCKDB_TYPE_VARCHAR = 17,
+	// duckdb_blob
+	DUCKDB_TYPE_BLOB = 18,
+	// duckdb_decimal
+	DUCKDB_TYPE_DECIMAL = 19,
+	// duckdb_timestamp_s (seconds)
+	DUCKDB_TYPE_TIMESTAMP_S = 20,
+	// duckdb_timestamp_ms (milliseconds)
+	DUCKDB_TYPE_TIMESTAMP_MS = 21,
+	// duckdb_timestamp_ns (nanoseconds)
+	DUCKDB_TYPE_TIMESTAMP_NS = 22,
+	// enum type, only useful as logical type
+	DUCKDB_TYPE_ENUM = 23,
+	// list type, only useful as logical type
+	DUCKDB_TYPE_LIST = 24,
+	// struct type, only useful as logical type
+	DUCKDB_TYPE_STRUCT = 25,
+	// map type, only useful as logical type
+	DUCKDB_TYPE_MAP = 26,
+	// duckdb_array, only useful as logical type
+	DUCKDB_TYPE_ARRAY = 33,
+	// duckdb_hugeint
+	DUCKDB_TYPE_UUID = 27,
+	// union type, only useful as logical type
+	DUCKDB_TYPE_UNION = 28,
+	// duckdb_bit
+	DUCKDB_TYPE_BIT = 29,
+	// duckdb_time_tz
+	DUCKDB_TYPE_TIME_TZ = 30,
+	// duckdb_timestamp (microseconds)
+	DUCKDB_TYPE_TIMESTAMP_TZ = 31,
+	// enum type, only useful as logical type
+	DUCKDB_TYPE_ANY = 34,
+	// duckdb_bignum
+	DUCKDB_TYPE_BIGNUM = 35,
+	// enum type, only useful as logical type
+	DUCKDB_TYPE_SQLNULL = 36,
+	// enum type, only useful as logical type
+	DUCKDB_TYPE_STRING_LITERAL = 37,
+	// enum type, only useful as logical type
+	DUCKDB_TYPE_INTEGER_LITERAL = 38,
+	// duckdb_time_ns (nanoseconds)
+	DUCKDB_TYPE_TIME_NS = 39,
+} duckdb_type;
+
+//! An enum over the returned state of different functions.
+typedef enum duckdb_state { DuckDBSuccess = 0, DuckDBError = 1 } duckdb_state;
+
+//! An enum over the pending state of a pending query result.
+typedef enum duckdb_pending_state {
+	DUCKDB_PENDING_RESULT_READY = 0,
+	DUCKDB_PENDING_RESULT_NOT_READY = 1,
+	DUCKDB_PENDING_ERROR = 2,
+	DUCKDB_PENDING_NO_TASKS_AVAILABLE = 3
+} duckdb_pending_state;
+
+//! An enum over DuckDB's different result types.
+typedef enum duckdb_result_type {
+	DUCKDB_RESULT_TYPE_INVALID = 0,
+	DUCKDB_RESULT_TYPE_CHANGED_ROWS = 1,
+	DUCKDB_RESULT_TYPE_NOTHING = 2,
+	DUCKDB_RESULT_TYPE_QUERY_RESULT = 3,
+} duckdb_result_type;
+
+//! An enum over DuckDB's different statement types.
+typedef enum duckdb_statement_type {
+	DUCKDB_STATEMENT_TYPE_INVALID = 0,
+	DUCKDB_STATEMENT_TYPE_SELECT = 1,
+	DUCKDB_STATEMENT_TYPE_INSERT = 2,
+	DUCKDB_STATEMENT_TYPE_UPDATE = 3,
+	DUCKDB_STATEMENT_TYPE_EXPLAIN = 4,
+	DUCKDB_STATEMENT_TYPE_DELETE = 5,
+	DUCKDB_STATEMENT_TYPE_PREPARE = 6,
+	DUCKDB_STATEMENT_TYPE_CREATE = 7,
+	DUCKDB_STATEMENT_TYPE_EXECUTE = 8,
+	DUCKDB_STATEMENT_TYPE_ALTER = 9,
+	DUCKDB_STATEMENT_TYPE_TRANSACTION = 10,
+	DUCKDB_STATEMENT_TYPE_COPY = 11,
+	DUCKDB_STATEMENT_TYPE_ANALYZE = 12,
+	DUCKDB_STATEMENT_TYPE_VARIABLE_SET = 13,
+	DUCKDB_STATEMENT_TYPE_CREATE_FUNC = 14,
+	DUCKDB_STATEMENT_TYPE_DROP = 15,
+	DUCKDB_STATEMENT_TYPE_EXPORT = 16,
+	DUCKDB_STATEMENT_TYPE_PRAGMA = 17,
+	DUCKDB_STATEMENT_TYPE_VACUUM = 18,
+	DUCKDB_STATEMENT_TYPE_CALL = 19,
+	DUCKDB_STATEMENT_TYPE_SET = 20,
+	DUCKDB_STATEMENT_TYPE_LOAD = 21,
+	DUCKDB_STATEMENT_TYPE_RELATION = 22,
+	DUCKDB_STATEMENT_TYPE_EXTENSION = 23,
+	DUCKDB_STATEMENT_TYPE_LOGICAL_PLAN = 24,
+	DUCKDB_STATEMENT_TYPE_ATTACH = 25,
+	DUCKDB_STATEMENT_TYPE_DETACH = 26,
+	DUCKDB_STATEMENT_TYPE_MULTI = 27,
+} duckdb_statement_type;
+
+//! An enum over DuckDB's different error types.
+typedef enum duckdb_error_type {
+	DUCKDB_ERROR_INVALID = 0,
+	DUCKDB_ERROR_OUT_OF_RANGE = 1,
+	DUCKDB_ERROR_CONVERSION = 2,
+	DUCKDB_ERROR_UNKNOWN_TYPE = 3,
+	DUCKDB_ERROR_DECIMAL = 4,
+	DUCKDB_ERROR_MISMATCH_TYPE = 5,
+	DUCKDB_ERROR_DIVIDE_BY_ZERO = 6,
+	DUCKDB_ERROR_OBJECT_SIZE = 7,
+	DUCKDB_ERROR_INVALID_TYPE = 8,
+	DUCKDB_ERROR_SERIALIZATION = 9,
+	DUCKDB_ERROR_TRANSACTION = 10,
+	DUCKDB_ERROR_NOT_IMPLEMENTED = 11,
+	DUCKDB_ERROR_EXPRESSION = 12,
+	DUCKDB_ERROR_CATALOG = 13,
+	DUCKDB_ERROR_PARSER = 14,
+	DUCKDB_ERROR_PLANNER = 15,
+	DUCKDB_ERROR_SCHEDULER = 16,
+	DUCKDB_ERROR_EXECUTOR = 17,
+	DUCKDB_ERROR_CONSTRAINT = 18,
+	DUCKDB_ERROR_INDEX = 19,
+	DUCKDB_ERROR_STAT = 20,
+	DUCKDB_ERROR_CONNECTION = 21,
+	DUCKDB_ERROR_SYNTAX = 22,
+	DUCKDB_ERROR_SETTINGS = 23,
+	DUCKDB_ERROR_BINDER = 24,
+	DUCKDB_ERROR_NETWORK = 25,
+	DUCKDB_ERROR_OPTIMIZER = 26,
+	DUCKDB_ERROR_NULL_POINTER = 27,
+	DUCKDB_ERROR_IO = 28,
+	DUCKDB_ERROR_INTERRUPT = 29,
+	DUCKDB_ERROR_FATAL = 30,
+	DUCKDB_ERROR_INTERNAL = 31,
+	DUCKDB_ERROR_INVALID_INPUT = 32,
+	DUCKDB_ERROR_OUT_OF_MEMORY = 33,
+	DUCKDB_ERROR_PERMISSION = 34,
+	DUCKDB_ERROR_PARAMETER_NOT_RESOLVED = 35,
+	DUCKDB_ERROR_PARAMETER_NOT_ALLOWED = 36,
+	DUCKDB_ERROR_DEPENDENCY = 37,
+	DUCKDB_ERROR_HTTP = 38,
+	DUCKDB_ERROR_MISSING_EXTENSION = 39,
+	DUCKDB_ERROR_AUTOLOAD = 40,
+	DUCKDB_ERROR_SEQUENCE = 41,
+	DUCKDB_INVALID_CONFIGURATION = 42
+} duckdb_error_type;
+
+//! An enum over DuckDB's different cast modes.
+typedef enum duckdb_cast_mode { DUCKDB_CAST_NORMAL = 0, DUCKDB_CAST_TRY = 1 } duckdb_cast_mode;
+
+//===--------------------------------------------------------------------===//
+// General type definitions
+//===--------------------------------------------------------------------===//
+
+//! DuckDB's index type.
+typedef uint64_t idx_t;
+
+//! Type definition for the data pointers of selection vectors.
+typedef uint32_t sel_t;
+
+//! The callback to destroy data, e.g.,
+//! bind data (if any), init data (if any), extra data for replacement scans (if any), etc.
+typedef void (*duckdb_delete_callback_t)(void *data);
+
+//! The callback to copy data, e.g., bind data (if any).
+typedef void *(*duckdb_copy_callback_t)(void *data);
+
+//! Used for threading, contains a task state.
+//! Must be destroyed with `duckdb_destroy_task_state`.
+typedef void *duckdb_task_state;
+
+//===--------------------------------------------------------------------===//
+// Types (no explicit freeing)
+//===--------------------------------------------------------------------===//
+
+//! DATE is stored as days since 1970-01-01.
+//! Use the `duckdb_from_date` and `duckdb_to_date` functions to extract individual information.
+typedef struct {
+	int32_t days;
+} duckdb_date;
+
+typedef struct {
+	int32_t year;
+	int8_t month;
+	int8_t day;
+} duckdb_date_struct;
+
+//! TIME is stored as microseconds since 00:00:00.
+//! Use the `duckdb_from_time` and `duckdb_to_time` functions to extract individual information.
+typedef struct {
+	int64_t micros;
+} duckdb_time;
+
+typedef struct {
+	int8_t hour;
+	int8_t min;
+	int8_t sec;
+	int32_t micros;
+} duckdb_time_struct;
+
+//! TIME_NS is stored as nanoseconds since 00:00:00.
+typedef struct {
+	int64_t nanos;
+} duckdb_time_ns;
+
+//! TIME_TZ is stored as 40 bits for the int64_t microseconds, and 24 bits for the int32_t offset.
+//! Use the `duckdb_from_time_tz` function to extract individual information.
+typedef struct {
+	uint64_t bits;
+} duckdb_time_tz;
+
+typedef struct {
+	duckdb_time_struct time;
+	int32_t offset;
+} duckdb_time_tz_struct;
+
+//! TIMESTAMP is stored as microseconds since 1970-01-01.
+//! Use the `duckdb_from_timestamp` and `duckdb_to_timestamp` functions to extract individual information.
+typedef struct {
+	int64_t micros;
+} duckdb_timestamp;
+
+typedef struct {
+	duckdb_date_struct date;
+	duckdb_time_struct time;
+} duckdb_timestamp_struct;
+
+//! TIMESTAMP_S is stored as seconds since 1970-01-01.
+typedef struct {
+	int64_t seconds;
+} duckdb_timestamp_s;
+
+//! TIMESTAMP_MS is stored as milliseconds since 1970-01-01.
+typedef struct {
+	int64_t millis;
+} duckdb_timestamp_ms;
+
+//! TIMESTAMP_NS is stored as nanoseconds since 1970-01-01.
+typedef struct {
+	int64_t nanos;
+} duckdb_timestamp_ns;
+
+//! INTERVAL is stored in months, days, and micros.
+typedef struct {
+	int32_t months;
+	int32_t days;
+	int64_t micros;
+} duckdb_interval;
+
+//! HUGEINT is composed of a lower and upper component.
+//! Its value is upper * 2^64 + lower.
+//! For simplified usage, use `duckdb_hugeint_to_double` and `duckdb_double_to_hugeint`.
+typedef struct {
+	uint64_t lower;
+	int64_t upper;
+} duckdb_hugeint;
+
+//! UHUGEINT is composed of a lower and upper component.
+//! Its value is upper * 2^64 + lower.
+//! For simplified usage, use `duckdb_uhugeint_to_double` and `duckdb_double_to_uhugeint`.
+typedef struct {
+	uint64_t lower;
+	uint64_t upper;
+} duckdb_uhugeint;
+
+//! DECIMAL is composed of a width and a scale.
+//! Their value is stored in a HUGEINT.
+typedef struct {
+	uint8_t width;
+	uint8_t scale;
+	duckdb_hugeint value;
+} duckdb_decimal;
+
+//! A type holding information about the query execution progress.
+typedef struct {
+	double percentage;
+	uint64_t rows_processed;
+	uint64_t total_rows_to_process;
+} duckdb_query_progress_type;
+
+//! The internal representation of a VARCHAR (string_t). If the VARCHAR does not
+//! exceed 12 characters, then we inline it. Otherwise, we inline a four-byte prefix for faster
+//! string comparisons and store a pointer to the remaining characters. This is a non-
+//! owning structure, i.e., it does not have to be freed.
+typedef struct {
+	union {
+		struct {
+			uint32_t length;
+			char prefix[4];
+			char *ptr;
+		} pointer;
+		struct {
+			uint32_t length;
+			char inlined[12];
+		} inlined;
+	} value;
+} duckdb_string_t;
+
+//! DuckDB's LISTs are composed of a 'parent' vector holding metadata of each list,
+//! and a child vector holding the entries of the lists.
+//! The `duckdb_list_entry` struct contains the internal representation of a LIST metadata entry.
+//! A metadata entry contains the length of the list, and its offset in the child vector.
+typedef struct {
+	uint64_t offset;
+	uint64_t length;
+} duckdb_list_entry;
+
+//! A column consists of a pointer to its internal data. Don't operate on this type directly.
+//! Instead, use functions such as `duckdb_column_data`, `duckdb_nullmask_data`,
+//! `duckdb_column_type`, and `duckdb_column_name`.
+typedef struct {
+	// Deprecated, use `duckdb_column_data`.
+	void *deprecated_data;
+	// Deprecated, use `duckdb_nullmask_data`.
+	bool *deprecated_nullmask;
+	// Deprecated, use `duckdb_column_type`.
+	duckdb_type deprecated_type;
+	// Deprecated, use `duckdb_column_name`.
+	char *deprecated_name;
+	void *internal_data;
+} duckdb_column;
+
+//! 1. A standalone vector that must be destroyed, or
+//! 2. A vector to a column in a data chunk that lives as long as the data chunk lives.
+typedef struct _duckdb_vector {
+	void *internal_ptr;
+} * duckdb_vector;
+
+//! A selection vector is a vector of indices, which usually refer to values in a vector.
+//! Can be used to slice vectors, changing their length and the order of their entries.
+//! Standalone selection vectors must be destroyed.
+typedef struct _duckdb_selection_vector {
+	void *internal_ptr;
+} * duckdb_selection_vector;
+
+//===--------------------------------------------------------------------===//
+// Types (explicit freeing/destroying)
+//===--------------------------------------------------------------------===//
+
+//! Strings are composed of a `char` pointer and a size.
+//! You must free `string.data` with `duckdb_free`.
+typedef struct {
+	char *data;
+	idx_t size;
+} duckdb_string;
+
+//! BLOBs are composed of a byte pointer and a size.
+//! You must free `blob.data` with `duckdb_free`.
+typedef struct {
+	void *data;
+	idx_t size;
+} duckdb_blob;
+
+//! BITs are composed of a byte pointer and a size.
+//! BIT byte data has 0 to 7 bits of padding.
+//! The first byte contains the number of padding bits.
+//! The padding bits of the second byte are set to 1, starting from the MSB.
+//! You must free `data` with `duckdb_free`.
+typedef struct {
+	uint8_t *data;
+	idx_t size;
+} duckdb_bit;
+
+//! BIGNUMs are composed of a byte pointer, a size, and an `is_negative` bool.
+//! The absolute value of the number is stored in `data` in little endian format.
+//! You must free `data` with `duckdb_free`.
+typedef struct {
+	uint8_t *data;
+	idx_t size;
+	bool is_negative;
+} duckdb_bignum;
+
+//! A query result consists of a pointer to its internal data.
+//! Must be freed with 'duckdb_destroy_result'.
+typedef struct {
+	// Deprecated, use `duckdb_column_count`.
+	idx_t deprecated_column_count;
+	// Deprecated, use `duckdb_row_count`.
+	idx_t deprecated_row_count;
+	// Deprecated, use `duckdb_rows_changed`.
+	idx_t deprecated_rows_changed;
+	// Deprecated, use `duckdb_column_*`-family of functions.
+	duckdb_column *deprecated_columns;
+	// Deprecated, use `duckdb_result_error`.
+	char *deprecated_error_message;
+	void *internal_data;
+} duckdb_result;
+
+//! A database instance cache object. Must be destroyed with `duckdb_destroy_instance_cache`.
+typedef struct _duckdb_instance_cache {
+	void *internal_ptr;
+} * duckdb_instance_cache;
+
+//! A database object. Must be closed with `duckdb_close`.
+typedef struct _duckdb_database {
+	void *internal_ptr;
+} * duckdb_database;
+
+//! A connection to a duckdb database. Must be closed with `duckdb_disconnect`.
+typedef struct _duckdb_connection {
+	void *internal_ptr;
+} * duckdb_connection;
+
+//! A client context of a duckdb connection. Must be destroyed with `duckdb_destroy_context`.
+typedef struct _duckdb_client_context {
+	void *internal_ptr;
+} * duckdb_client_context;
+
+//! A prepared statement is a parameterized query that allows you to bind parameters to it.
+//! Must be destroyed with `duckdb_destroy_prepare`.
+typedef struct _duckdb_prepared_statement {
+	void *internal_ptr;
+} * duckdb_prepared_statement;
+
+//! Extracted statements. Must be destroyed with `duckdb_destroy_extracted`.
+typedef struct _duckdb_extracted_statements {
+	void *internal_ptr;
+} * duckdb_extracted_statements;
+
+//! The pending result represents an intermediate structure for a query that is not yet fully executed.
+//! Must be destroyed with `duckdb_destroy_pending`.
+typedef struct _duckdb_pending_result {
+	void *internal_ptr;
+} * duckdb_pending_result;
+
+//! The appender enables fast data loading into DuckDB.
+//! Must be destroyed with `duckdb_appender_destroy`.
+typedef struct _duckdb_appender {
+	void *internal_ptr;
+} * duckdb_appender;
+
+//! The table description allows querying information about the table.
+//! Must be destroyed with `duckdb_table_description_destroy`.
+typedef struct _duckdb_table_description {
+	void *internal_ptr;
+} * duckdb_table_description;
+
+//! The configuration can be used to provide start-up options for a database.
+//! Must be destroyed with `duckdb_destroy_config`.
+typedef struct _duckdb_config {
+	void *internal_ptr;
+} * duckdb_config;
+
+//! A logical type.
+//! Must be destroyed with `duckdb_destroy_logical_type`.
+typedef struct _duckdb_logical_type {
+	void *internal_ptr;
+} * duckdb_logical_type;
+
+//! Holds extra information to register a custom logical type.
+//! Reserved for future use.
+typedef struct _duckdb_create_type_info {
+	void *internal_ptr;
+} * duckdb_create_type_info;
+
+//! Contains a data chunk of a duckdb_result.
+//! Must be destroyed with `duckdb_destroy_data_chunk`.
+typedef struct _duckdb_data_chunk {
+	void *internal_ptr;
+} * duckdb_data_chunk;
+
+//! A value of a logical type.
+//! Must be destroyed with `duckdb_destroy_value`.
+typedef struct _duckdb_value {
+	void *internal_ptr;
+} * duckdb_value;
+
+//! Holds a recursive tree containing profiling metrics.
+//! The tree matches the query plan, and has a top-level node.
+typedef struct _duckdb_profiling_info {
+	void *internal_ptr;
+} * duckdb_profiling_info;
+
+//! Holds error data.
+//! Must be destroyed with `duckdb_destroy_error_data`.
+typedef struct _duckdb_error_data {
+	void *internal_ptr;
+} * duckdb_error_data;
+
+//! Holds a bound expression.
+//! Must be destroyed with `duckdb_destroy_expression`.
+typedef struct _duckdb_expression {
+	void *internal_ptr;
+} * duckdb_expression;
+
+//===--------------------------------------------------------------------===//
+// C API extension information
+//===--------------------------------------------------------------------===//
+
+//! Holds the state of the C API extension initialization process.
+typedef struct _duckdb_extension_info {
+	void *internal_ptr;
+} * duckdb_extension_info;
+
+//===--------------------------------------------------------------------===//
+// Function types
+//===--------------------------------------------------------------------===//
+
+//! Additional function info.
+//! When setting this info, it is necessary to pass a destroy-callback function.
+typedef struct _duckdb_function_info {
+	void *internal_ptr;
+} * duckdb_function_info;
+
+//! The bind info of a function.
+//! When setting this info, it is necessary to pass a destroy-callback function.
+typedef struct _duckdb_bind_info {
+	void *internal_ptr;
+} * duckdb_bind_info;
+
+//===--------------------------------------------------------------------===//
+// Scalar function types
+//===--------------------------------------------------------------------===//
+
+//! A scalar function. Must be destroyed with `duckdb_destroy_scalar_function`.
+typedef struct _duckdb_scalar_function {
+	void *internal_ptr;
+} * duckdb_scalar_function;
+
+//! A scalar function set. Must be destroyed with `duckdb_destroy_scalar_function_set`.
+typedef struct _duckdb_scalar_function_set {
+	void *internal_ptr;
+} * duckdb_scalar_function_set;
+
+//! The bind function callback of the scalar function.
+typedef void (*duckdb_scalar_function_bind_t)(duckdb_bind_info info);
+
+//! The function to execute the scalar function on an input chunk.
+typedef void (*duckdb_scalar_function_t)(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output);
+
+//===--------------------------------------------------------------------===//
+// Aggregate function types
+//===--------------------------------------------------------------------===//
+
+//! An aggregate function. Must be destroyed with `duckdb_destroy_aggregate_function`.
+typedef struct _duckdb_aggregate_function {
+	void *internal_ptr;
+} * duckdb_aggregate_function;
+
+//! A aggregate function set. Must be destroyed with `duckdb_destroy_aggregate_function_set`.
+typedef struct _duckdb_aggregate_function_set {
+	void *internal_ptr;
+} * duckdb_aggregate_function_set;
+
+//! The state of an aggregate function.
+typedef struct _duckdb_aggregate_state {
+	void *internal_ptr;
+} * duckdb_aggregate_state;
+
+//! A function to return the aggregate state's size.
+typedef idx_t (*duckdb_aggregate_state_size)(duckdb_function_info info);
+
+//! A function to initialize an aggregate state.
+typedef void (*duckdb_aggregate_init_t)(duckdb_function_info info, duckdb_aggregate_state state);
+
+//! An optional function to destroy an aggregate state.
+typedef void (*duckdb_aggregate_destroy_t)(duckdb_aggregate_state *states, idx_t count);
+
+//! A function to update a set of aggregate states with new values.
+typedef void (*duckdb_aggregate_update_t)(duckdb_function_info info, duckdb_data_chunk input,
+                                          duckdb_aggregate_state *states);
+
+//! A function to combine aggregate states.
+typedef void (*duckdb_aggregate_combine_t)(duckdb_function_info info, duckdb_aggregate_state *source,
+                                           duckdb_aggregate_state *target, idx_t count);
+
+//! A function to finalize aggregate states into a result vector.
+typedef void (*duckdb_aggregate_finalize_t)(duckdb_function_info info, duckdb_aggregate_state *source,
+                                            duckdb_vector result, idx_t count, idx_t offset);
+
+//===--------------------------------------------------------------------===//
+// Table function types
+//===--------------------------------------------------------------------===//
+
+//! A table function. Must be destroyed with `duckdb_destroy_table_function`.
+typedef struct _duckdb_table_function {
+	void *internal_ptr;
+} * duckdb_table_function;
+
+//! Additional function initialization info.
+//! When setting this info, it is necessary to pass a destroy-callback function.
+typedef struct _duckdb_init_info {
+	void *internal_ptr;
+} * duckdb_init_info;
+
+//! The bind function of the table function.
+typedef void (*duckdb_table_function_bind_t)(duckdb_bind_info info);
+
+//! The possibly thread-local initialization function of the table function.
+typedef void (*duckdb_table_function_init_t)(duckdb_init_info info);
+
+//! The function to generate an output chunk during table function execution.
+typedef void (*duckdb_table_function_t)(duckdb_function_info info, duckdb_data_chunk output);
+
+//===--------------------------------------------------------------------===//
+// Cast types
+//===--------------------------------------------------------------------===//
+
+//! A cast function. Must be destroyed with `duckdb_destroy_cast_function`.
+typedef struct _duckdb_cast_function {
+	void *internal_ptr;
+} * duckdb_cast_function;
+
+//! The function to cast from an input vector to an output vector.
+typedef bool (*duckdb_cast_function_t)(duckdb_function_info info, idx_t count, duckdb_vector input,
+                                       duckdb_vector output);
+
+//===--------------------------------------------------------------------===//
+// Replacement scan types
+//===--------------------------------------------------------------------===//
+
+//! Additional replacement scan info. When setting this info, it is necessary to pass a destroy-callback function.
+typedef struct _duckdb_replacement_scan_info {
+	void *internal_ptr;
+} * duckdb_replacement_scan_info;
+
+//! A replacement scan function.
+typedef void (*duckdb_replacement_callback_t)(duckdb_replacement_scan_info info, const char *table_name, void *data);
+
+//===--------------------------------------------------------------------===//
+// Arrow-related types
+//===--------------------------------------------------------------------===//
+
+//! Forward declare Arrow structs
+//! It is important to notice that these structs are not defined by DuckDB but are actually Arrow external objects.
+//! They're defined by the C Data Interface Arrow spec: https://arrow.apache.org/docs/format/CDataInterface.html
+
+struct ArrowArray;
+
+struct ArrowSchema;
+
+//! Holds an arrow query result. Must be destroyed with `duckdb_destroy_arrow`.
+typedef struct _duckdb_arrow {
+	void *internal_ptr;
+} * duckdb_arrow;
+
+//! Holds an arrow array stream. Must be destroyed with `duckdb_destroy_arrow_stream`.
+typedef struct _duckdb_arrow_stream {
+	void *internal_ptr;
+} * duckdb_arrow_stream;
+
+//! Holds an arrow schema. Remember to release the respective ArrowSchema object.
+typedef struct _duckdb_arrow_schema {
+	void *internal_ptr;
+} * duckdb_arrow_schema;
+
+//! Holds an arrow converted schema (i.e., duckdb::ArrowTableSchema).
+//! In practice, this object holds the information necessary to do proper conversion between Arrow Types and DuckDB
+//! Types. Check duckdb/function/table/arrow/arrow_duck_schema.hpp for more details! Must be destroyed with
+//! `duckdb_destroy_arrow_converted_schema`
+typedef struct _duckdb_arrow_converted_schema {
+	void *internal_ptr;
+} * duckdb_arrow_converted_schema;
+
+//! Holds an arrow array. Remember to release the respective ArrowSchema object.
+typedef struct _duckdb_arrow_array {
+	void *internal_ptr;
+} * duckdb_arrow_array;
+
+//! The arrow options used when transforming the DuckDB schema and datachunks into Arrow schema and arrays.
+//! Used in `duckdb_to_arrow_schema` and `duckdb_data_chunk_to_arrow`
+typedef struct _duckdb_arrow_options {
+	void *internal_ptr;
+} * duckdb_arrow_options;
+
+//===--------------------------------------------------------------------===//
+// DuckDB extension access
+//===--------------------------------------------------------------------===//
+
+//! Passed to C API extension as a parameter to the entrypoint.
+struct duckdb_extension_access {
+	//! Indicate that an error has occurred.
+	void (*set_error)(duckdb_extension_info info, const char *error);
+	//! Fetch the database on which to register the extension.
+	duckdb_database *(*get_database)(duckdb_extension_info info);
+	//! Fetch the API struct pointer.
+	const void *(*get_api)(duckdb_extension_info info, const char *version);
+};
+
+#ifndef DUCKDB_API_EXCLUDE_FUNCTIONS
+
+//===--------------------------------------------------------------------===//
+// Functions
+//===--------------------------------------------------------------------===//
+
+//===--------------------------------------------------------------------===//
+// Open Connect
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new database instance cache.
+The instance cache is necessary if a client/program (re)opens multiple databases to the same file within the same
+process. Must be destroyed with 'duckdb_destroy_instance_cache'.
+
+* @return The database instance cache.
+*/
+DUCKDB_C_API duckdb_instance_cache duckdb_create_instance_cache();
+
+/*!
+Creates a new database instance in the instance cache, or retrieves an existing database instance.
+Must be closed with 'duckdb_close'.
+
+* @param instance_cache The instance cache in which to create the database, or from which to take the database.
+* @param path Path to the database file on disk. Both `nullptr` and `:memory:` open or retrieve an in-memory database.
+* @param out_database The resulting cached database.
+* @param config (Optional) configuration used to create the database.
+* @param out_error If set and the function returns `DuckDBError`, this contains the error message.
+Note that the error message must be freed using `duckdb_free`.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_get_or_create_from_cache(duckdb_instance_cache instance_cache, const char *path,
+                                                          duckdb_database *out_database, duckdb_config config,
+                                                          char **out_error);
+
+/*!
+Destroys an existing database instance cache and de-allocates its memory.
+
+* @param instance_cache The instance cache to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_instance_cache(duckdb_instance_cache *instance_cache);
+
+/*!
+Creates a new database or opens an existing database file stored at the given path.
+If no path is given a new in-memory database is created instead.
+The database must be closed with 'duckdb_close'.
+
+* @param path Path to the database file on disk. Both `nullptr` and `:memory:` open an in-memory database.
+* @param out_database The result database object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_open(const char *path, duckdb_database *out_database);
+
+/*!
+Extended version of duckdb_open. Creates a new database or opens an existing database file stored at the given path.
+The database must be closed with 'duckdb_close'.
+
+* @param path Path to the database file on disk. Both `nullptr` and `:memory:` open an in-memory database.
+* @param out_database The result database object.
+* @param config (Optional) configuration used to start up the database.
+* @param out_error If set and the function returns `DuckDBError`, this contains the error message.
+Note that the error message must be freed using `duckdb_free`.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_open_ext(const char *path, duckdb_database *out_database, duckdb_config config,
+                                          char **out_error);
+
+/*!
+Closes the specified database and de-allocates all memory allocated for that database.
+This should be called after you are done with any database allocated through `duckdb_open` or `duckdb_open_ext`.
+Note that failing to call `duckdb_close` (in case of e.g. a program crash) will not cause data corruption.
+Still, it is recommended to always correctly close a database object after you are done with it.
+
+* @param database The database object to shut down.
+*/
+DUCKDB_C_API void duckdb_close(duckdb_database *database);
+
+/*!
+Opens a connection to a database. Connections are required to query the database, and store transactional state
+associated with the connection.
+The instantiated connection should be closed using 'duckdb_disconnect'.
+
+* @param database The database file to connect to.
+* @param out_connection The result connection object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_connect(duckdb_database database, duckdb_connection *out_connection);
+
+/*!
+Interrupt running query
+
+* @param connection The connection to interrupt
+*/
+DUCKDB_C_API void duckdb_interrupt(duckdb_connection connection);
+
+/*!
+Get progress of the running query
+
+* @param connection The working connection
+* @return -1 if no progress or a percentage of the progress
+*/
+DUCKDB_C_API duckdb_query_progress_type duckdb_query_progress(duckdb_connection connection);
+
+/*!
+Closes the specified connection and de-allocates all memory allocated for that connection.
+
+* @param connection The connection to close.
+*/
+DUCKDB_C_API void duckdb_disconnect(duckdb_connection *connection);
+
+/*!
+Retrieves the client context of the connection.
+
+* @param connection The connection.
+* @param out_context The client context of the connection. Must be destroyed with `duckdb_destroy_client_context`.
+*/
+DUCKDB_C_API void duckdb_connection_get_client_context(duckdb_connection connection,
+                                                       duckdb_client_context *out_context);
+
+/*!
+Retrieves the arrow options of the connection.
+
+* @param connection The connection.
+*/
+DUCKDB_C_API void duckdb_connection_get_arrow_options(duckdb_connection connection,
+                                                      duckdb_arrow_options *out_arrow_options);
+
+/*!
+Returns the connection id of the client context.
+
+* @param context The client context.
+* @return The connection id of the client context.
+*/
+DUCKDB_C_API idx_t duckdb_client_context_get_connection_id(duckdb_client_context context);
+
+/*!
+Destroys the client context and deallocates its memory.
+
+* @param context The client context to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_client_context(duckdb_client_context *context);
+
+/*!
+Destroys the arrow options and deallocates its memory.
+
+* @param arrow_options The arrow options to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_arrow_options(duckdb_arrow_options *arrow_options);
+
+/*!
+Returns the version of the linked DuckDB, with a version postfix for dev versions
+
+Usually used for developing C extensions that must return this for a compatibility check.
+*/
+DUCKDB_C_API const char *duckdb_library_version();
+
+/*!
+Get the list of (fully qualified) table names of the query.
+
+* @param connection The connection for which to get the table names.
+* @param query The query for which to get the table names.
+* @param qualified Returns fully qualified table names (catalog.schema.table), if set to true, else only the (not
+escaped) table names.
+* @return A duckdb_value of type VARCHAR[] containing the (fully qualified) table names of the query. Must be destroyed
+with duckdb_destroy_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_get_table_names(duckdb_connection connection, const char *query, bool qualified);
+
+//===--------------------------------------------------------------------===//
+// Configuration
+//===--------------------------------------------------------------------===//
+
+/*!
+Initializes an empty configuration object that can be used to provide start-up options for the DuckDB instance
+through `duckdb_open_ext`.
+The duckdb_config must be destroyed using 'duckdb_destroy_config'
+
+This will always succeed unless there is a malloc failure.
+
+Note that `duckdb_destroy_config` should always be called on the resulting config, even if the function returns
+`DuckDBError`.
+
+* @param out_config The result configuration object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_create_config(duckdb_config *out_config);
+
+/*!
+This returns the total amount of configuration options available for usage with `duckdb_get_config_flag`.
+
+This should not be called in a loop as it internally loops over all the options.
+
+* @return The amount of config options available.
+*/
+DUCKDB_C_API size_t duckdb_config_count();
+
+/*!
+Obtains a human-readable name and description of a specific configuration option. This can be used to e.g.
+display configuration options. This will succeed unless `index` is out of range (i.e. `>= duckdb_config_count`).
+
+The result name or description MUST NOT be freed.
+
+* @param index The index of the configuration option (between 0 and `duckdb_config_count`)
+* @param out_name A name of the configuration flag.
+* @param out_description A description of the configuration flag.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_get_config_flag(size_t index, const char **out_name, const char **out_description);
+
+/*!
+Sets the specified option for the specified configuration. The configuration option is indicated by name.
+To obtain a list of config options, see `duckdb_get_config_flag`.
+
+In the source code, configuration options are defined in `config.cpp`.
+
+This can fail if either the name is invalid, or if the value provided for the option is invalid.
+
+* @param config The configuration object to set the option on.
+* @param name The name of the configuration flag to set.
+* @param option The value to set the configuration flag to.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_set_config(duckdb_config config, const char *name, const char *option);
+
+/*!
+Destroys the specified configuration object and de-allocates all memory allocated for the object.
+
+* @param config The configuration object to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_config(duckdb_config *config);
+
+//===--------------------------------------------------------------------===//
+// Error Data
+//===--------------------------------------------------------------------===//
+
+// Functions that can throw DuckDB errors must return duckdb_error_data.
+// Please use this interface for all new functions, as it deprecates all previous error handling approaches.
+
+/*!
+Creates duckdb_error_data.
+Must be destroyed with `duckdb_destroy_error_data`.
+
+* @param type The error type.
+* @param message The error message.
+* @return The error data.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_create_error_data(duckdb_error_type type, const char *message);
+
+/*!
+Destroys the error data and deallocates its memory.
+
+* @param error_data The error data to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_error_data(duckdb_error_data *error_data);
+
+/*!
+Returns the duckdb_error_type of the error data.
+
+* @param error_data The error data.
+* @return The error type.
+*/
+DUCKDB_C_API duckdb_error_type duckdb_error_data_error_type(duckdb_error_data error_data);
+
+/*!
+Returns the error message of the error data. Must not be freed.
+
+* @param error_data The error data.
+* @return The error message.
+*/
+DUCKDB_C_API const char *duckdb_error_data_message(duckdb_error_data error_data);
+
+/*!
+Returns whether the error data contains an error or not.
+
+* @param error_data The error data.
+* @return True, if the error data contains an exception, else false.
+*/
+DUCKDB_C_API bool duckdb_error_data_has_error(duckdb_error_data error_data);
+
+//===--------------------------------------------------------------------===//
+// Query Execution
+//===--------------------------------------------------------------------===//
+
+/*!
+Executes a SQL query within a connection and stores the full (materialized) result in the out_result pointer.
+If the query fails to execute, DuckDBError is returned and the error message can be retrieved by calling
+`duckdb_result_error`.
+
+Note that after running `duckdb_query`, `duckdb_destroy_result` must be called on the result object even if the
+query fails, otherwise the error stored within the result will not be freed correctly.
+
+* @param connection The connection to perform the query in.
+* @param query The SQL query to run.
+* @param out_result The query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_query(duckdb_connection connection, const char *query, duckdb_result *out_result);
+
+/*!
+Closes the result and de-allocates all memory allocated for that result.
+
+* @param result The result to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_result(duckdb_result *result);
+
+/*!
+Returns the column name of the specified column. The result should not need to be freed; the column names will
+automatically be destroyed when the result is destroyed.
+
+Returns `NULL` if the column is out of range.
+
+* @param result The result object to fetch the column name from.
+* @param col The column index.
+* @return The column name of the specified column.
+*/
+DUCKDB_C_API const char *duckdb_column_name(duckdb_result *result, idx_t col);
+
+/*!
+Returns the column type of the specified column.
+
+Returns `DUCKDB_TYPE_INVALID` if the column is out of range.
+
+* @param result The result object to fetch the column type from.
+* @param col The column index.
+* @return The column type of the specified column.
+*/
+DUCKDB_C_API duckdb_type duckdb_column_type(duckdb_result *result, idx_t col);
+
+/*!
+Returns the statement type of the statement that was executed
+
+* @param result The result object to fetch the statement type from.
+* @return duckdb_statement_type value or DUCKDB_STATEMENT_TYPE_INVALID
+*/
+DUCKDB_C_API duckdb_statement_type duckdb_result_statement_type(duckdb_result result);
+
+/*!
+Returns the logical column type of the specified column.
+
+The return type of this call should be destroyed with `duckdb_destroy_logical_type`.
+
+Returns `NULL` if the column is out of range.
+
+* @param result The result object to fetch the column type from.
+* @param col The column index.
+* @return The logical column type of the specified column.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_column_logical_type(duckdb_result *result, idx_t col);
+
+/*!
+Returns the arrow options associated with the given result. These options are definitions of how the arrow arrays/schema
+should be produced.
+* @param result The result object to fetch arrow options from.
+* @return The arrow options associated with the given result. This must be destroyed with
+`duckdb_destroy_arrow_options`.
+*/
+DUCKDB_C_API duckdb_arrow_options duckdb_result_get_arrow_options(duckdb_result *result);
+
+/*!
+Returns the number of columns present in a the result object.
+
+* @param result The result object.
+* @return The number of columns present in the result object.
+*/
+DUCKDB_C_API idx_t duckdb_column_count(duckdb_result *result);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Returns the number of rows present in the result object.
+
+* @param result The result object.
+* @return The number of rows present in the result object.
+*/
+DUCKDB_C_API idx_t duckdb_row_count(duckdb_result *result);
+
+#endif
+
+/*!
+Returns the number of rows changed by the query stored in the result. This is relevant only for INSERT/UPDATE/DELETE
+queries. For other queries the rows_changed will be 0.
+
+* @param result The result object.
+* @return The number of rows changed.
+*/
+DUCKDB_C_API idx_t duckdb_rows_changed(duckdb_result *result);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATED**: Prefer using `duckdb_result_get_chunk` instead.
+
+Returns the data of a specific column of a result in columnar format.
+
+The function returns a dense array which contains the result data. The exact type stored in the array depends on the
+corresponding duckdb_type (as provided by `duckdb_column_type`). For the exact type by which the data should be
+accessed, see the comments in [the types section](types) or the `DUCKDB_TYPE` enum.
+
+For example, for a column of type `DUCKDB_TYPE_INTEGER`, rows can be accessed in the following manner:
+```c
+int32_t *data = (int32_t *) duckdb_column_data(&result, 0);
+printf("Data for row %d: %d\n", row, data[row]);
+```
+
+* @param result The result object to fetch the column data from.
+* @param col The column index.
+* @return The column data of the specified column.
+*/
+DUCKDB_C_API void *duckdb_column_data(duckdb_result *result, idx_t col);
+
+/*!
+**DEPRECATED**: Prefer using `duckdb_result_get_chunk` instead.
+
+Returns the nullmask of a specific column of a result in columnar format. The nullmask indicates for every row
+whether or not the corresponding row is `NULL`. If a row is `NULL`, the values present in the array provided
+by `duckdb_column_data` are undefined.
+
+```c
+int32_t *data = (int32_t *) duckdb_column_data(&result, 0);
+bool *nullmask = duckdb_nullmask_data(&result, 0);
+if (nullmask[row]) {
+    printf("Data for row %d: NULL\n", row);
+} else {
+    printf("Data for row %d: %d\n", row, data[row]);
+}
+```
+
+* @param result The result object to fetch the nullmask from.
+* @param col The column index.
+* @return The nullmask of the specified column.
+*/
+DUCKDB_C_API bool *duckdb_nullmask_data(duckdb_result *result, idx_t col);
+
+#endif
+
+/*!
+Returns the error message contained within the result. The error is only set if `duckdb_query` returns `DuckDBError`.
+
+The result of this function must not be freed. It will be cleaned up when `duckdb_destroy_result` is called.
+
+* @param result The result object to fetch the error from.
+* @return The error of the result.
+*/
+DUCKDB_C_API const char *duckdb_result_error(duckdb_result *result);
+
+/*!
+Returns the result error type contained within the result. The error is only set if `duckdb_query` returns
+`DuckDBError`.
+
+* @param result The result object to fetch the error from.
+* @return The error type of the result.
+*/
+DUCKDB_C_API duckdb_error_type duckdb_result_error_type(duckdb_result *result);
+
+//===--------------------------------------------------------------------===//
+// Result Functions
+//===--------------------------------------------------------------------===//
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Fetches a data chunk from the duckdb_result. This function should be called repeatedly until the result is exhausted.
+
+The result must be destroyed with `duckdb_destroy_data_chunk`.
+
+This function supersedes all `duckdb_value` functions, as well as the `duckdb_column_data` and `duckdb_nullmask_data`
+functions. It results in significantly better performance, and should be preferred in newer code-bases.
+
+If this function is used, none of the other result functions can be used and vice versa (i.e. this function cannot be
+mixed with the legacy result functions).
+
+Use `duckdb_result_chunk_count` to figure out how many chunks there are in the result.
+
+* @param result The result object to fetch the data chunk from.
+* @param chunk_index The chunk index to fetch from.
+* @return The resulting data chunk. Returns `NULL` if the chunk index is out of bounds.
+*/
+DUCKDB_C_API duckdb_data_chunk duckdb_result_get_chunk(duckdb_result result, idx_t chunk_index);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Checks if the type of the internal result is StreamQueryResult.
+
+* @param result The result object to check.
+* @return Whether or not the result object is of the type StreamQueryResult
+*/
+DUCKDB_C_API bool duckdb_result_is_streaming(duckdb_result result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Returns the number of data chunks present in the result.
+
+* @param result The result object
+* @return Number of data chunks present in the result.
+*/
+DUCKDB_C_API idx_t duckdb_result_chunk_count(duckdb_result result);
+
+#endif
+
+/*!
+Returns the return_type of the given result, or DUCKDB_RETURN_TYPE_INVALID on error
+
+* @param result The result object
+* @return The return_type
+*/
+DUCKDB_C_API duckdb_result_type duckdb_result_return_type(duckdb_result result);
+
+//===--------------------------------------------------------------------===//
+// Safe Fetch Functions
+//===--------------------------------------------------------------------===//
+
+// These functions will perform conversions if necessary.
+// On failure (e.g. if conversion cannot be performed or if the value is NULL) a default value is returned.
+// Note that these functions are slow since they perform bounds checking and conversion
+// For fast access of values prefer using `duckdb_result_get_chunk`
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The boolean value at the specified location, or false if the value cannot be converted.
+*/
+DUCKDB_C_API bool duckdb_value_boolean(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The int8_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API int8_t duckdb_value_int8(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The int16_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API int16_t duckdb_value_int16(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The int32_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API int32_t duckdb_value_int32(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The int64_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API int64_t duckdb_value_int64(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_hugeint value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_hugeint duckdb_value_hugeint(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_uhugeint value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_uhugeint duckdb_value_uhugeint(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_decimal value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_decimal duckdb_value_decimal(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The uint8_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API uint8_t duckdb_value_uint8(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The uint16_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API uint16_t duckdb_value_uint16(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The uint32_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API uint32_t duckdb_value_uint32(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The uint64_t value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API uint64_t duckdb_value_uint64(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The float value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API float duckdb_value_float(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The double value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API double duckdb_value_double(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_date value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_date duckdb_value_date(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_time value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_time duckdb_value_time(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_timestamp value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_timestamp duckdb_value_timestamp(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_interval value at the specified location, or 0 if the value cannot be converted.
+*/
+DUCKDB_C_API duckdb_interval duckdb_value_interval(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATED**: Use duckdb_value_string instead. This function does not work correctly if the string contains null
+bytes.
+
+* @return The text value at the specified location as a null-terminated string, or nullptr if the value cannot be
+converted. The result must be freed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_value_varchar(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+No support for nested types, and for other complex types.
+The resulting field "string.data" must be freed with `duckdb_free.`
+
+* @return The string value at the specified location. Attempts to cast the result value to string.
+*/
+DUCKDB_C_API duckdb_string duckdb_value_string(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATED**: Use duckdb_value_string_internal instead. This function does not work correctly if the string contains
+null bytes.
+
+* @return The char* value at the specified location. ONLY works on VARCHAR columns and does not auto-cast.
+If the column is NOT a VARCHAR column this function will return NULL.
+
+The result must NOT be freed.
+*/
+DUCKDB_C_API char *duckdb_value_varchar_internal(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATED**: Use duckdb_value_string_internal instead. This function does not work correctly if the string contains
+null bytes.
+* @return The char* value at the specified location. ONLY works on VARCHAR columns and does not auto-cast.
+If the column is NOT a VARCHAR column this function will return NULL.
+
+The result must NOT be freed.
+*/
+DUCKDB_C_API duckdb_string duckdb_value_string_internal(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return The duckdb_blob value at the specified location. Returns a blob with blob.data set to nullptr if the
+value cannot be converted. The resulting field "blob.data" must be freed with `duckdb_free.`
+*/
+DUCKDB_C_API duckdb_blob duckdb_value_blob(duckdb_result *result, idx_t col, idx_t row);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+* @return Returns true if the value at the specified index is NULL, and false otherwise.
+*/
+DUCKDB_C_API bool duckdb_value_is_null(duckdb_result *result, idx_t col, idx_t row);
+
+#endif
+
+//===--------------------------------------------------------------------===//
+// Helpers
+//===--------------------------------------------------------------------===//
+
+/*!
+Allocate `size` bytes of memory using the duckdb internal malloc function. Any memory allocated in this manner
+should be freed using `duckdb_free`.
+
+* @param size The number of bytes to allocate.
+* @return A pointer to the allocated memory region.
+*/
+DUCKDB_C_API void *duckdb_malloc(size_t size);
+
+/*!
+Free a value returned from `duckdb_malloc`, `duckdb_value_varchar`, `duckdb_value_blob`, or
+`duckdb_value_string`.
+
+* @param ptr The memory region to de-allocate.
+*/
+DUCKDB_C_API void duckdb_free(void *ptr);
+
+/*!
+The internal vector size used by DuckDB.
+This is the amount of tuples that will fit into a data chunk created by `duckdb_create_data_chunk`.
+
+* @return The vector size.
+*/
+DUCKDB_C_API idx_t duckdb_vector_size();
+
+/*!
+Whether or not the duckdb_string_t value is inlined.
+This means that the data of the string does not have a separate allocation.
+
+*/
+DUCKDB_C_API bool duckdb_string_is_inlined(duckdb_string_t string);
+
+/*!
+Get the string length of a string_t
+
+* @param string The string to get the length of.
+* @return The length.
+*/
+DUCKDB_C_API uint32_t duckdb_string_t_length(duckdb_string_t string);
+
+/*!
+Get a pointer to the string data of a string_t
+
+* @param string The string to get the pointer to.
+* @return The pointer.
+*/
+DUCKDB_C_API const char *duckdb_string_t_data(duckdb_string_t *string);
+
+//===--------------------------------------------------------------------===//
+// Date Time Timestamp Helpers
+//===--------------------------------------------------------------------===//
+
+/*!
+Decompose a `duckdb_date` object into year, month and date (stored as `duckdb_date_struct`).
+
+* @param date The date object, as obtained from a `DUCKDB_TYPE_DATE` column.
+* @return The `duckdb_date_struct` with the decomposed elements.
+*/
+DUCKDB_C_API duckdb_date_struct duckdb_from_date(duckdb_date date);
+
+/*!
+Re-compose a `duckdb_date` from year, month and date (`duckdb_date_struct`).
+
+* @param date The year, month and date stored in a `duckdb_date_struct`.
+* @return The `duckdb_date` element.
+*/
+DUCKDB_C_API duckdb_date duckdb_to_date(duckdb_date_struct date);
+
+/*!
+Test a `duckdb_date` to see if it is a finite value.
+
+* @param date The date object, as obtained from a `DUCKDB_TYPE_DATE` column.
+* @return True if the date is finite, false if it is ±infinity.
+*/
+DUCKDB_C_API bool duckdb_is_finite_date(duckdb_date date);
+
+/*!
+Decompose a `duckdb_time` object into hour, minute, second and microsecond (stored as `duckdb_time_struct`).
+
+* @param time The time object, as obtained from a `DUCKDB_TYPE_TIME` column.
+* @return The `duckdb_time_struct` with the decomposed elements.
+*/
+DUCKDB_C_API duckdb_time_struct duckdb_from_time(duckdb_time time);
+
+/*!
+Create a `duckdb_time_tz` object from micros and a timezone offset.
+
+* @param micros The microsecond component of the time.
+* @param offset The timezone offset component of the time.
+* @return The `duckdb_time_tz` element.
+*/
+DUCKDB_C_API duckdb_time_tz duckdb_create_time_tz(int64_t micros, int32_t offset);
+
+/*!
+Decompose a TIME_TZ objects into micros and a timezone offset.
+
+Use `duckdb_from_time` to further decompose the micros into hour, minute, second and microsecond.
+
+* @param micros The time object, as obtained from a `DUCKDB_TYPE_TIME_TZ` column.
+*/
+DUCKDB_C_API duckdb_time_tz_struct duckdb_from_time_tz(duckdb_time_tz micros);
+
+/*!
+Re-compose a `duckdb_time` from hour, minute, second and microsecond (`duckdb_time_struct`).
+
+* @param time The hour, minute, second and microsecond in a `duckdb_time_struct`.
+* @return The `duckdb_time` element.
+*/
+DUCKDB_C_API duckdb_time duckdb_to_time(duckdb_time_struct time);
+
+/*!
+Decompose a `duckdb_timestamp` object into a `duckdb_timestamp_struct`.
+
+* @param ts The ts object, as obtained from a `DUCKDB_TYPE_TIMESTAMP` column.
+* @return The `duckdb_timestamp_struct` with the decomposed elements.
+*/
+DUCKDB_C_API duckdb_timestamp_struct duckdb_from_timestamp(duckdb_timestamp ts);
+
+/*!
+Re-compose a `duckdb_timestamp` from a duckdb_timestamp_struct.
+
+* @param ts The de-composed elements in a `duckdb_timestamp_struct`.
+* @return The `duckdb_timestamp` element.
+*/
+DUCKDB_C_API duckdb_timestamp duckdb_to_timestamp(duckdb_timestamp_struct ts);
+
+/*!
+Test a `duckdb_timestamp` to see if it is a finite value.
+
+* @param ts The duckdb_timestamp object, as obtained from a `DUCKDB_TYPE_TIMESTAMP` column.
+* @return True if the timestamp is finite, false if it is ±infinity.
+*/
+DUCKDB_C_API bool duckdb_is_finite_timestamp(duckdb_timestamp ts);
+
+/*!
+Test a `duckdb_timestamp_s` to see if it is a finite value.
+
+* @param ts The duckdb_timestamp_s object, as obtained from a `DUCKDB_TYPE_TIMESTAMP_S` column.
+* @return True if the timestamp is finite, false if it is ±infinity.
+*/
+DUCKDB_C_API bool duckdb_is_finite_timestamp_s(duckdb_timestamp_s ts);
+
+/*!
+Test a `duckdb_timestamp_ms` to see if it is a finite value.
+
+* @param ts The duckdb_timestamp_ms object, as obtained from a `DUCKDB_TYPE_TIMESTAMP_MS` column.
+* @return True if the timestamp is finite, false if it is ±infinity.
+*/
+DUCKDB_C_API bool duckdb_is_finite_timestamp_ms(duckdb_timestamp_ms ts);
+
+/*!
+Test a `duckdb_timestamp_ns` to see if it is a finite value.
+
+* @param ts The duckdb_timestamp_ns object, as obtained from a `DUCKDB_TYPE_TIMESTAMP_NS` column.
+* @return True if the timestamp is finite, false if it is ±infinity.
+*/
+DUCKDB_C_API bool duckdb_is_finite_timestamp_ns(duckdb_timestamp_ns ts);
+
+//===--------------------------------------------------------------------===//
+// Hugeint Helpers
+//===--------------------------------------------------------------------===//
+
+/*!
+Converts a duckdb_hugeint object (as obtained from a `DUCKDB_TYPE_HUGEINT` column) into a double.
+
+* @param val The hugeint value.
+* @return The converted `double` element.
+*/
+DUCKDB_C_API double duckdb_hugeint_to_double(duckdb_hugeint val);
+
+/*!
+Converts a double value to a duckdb_hugeint object.
+
+If the conversion fails because the double value is too big the result will be 0.
+
+* @param val The double value.
+* @return The converted `duckdb_hugeint` element.
+*/
+DUCKDB_C_API duckdb_hugeint duckdb_double_to_hugeint(double val);
+
+//===--------------------------------------------------------------------===//
+// Unsigned Hugeint Helpers
+//===--------------------------------------------------------------------===//
+
+/*!
+Converts a duckdb_uhugeint object (as obtained from a `DUCKDB_TYPE_UHUGEINT` column) into a double.
+
+* @param val The uhugeint value.
+* @return The converted `double` element.
+*/
+DUCKDB_C_API double duckdb_uhugeint_to_double(duckdb_uhugeint val);
+
+/*!
+Converts a double value to a duckdb_uhugeint object.
+
+If the conversion fails because the double value is too big the result will be 0.
+
+* @param val The double value.
+* @return The converted `duckdb_uhugeint` element.
+*/
+DUCKDB_C_API duckdb_uhugeint duckdb_double_to_uhugeint(double val);
+
+//===--------------------------------------------------------------------===//
+// Decimal Helpers
+//===--------------------------------------------------------------------===//
+
+/*!
+Converts a double value to a duckdb_decimal object.
+
+If the conversion fails because the double value is too big, or the width/scale are invalid the result will be 0.
+
+* @param val The double value.
+* @return The converted `duckdb_decimal` element.
+*/
+DUCKDB_C_API duckdb_decimal duckdb_double_to_decimal(double val, uint8_t width, uint8_t scale);
+
+/*!
+Converts a duckdb_decimal object (as obtained from a `DUCKDB_TYPE_DECIMAL` column) into a double.
+
+* @param val The decimal value.
+* @return The converted `double` element.
+*/
+DUCKDB_C_API double duckdb_decimal_to_double(duckdb_decimal val);
+
+//===--------------------------------------------------------------------===//
+// Prepared Statements
+//===--------------------------------------------------------------------===//
+
+// A prepared statement is a parameterized query that allows you to bind parameters to it.
+// * This is useful to easily supply parameters to functions and avoid SQL injection attacks.
+// * This is useful to speed up queries that you will execute several times with different parameters.
+// Because the query will only be parsed, bound, optimized and planned once during the prepare stage,
+// rather than once per execution.
+// For example:
+//   SELECT * FROM tbl WHERE id=?
+// Or a query with multiple parameters:
+//   SELECT * FROM tbl WHERE id=$1 OR name=$2
+/*!
+Create a prepared statement object from a query.
+
+Note that after calling `duckdb_prepare`, the prepared statement should always be destroyed using
+`duckdb_destroy_prepare`, even if the prepare fails.
+
+If the prepare fails, `duckdb_prepare_error` can be called to obtain the reason why the prepare failed.
+
+* @param connection The connection object
+* @param query The SQL query to prepare
+* @param out_prepared_statement The resulting prepared statement object
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_prepare(duckdb_connection connection, const char *query,
+                                         duckdb_prepared_statement *out_prepared_statement);
+
+/*!
+Closes the prepared statement and de-allocates all memory allocated for the statement.
+
+* @param prepared_statement The prepared statement to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_prepare(duckdb_prepared_statement *prepared_statement);
+
+/*!
+Returns the error message associated with the given prepared statement.
+If the prepared statement has no error message, this returns `nullptr` instead.
+
+The error message should not be freed. It will be de-allocated when `duckdb_destroy_prepare` is called.
+
+* @param prepared_statement The prepared statement to obtain the error from.
+* @return The error message, or `nullptr` if there is none.
+*/
+DUCKDB_C_API const char *duckdb_prepare_error(duckdb_prepared_statement prepared_statement);
+
+/*!
+Returns the number of parameters that can be provided to the given prepared statement.
+
+Returns 0 if the query was not successfully prepared.
+
+* @param prepared_statement The prepared statement to obtain the number of parameters for.
+*/
+DUCKDB_C_API idx_t duckdb_nparams(duckdb_prepared_statement prepared_statement);
+
+/*!
+Returns the name used to identify the parameter
+The returned string should be freed using `duckdb_free`.
+
+Returns NULL if the index is out of range for the provided prepared statement.
+
+* @param prepared_statement The prepared statement for which to get the parameter name from.
+*/
+DUCKDB_C_API const char *duckdb_parameter_name(duckdb_prepared_statement prepared_statement, idx_t index);
+
+/*!
+Returns the parameter type for the parameter at the given index.
+
+Returns `DUCKDB_TYPE_INVALID` if the parameter index is out of range or the statement was not successfully prepared.
+
+* @param prepared_statement The prepared statement.
+* @param param_idx The parameter index.
+* @return The parameter type
+*/
+DUCKDB_C_API duckdb_type duckdb_param_type(duckdb_prepared_statement prepared_statement, idx_t param_idx);
+
+/*!
+Returns the logical type for the parameter at the given index.
+
+Returns `nullptr` if the parameter index is out of range or the statement was not successfully prepared.
+
+The return type of this call should be destroyed with `duckdb_destroy_logical_type`.
+
+* @param prepared_statement The prepared statement.
+* @param param_idx The parameter index.
+* @return The logical type of the parameter
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_param_logical_type(duckdb_prepared_statement prepared_statement,
+                                                           idx_t param_idx);
+
+/*!
+Clear the params bind to the prepared statement.
+*/
+DUCKDB_C_API duckdb_state duckdb_clear_bindings(duckdb_prepared_statement prepared_statement);
+
+/*!
+Returns the statement type of the statement to be executed
+
+* @param statement The prepared statement.
+* @return duckdb_statement_type value or DUCKDB_STATEMENT_TYPE_INVALID
+*/
+DUCKDB_C_API duckdb_statement_type duckdb_prepared_statement_type(duckdb_prepared_statement statement);
+
+/*!
+Returns the number of columns present in a the result of the prepared statement. If any of the column types are invalid,
+the result will be 1.
+
+* @param prepared_statement The prepared statement.
+* @return The number of columns present in the result of the prepared statement.
+*/
+DUCKDB_C_API idx_t duckdb_prepared_statement_column_count(duckdb_prepared_statement prepared_statement);
+
+/*!
+Returns the name of the specified column of the result of the prepared_statement.
+The returned string should be freed using `duckdb_free`.
+
+Returns `nullptr` if the column is out of range.
+
+* @param prepared_statement The prepared statement.
+* @param col_idx The column index.
+* @return The column name of the specified column.
+*/
+DUCKDB_C_API const char *duckdb_prepared_statement_column_name(duckdb_prepared_statement prepared_statement,
+                                                               idx_t col_idx);
+
+/*!
+Returns the column type of the specified column of the result of the prepared_statement.
+
+Returns `DUCKDB_TYPE_INVALID` if the column is out of range.
+The return type of this call should be destroyed with `duckdb_destroy_logical_type`.
+
+* @param prepared_statement The prepared statement to fetch the column type from.
+* @param col_idx The column index.
+* @return The logical type of the specified column.
+*/
+DUCKDB_C_API duckdb_logical_type
+duckdb_prepared_statement_column_logical_type(duckdb_prepared_statement prepared_statement, idx_t col_idx);
+
+/*!
+Returns the column type of the specified column of the result of the prepared_statement.
+
+Returns `DUCKDB_TYPE_INVALID` if the column is out of range.
+
+* @param prepared_statement The prepared statement to fetch the column type from.
+* @param col_idx The column index.
+* @return The type of the specified column.
+*/
+DUCKDB_C_API duckdb_type duckdb_prepared_statement_column_type(duckdb_prepared_statement prepared_statement,
+                                                               idx_t col_idx);
+
+//===--------------------------------------------------------------------===//
+// Bind Values to Prepared Statements
+//===--------------------------------------------------------------------===//
+
+/*!
+Binds a value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_value(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                            duckdb_value val);
+
+/*!
+Retrieve the index of the parameter for the prepared statement, identified by name
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_parameter_index(duckdb_prepared_statement prepared_statement,
+                                                      idx_t *param_idx_out, const char *name);
+
+/*!
+Binds a bool value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_boolean(duckdb_prepared_statement prepared_statement, idx_t param_idx, bool val);
+
+/*!
+Binds an int8_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_int8(duckdb_prepared_statement prepared_statement, idx_t param_idx, int8_t val);
+
+/*!
+Binds an int16_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_int16(duckdb_prepared_statement prepared_statement, idx_t param_idx, int16_t val);
+
+/*!
+Binds an int32_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_int32(duckdb_prepared_statement prepared_statement, idx_t param_idx, int32_t val);
+
+/*!
+Binds an int64_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_int64(duckdb_prepared_statement prepared_statement, idx_t param_idx, int64_t val);
+
+/*!
+Binds a duckdb_hugeint value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_hugeint(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                              duckdb_hugeint val);
+
+/*!
+Binds a duckdb_uhugeint value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_uhugeint(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                               duckdb_uhugeint val);
+
+/*!
+Binds a duckdb_decimal value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_decimal(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                              duckdb_decimal val);
+
+/*!
+Binds a uint8_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_uint8(duckdb_prepared_statement prepared_statement, idx_t param_idx, uint8_t val);
+
+/*!
+Binds a uint16_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_uint16(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                             uint16_t val);
+
+/*!
+Binds a uint32_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_uint32(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                             uint32_t val);
+
+/*!
+Binds a uint64_t value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_uint64(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                             uint64_t val);
+
+/*!
+Binds a float value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_float(duckdb_prepared_statement prepared_statement, idx_t param_idx, float val);
+
+/*!
+Binds a double value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_double(duckdb_prepared_statement prepared_statement, idx_t param_idx, double val);
+
+/*!
+Binds a duckdb_date value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_date(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                           duckdb_date val);
+
+/*!
+Binds a duckdb_time value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_time(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                           duckdb_time val);
+
+/*!
+Binds a duckdb_timestamp value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_timestamp(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                                duckdb_timestamp val);
+
+/*!
+Binds a duckdb_timestamp value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_timestamp_tz(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                                   duckdb_timestamp val);
+
+/*!
+Binds a duckdb_interval value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_interval(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                               duckdb_interval val);
+
+/*!
+Binds a null-terminated varchar value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_varchar(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                              const char *val);
+
+/*!
+Binds a varchar value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_varchar_length(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                                     const char *val, idx_t length);
+
+/*!
+Binds a blob value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_blob(duckdb_prepared_statement prepared_statement, idx_t param_idx,
+                                           const void *data, idx_t length);
+
+/*!
+Binds a NULL value to the prepared statement at the specified index.
+*/
+DUCKDB_C_API duckdb_state duckdb_bind_null(duckdb_prepared_statement prepared_statement, idx_t param_idx);
+
+//===--------------------------------------------------------------------===//
+// Execute Prepared Statements
+//===--------------------------------------------------------------------===//
+
+/*!
+Executes the prepared statement with the given bound parameters, and returns a materialized query result.
+
+This method can be called multiple times for each prepared statement, and the parameters can be modified
+between calls to this function.
+
+Note that the result must be freed with `duckdb_destroy_result`.
+
+* @param prepared_statement The prepared statement to execute.
+* @param out_result The query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_execute_prepared(duckdb_prepared_statement prepared_statement,
+                                                  duckdb_result *out_result);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Executes the prepared statement with the given bound parameters, and returns an optionally-streaming query result.
+To determine if the resulting query was in fact streamed, use `duckdb_result_is_streaming`
+
+This method can be called multiple times for each prepared statement, and the parameters can be modified
+between calls to this function.
+
+Note that the result must be freed with `duckdb_destroy_result`.
+
+* @param prepared_statement The prepared statement to execute.
+* @param out_result The query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_execute_prepared_streaming(duckdb_prepared_statement prepared_statement,
+                                                            duckdb_result *out_result);
+
+#endif
+
+//===--------------------------------------------------------------------===//
+// Extract Statements
+//===--------------------------------------------------------------------===//
+
+// A query string can be extracted into multiple SQL statements. Each statement can be prepared and executed separately.
+/*!
+Extract all statements from a query.
+Note that after calling `duckdb_extract_statements`, the extracted statements should always be destroyed using
+`duckdb_destroy_extracted`, even if no statements were extracted.
+
+If the extract fails, `duckdb_extract_statements_error` can be called to obtain the reason why the extract failed.
+
+* @param connection The connection object
+* @param query The SQL query to extract
+* @param out_extracted_statements The resulting extracted statements object
+* @return The number of extracted statements or 0 on failure.
+*/
+DUCKDB_C_API idx_t duckdb_extract_statements(duckdb_connection connection, const char *query,
+                                             duckdb_extracted_statements *out_extracted_statements);
+
+/*!
+Prepare an extracted statement.
+Note that after calling `duckdb_prepare_extracted_statement`, the prepared statement should always be destroyed using
+`duckdb_destroy_prepare`, even if the prepare fails.
+
+If the prepare fails, `duckdb_prepare_error` can be called to obtain the reason why the prepare failed.
+
+* @param connection The connection object
+* @param extracted_statements The extracted statements object
+* @param index The index of the extracted statement to prepare
+* @param out_prepared_statement The resulting prepared statement object
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_prepare_extracted_statement(duckdb_connection connection,
+                                                             duckdb_extracted_statements extracted_statements,
+                                                             idx_t index,
+                                                             duckdb_prepared_statement *out_prepared_statement);
+
+/*!
+Returns the error message contained within the extracted statements.
+The result of this function must not be freed. It will be cleaned up when `duckdb_destroy_extracted` is called.
+
+* @param extracted_statements The extracted statements to fetch the error from.
+* @return The error of the extracted statements.
+*/
+DUCKDB_C_API const char *duckdb_extract_statements_error(duckdb_extracted_statements extracted_statements);
+
+/*!
+De-allocates all memory allocated for the extracted statements.
+* @param extracted_statements The extracted statements to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_extracted(duckdb_extracted_statements *extracted_statements);
+
+//===--------------------------------------------------------------------===//
+// Pending Result Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Executes the prepared statement with the given bound parameters, and returns a pending result.
+The pending result represents an intermediate structure for a query that is not yet fully executed.
+The pending result can be used to incrementally execute a query, returning control to the client between tasks.
+
+Note that after calling `duckdb_pending_prepared`, the pending result should always be destroyed using
+`duckdb_destroy_pending`, even if this function returns DuckDBError.
+
+* @param prepared_statement The prepared statement to execute.
+* @param out_result The pending query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_pending_prepared(duckdb_prepared_statement prepared_statement,
+                                                  duckdb_pending_result *out_result);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Executes the prepared statement with the given bound parameters, and returns a pending result.
+This pending result will create a streaming duckdb_result when executed.
+The pending result represents an intermediate structure for a query that is not yet fully executed.
+
+Note that after calling `duckdb_pending_prepared_streaming`, the pending result should always be destroyed using
+`duckdb_destroy_pending`, even if this function returns DuckDBError.
+
+* @param prepared_statement The prepared statement to execute.
+* @param out_result The pending query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_pending_prepared_streaming(duckdb_prepared_statement prepared_statement,
+                                                            duckdb_pending_result *out_result);
+
+#endif
+
+/*!
+Closes the pending result and de-allocates all memory allocated for the result.
+
+* @param pending_result The pending result to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_pending(duckdb_pending_result *pending_result);
+
+/*!
+Returns the error message contained within the pending result.
+
+The result of this function must not be freed. It will be cleaned up when `duckdb_destroy_pending` is called.
+
+* @param pending_result The pending result to fetch the error from.
+* @return The error of the pending result.
+*/
+DUCKDB_C_API const char *duckdb_pending_error(duckdb_pending_result pending_result);
+
+/*!
+Executes a single task within the query, returning whether or not the query is ready.
+
+If this returns DUCKDB_PENDING_RESULT_READY, the duckdb_execute_pending function can be called to obtain the result.
+If this returns DUCKDB_PENDING_RESULT_NOT_READY, the duckdb_pending_execute_task function should be called again.
+If this returns DUCKDB_PENDING_ERROR, an error occurred during execution.
+
+The error message can be obtained by calling duckdb_pending_error on the pending_result.
+
+* @param pending_result The pending result to execute a task within.
+* @return The state of the pending result after the execution.
+*/
+DUCKDB_C_API duckdb_pending_state duckdb_pending_execute_task(duckdb_pending_result pending_result);
+
+/*!
+If this returns DUCKDB_PENDING_RESULT_READY, the duckdb_execute_pending function can be called to obtain the result.
+If this returns DUCKDB_PENDING_RESULT_NOT_READY, the duckdb_pending_execute_check_state function should be called again.
+If this returns DUCKDB_PENDING_ERROR, an error occurred during execution.
+
+The error message can be obtained by calling duckdb_pending_error on the pending_result.
+
+* @param pending_result The pending result.
+* @return The state of the pending result.
+*/
+DUCKDB_C_API duckdb_pending_state duckdb_pending_execute_check_state(duckdb_pending_result pending_result);
+
+/*!
+Fully execute a pending query result, returning the final query result.
+
+If duckdb_pending_execute_task has been called until DUCKDB_PENDING_RESULT_READY was returned, this will return fast.
+Otherwise, all remaining tasks must be executed first.
+
+Note that the result must be freed with `duckdb_destroy_result`.
+
+* @param pending_result The pending result to execute.
+* @param out_result The result object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_execute_pending(duckdb_pending_result pending_result, duckdb_result *out_result);
+
+/*!
+Returns whether a duckdb_pending_state is finished executing. For example if `pending_state` is
+DUCKDB_PENDING_RESULT_READY, this function will return true.
+
+* @param pending_state The pending state on which to decide whether to finish execution.
+* @return Boolean indicating pending execution should be considered finished.
+*/
+DUCKDB_C_API bool duckdb_pending_execution_is_finished(duckdb_pending_state pending_state);
+
+//===--------------------------------------------------------------------===//
+// Value Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Destroys the value and de-allocates all memory allocated for that type.
+
+* @param value The value to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_value(duckdb_value *value);
+
+/*!
+Creates a value from a null-terminated string
+
+* @param text The null-terminated string
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_varchar(const char *text);
+
+/*!
+Creates a value from a string
+
+* @param text The text
+* @param length The length of the text
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_varchar_length(const char *text, idx_t length);
+
+/*!
+Creates a value from a boolean
+
+* @param input The boolean value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_bool(bool input);
+
+/*!
+Creates a value from an int8_t (a tinyint)
+
+* @param input The tinyint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_int8(int8_t input);
+
+/*!
+Creates a value from a uint8_t (a utinyint)
+
+* @param input The utinyint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uint8(uint8_t input);
+
+/*!
+Creates a value from an int16_t (a smallint)
+
+* @param input The smallint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_int16(int16_t input);
+
+/*!
+Creates a value from a uint16_t (a usmallint)
+
+* @param input The usmallint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uint16(uint16_t input);
+
+/*!
+Creates a value from an int32_t (an integer)
+
+* @param input The integer value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_int32(int32_t input);
+
+/*!
+Creates a value from a uint32_t (a uinteger)
+
+* @param input The uinteger value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uint32(uint32_t input);
+
+/*!
+Creates a value from a uint64_t (a ubigint)
+
+* @param input The ubigint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uint64(uint64_t input);
+
+/*!
+Creates a value from an int64
+
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_int64(int64_t val);
+
+/*!
+Creates a value from a hugeint
+
+* @param input The hugeint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_hugeint(duckdb_hugeint input);
+
+/*!
+Creates a value from a uhugeint
+
+* @param input The uhugeint value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uhugeint(duckdb_uhugeint input);
+
+/*!
+Creates a BIGNUM value from a duckdb_bignum
+
+* @param input The duckdb_bignum value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_bignum(duckdb_bignum input);
+
+/*!
+Creates a DECIMAL value from a duckdb_decimal
+
+* @param input The duckdb_decimal value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_decimal(duckdb_decimal input);
+
+/*!
+Creates a value from a float
+
+* @param input The float value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_float(float input);
+
+/*!
+Creates a value from a double
+
+* @param input The double value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_double(double input);
+
+/*!
+Creates a value from a date
+
+* @param input The date value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_date(duckdb_date input);
+
+/*!
+Creates a value from a time
+
+* @param input The time value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_time(duckdb_time input);
+
+/*!
+Creates a value from a time_ns
+
+* @param input The time value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_time_ns(duckdb_time_ns input);
+
+/*!
+Creates a value from a time_tz.
+Not to be confused with `duckdb_create_time_tz`, which creates a duckdb_time_tz_t.
+
+* @param value The time_tz value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_time_tz_value(duckdb_time_tz value);
+
+/*!
+Creates a TIMESTAMP value from a duckdb_timestamp
+
+* @param input The duckdb_timestamp value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_timestamp(duckdb_timestamp input);
+
+/*!
+Creates a TIMESTAMP_TZ value from a duckdb_timestamp
+
+* @param input The duckdb_timestamp value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_timestamp_tz(duckdb_timestamp input);
+
+/*!
+Creates a TIMESTAMP_S value from a duckdb_timestamp_s
+
+* @param input The duckdb_timestamp_s value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_timestamp_s(duckdb_timestamp_s input);
+
+/*!
+Creates a TIMESTAMP_MS value from a duckdb_timestamp_ms
+
+* @param input The duckdb_timestamp_ms value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_timestamp_ms(duckdb_timestamp_ms input);
+
+/*!
+Creates a TIMESTAMP_NS value from a duckdb_timestamp_ns
+
+* @param input The duckdb_timestamp_ns value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_timestamp_ns(duckdb_timestamp_ns input);
+
+/*!
+Creates a value from an interval
+
+* @param input The interval value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_interval(duckdb_interval input);
+
+/*!
+Creates a value from a blob
+
+* @param data The blob data
+* @param length The length of the blob data
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_blob(const uint8_t *data, idx_t length);
+
+/*!
+Creates a BIT value from a duckdb_bit
+
+* @param input The duckdb_bit value
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_bit(duckdb_bit input);
+
+/*!
+Creates a UUID value from a uhugeint
+
+* @param input The duckdb_uhugeint containing the UUID
+* @return The value. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_uuid(duckdb_uhugeint input);
+
+/*!
+Returns the boolean value of the given value.
+
+* @param val A duckdb_value containing a boolean
+* @return A boolean, or false if the value cannot be converted
+*/
+DUCKDB_C_API bool duckdb_get_bool(duckdb_value val);
+
+/*!
+Returns the int8_t value of the given value.
+
+* @param val A duckdb_value containing a tinyint
+* @return A int8_t, or MinValue<int8> if the value cannot be converted
+*/
+DUCKDB_C_API int8_t duckdb_get_int8(duckdb_value val);
+
+/*!
+Returns the uint8_t value of the given value.
+
+* @param val A duckdb_value containing a utinyint
+* @return A uint8_t, or MinValue<uint8> if the value cannot be converted
+*/
+DUCKDB_C_API uint8_t duckdb_get_uint8(duckdb_value val);
+
+/*!
+Returns the int16_t value of the given value.
+
+* @param val A duckdb_value containing a smallint
+* @return A int16_t, or MinValue<int16> if the value cannot be converted
+*/
+DUCKDB_C_API int16_t duckdb_get_int16(duckdb_value val);
+
+/*!
+Returns the uint16_t value of the given value.
+
+* @param val A duckdb_value containing a usmallint
+* @return A uint16_t, or MinValue<uint16> if the value cannot be converted
+*/
+DUCKDB_C_API uint16_t duckdb_get_uint16(duckdb_value val);
+
+/*!
+Returns the int32_t value of the given value.
+
+* @param val A duckdb_value containing an integer
+* @return A int32_t, or MinValue<int32> if the value cannot be converted
+*/
+DUCKDB_C_API int32_t duckdb_get_int32(duckdb_value val);
+
+/*!
+Returns the uint32_t value of the given value.
+
+* @param val A duckdb_value containing a uinteger
+* @return A uint32_t, or MinValue<uint32> if the value cannot be converted
+*/
+DUCKDB_C_API uint32_t duckdb_get_uint32(duckdb_value val);
+
+/*!
+Returns the int64_t value of the given value.
+
+* @param val A duckdb_value containing a bigint
+* @return A int64_t, or MinValue<int64> if the value cannot be converted
+*/
+DUCKDB_C_API int64_t duckdb_get_int64(duckdb_value val);
+
+/*!
+Returns the uint64_t value of the given value.
+
+* @param val A duckdb_value containing a ubigint
+* @return A uint64_t, or MinValue<uint64> if the value cannot be converted
+*/
+DUCKDB_C_API uint64_t duckdb_get_uint64(duckdb_value val);
+
+/*!
+Returns the hugeint value of the given value.
+
+* @param val A duckdb_value containing a hugeint
+* @return A duckdb_hugeint, or MinValue<hugeint> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_hugeint duckdb_get_hugeint(duckdb_value val);
+
+/*!
+Returns the uhugeint value of the given value.
+
+* @param val A duckdb_value containing a uhugeint
+* @return A duckdb_uhugeint, or MinValue<uhugeint> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_uhugeint duckdb_get_uhugeint(duckdb_value val);
+
+/*!
+Returns the duckdb_bignum value of the given value.
+The `data` field must be destroyed with `duckdb_free`.
+
+* @param val A duckdb_value containing a BIGNUM
+* @return A duckdb_bignum. The `data` field must be destroyed with `duckdb_free`.
+*/
+DUCKDB_C_API duckdb_bignum duckdb_get_bignum(duckdb_value val);
+
+/*!
+Returns the duckdb_decimal value of the given value.
+
+* @param val A duckdb_value containing a DECIMAL
+* @return A duckdb_decimal, or MinValue<decimal> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_decimal duckdb_get_decimal(duckdb_value val);
+
+/*!
+Returns the float value of the given value.
+
+* @param val A duckdb_value containing a float
+* @return A float, or NAN if the value cannot be converted
+*/
+DUCKDB_C_API float duckdb_get_float(duckdb_value val);
+
+/*!
+Returns the double value of the given value.
+
+* @param val A duckdb_value containing a double
+* @return A double, or NAN if the value cannot be converted
+*/
+DUCKDB_C_API double duckdb_get_double(duckdb_value val);
+
+/*!
+Returns the date value of the given value.
+
+* @param val A duckdb_value containing a date
+* @return A duckdb_date, or MinValue<date> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_date duckdb_get_date(duckdb_value val);
+
+/*!
+Returns the time value of the given value.
+
+* @param val A duckdb_value containing a time
+* @return A duckdb_time, or MinValue<time> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_time duckdb_get_time(duckdb_value val);
+
+/*!
+Returns the time_ns value of the given value.
+
+* @param val A duckdb_value containing a time_ns
+* @return A duckdb_time_ns, or MinValue<time_ns> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_time_ns duckdb_get_time_ns(duckdb_value val);
+
+/*!
+Returns the time_tz value of the given value.
+
+* @param val A duckdb_value containing a time_tz
+* @return A duckdb_time_tz, or MinValue<time_tz> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_time_tz duckdb_get_time_tz(duckdb_value val);
+
+/*!
+Returns the TIMESTAMP value of the given value.
+
+* @param val A duckdb_value containing a TIMESTAMP
+* @return A duckdb_timestamp, or MinValue<timestamp> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_timestamp duckdb_get_timestamp(duckdb_value val);
+
+/*!
+Returns the TIMESTAMP_TZ value of the given value.
+
+* @param val A duckdb_value containing a TIMESTAMP_TZ
+* @return A duckdb_timestamp, or MinValue<timestamp_tz> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_timestamp duckdb_get_timestamp_tz(duckdb_value val);
+
+/*!
+Returns the duckdb_timestamp_s value of the given value.
+
+* @param val A duckdb_value containing a TIMESTAMP_S
+* @return A duckdb_timestamp_s, or MinValue<timestamp_s> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_timestamp_s duckdb_get_timestamp_s(duckdb_value val);
+
+/*!
+Returns the duckdb_timestamp_ms value of the given value.
+
+* @param val A duckdb_value containing a TIMESTAMP_MS
+* @return A duckdb_timestamp_ms, or MinValue<timestamp_ms> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_timestamp_ms duckdb_get_timestamp_ms(duckdb_value val);
+
+/*!
+Returns the duckdb_timestamp_ns value of the given value.
+
+* @param val A duckdb_value containing a TIMESTAMP_NS
+* @return A duckdb_timestamp_ns, or MinValue<timestamp_ns> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_timestamp_ns duckdb_get_timestamp_ns(duckdb_value val);
+
+/*!
+Returns the interval value of the given value.
+
+* @param val A duckdb_value containing a interval
+* @return A duckdb_interval, or MinValue<interval> if the value cannot be converted
+*/
+DUCKDB_C_API duckdb_interval duckdb_get_interval(duckdb_value val);
+
+/*!
+Returns the type of the given value. The type is valid as long as the value is not destroyed.
+The type itself must not be destroyed.
+
+* @param val A duckdb_value
+* @return A duckdb_logical_type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_get_value_type(duckdb_value val);
+
+/*!
+Returns the blob value of the given value.
+
+* @param val A duckdb_value containing a blob
+* @return A duckdb_blob
+*/
+DUCKDB_C_API duckdb_blob duckdb_get_blob(duckdb_value val);
+
+/*!
+Returns the duckdb_bit value of the given value.
+The `data` field must be destroyed with `duckdb_free`.
+
+* @param val A duckdb_value containing a BIT
+* @return A duckdb_bit
+*/
+DUCKDB_C_API duckdb_bit duckdb_get_bit(duckdb_value val);
+
+/*!
+Returns a duckdb_uhugeint representing the UUID value of the given value.
+
+* @param val A duckdb_value containing a UUID
+* @return A duckdb_uhugeint representing the UUID value
+*/
+DUCKDB_C_API duckdb_uhugeint duckdb_get_uuid(duckdb_value val);
+
+/*!
+Obtains a string representation of the given value.
+The result must be destroyed with `duckdb_free`.
+
+* @param value The value
+* @return The string value. This must be destroyed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_get_varchar(duckdb_value value);
+
+/*!
+Creates a struct value from a type and an array of values. Must be destroyed with `duckdb_destroy_value`.
+
+* @param type The type of the struct
+* @param values The values for the struct fields
+* @return The struct value, or nullptr, if any child type is `DUCKDB_TYPE_ANY` or `DUCKDB_TYPE_INVALID`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_struct_value(duckdb_logical_type type, duckdb_value *values);
+
+/*!
+Creates a list value from a child (element) type and an array of values of length `value_count`.
+Must be destroyed with `duckdb_destroy_value`.
+
+* @param type The type of the list
+* @param values The values for the list
+* @param value_count The number of values in the list
+* @return The list value, or nullptr, if the child type is `DUCKDB_TYPE_ANY` or `DUCKDB_TYPE_INVALID`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_list_value(duckdb_logical_type type, duckdb_value *values, idx_t value_count);
+
+/*!
+Creates an array value from a child (element) type and an array of values of length `value_count`.
+Must be destroyed with `duckdb_destroy_value`.
+
+* @param type The type of the array
+* @param values The values for the array
+* @param value_count The number of values in the array
+* @return The array value, or nullptr, if the child type is `DUCKDB_TYPE_ANY` or `DUCKDB_TYPE_INVALID`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_array_value(duckdb_logical_type type, duckdb_value *values, idx_t value_count);
+
+/*!
+Creates a map value from a map type and two arrays, one for the keys and one for the values, each of length
+`entry_count`. Must be destroyed with `duckdb_destroy_value`.
+
+* @param map_type The map type
+* @param keys The keys of the map
+* @param values The values of the map
+* @param entry_count The number of entrys (key-value pairs) in the map
+* @return The map value, or nullptr, if the parameters are invalid.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_map_value(duckdb_logical_type map_type, duckdb_value *keys,
+                                                  duckdb_value *values, idx_t entry_count);
+
+/*!
+Creates a union value from a union type, a tag index, and a value.
+Must be destroyed with `duckdb_destroy_value`.
+
+* @param union_type The union type
+* @param tag_index The index of the tag of the union
+* @param value The value of the union for that tag
+* @return The union value, or nullptr, if the parameters are invalid.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_union_value(duckdb_logical_type union_type, idx_t tag_index,
+                                                    duckdb_value value);
+
+/*!
+Returns the number of elements in a MAP value.
+
+* @param value The MAP value.
+* @return The number of elements in the map.
+*/
+DUCKDB_C_API idx_t duckdb_get_map_size(duckdb_value value);
+
+/*!
+Returns the MAP key at index as a duckdb_value.
+
+* @param value The MAP value.
+* @param index The index of the key.
+* @return The key as a duckdb_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_get_map_key(duckdb_value value, idx_t index);
+
+/*!
+Returns the MAP value at index as a duckdb_value.
+
+* @param value The MAP value.
+* @param index The index of the value.
+* @return The value as a duckdb_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_get_map_value(duckdb_value value, idx_t index);
+
+/*!
+Returns whether the value's type is SQLNULL or not.
+
+* @param value The value to check.
+* @return True, if the value's type is SQLNULL, otherwise false.
+*/
+DUCKDB_C_API bool duckdb_is_null_value(duckdb_value value);
+
+/*!
+Creates a value of type SQLNULL.
+
+* @return The duckdb_value representing SQLNULL. This must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_null_value();
+
+/*!
+Returns the number of elements in a LIST value.
+
+* @param value The LIST value.
+* @return The number of elements in the list.
+*/
+DUCKDB_C_API idx_t duckdb_get_list_size(duckdb_value value);
+
+/*!
+Returns the LIST child at index as a duckdb_value.
+
+* @param value The LIST value.
+* @param index The index of the child.
+* @return The child as a duckdb_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_get_list_child(duckdb_value value, idx_t index);
+
+/*!
+Creates an enum value from a type and a value. Must be destroyed with `duckdb_destroy_value`.
+
+* @param type The type of the enum
+* @param value The value for the enum
+* @return The enum value, or nullptr.
+*/
+DUCKDB_C_API duckdb_value duckdb_create_enum_value(duckdb_logical_type type, uint64_t value);
+
+/*!
+Returns the enum value of the given value.
+
+* @param value A duckdb_value containing an enum
+* @return A uint64_t, or MinValue<uint64> if the value cannot be converted
+*/
+DUCKDB_C_API uint64_t duckdb_get_enum_value(duckdb_value value);
+
+/*!
+Returns the STRUCT child at index as a duckdb_value.
+
+* @param value The STRUCT value.
+* @param index The index of the child.
+* @return The child as a duckdb_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_get_struct_child(duckdb_value value, idx_t index);
+
+/*!
+Returns the SQL string representation of the given value.
+
+* @param value A duckdb_value.
+* @return The SQL string representation as a null-terminated string. The result must be freed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_value_to_string(duckdb_value value);
+
+//===--------------------------------------------------------------------===//
+// Logical Type Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a `duckdb_logical_type` from a primitive type.
+The resulting logical type must be destroyed with `duckdb_destroy_logical_type`.
+
+Returns an invalid logical type, if type is: `DUCKDB_TYPE_INVALID`, `DUCKDB_TYPE_DECIMAL`, `DUCKDB_TYPE_ENUM`,
+`DUCKDB_TYPE_LIST`, `DUCKDB_TYPE_STRUCT`, `DUCKDB_TYPE_MAP`, `DUCKDB_TYPE_ARRAY`, or `DUCKDB_TYPE_UNION`.
+
+* @param type The primitive type to create.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_logical_type(duckdb_type type);
+
+/*!
+Returns the alias of a duckdb_logical_type, if set, else `nullptr`.
+The result must be destroyed with `duckdb_free`.
+
+* @param type The logical type
+* @return The alias or `nullptr`
+*/
+DUCKDB_C_API char *duckdb_logical_type_get_alias(duckdb_logical_type type);
+
+/*!
+Sets the alias of a duckdb_logical_type.
+
+* @param type The logical type
+* @param alias The alias to set
+*/
+DUCKDB_C_API void duckdb_logical_type_set_alias(duckdb_logical_type type, const char *alias);
+
+/*!
+Creates a LIST type from its child type.
+The return type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param type The child type of the list
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_list_type(duckdb_logical_type type);
+
+/*!
+Creates an ARRAY type from its child type.
+The return type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param type The child type of the array.
+* @param array_size The number of elements in the array.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_array_type(duckdb_logical_type type, idx_t array_size);
+
+/*!
+Creates a MAP type from its key type and value type.
+The return type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param key_type The map's key type.
+* @param value_type The map's value type.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_map_type(duckdb_logical_type key_type, duckdb_logical_type value_type);
+
+/*!
+Creates a UNION type from the passed arrays.
+The return type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param member_types The array of union member types.
+* @param member_names The union member names.
+* @param member_count The number of union members.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_union_type(duckdb_logical_type *member_types, const char **member_names,
+                                                          idx_t member_count);
+
+/*!
+Creates a STRUCT type based on the member types and names.
+The resulting type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param member_types The array of types of the struct members.
+* @param member_names The array of names of the struct members.
+* @param member_count The number of members of the struct.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_struct_type(duckdb_logical_type *member_types, const char **member_names,
+                                                           idx_t member_count);
+
+/*!
+Creates an ENUM type from the passed member name array.
+The resulting type should be destroyed with `duckdb_destroy_logical_type`.
+
+* @param member_names The array of names that the enum should consist of.
+* @param member_count The number of elements that were specified in the array.
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_enum_type(const char **member_names, idx_t member_count);
+
+/*!
+Creates a DECIMAL type with the specified width and scale.
+The resulting type should be destroyed with `duckdb_destroy_logical_type`.
+
+* @param width The width of the decimal type
+* @param scale The scale of the decimal type
+* @return The logical type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_create_decimal_type(uint8_t width, uint8_t scale);
+
+/*!
+Retrieves the enum `duckdb_type` of a `duckdb_logical_type`.
+
+* @param type The logical type.
+* @return The `duckdb_type` id.
+*/
+DUCKDB_C_API duckdb_type duckdb_get_type_id(duckdb_logical_type type);
+
+/*!
+Retrieves the width of a decimal type.
+
+* @param type The logical type object
+* @return The width of the decimal type
+*/
+DUCKDB_C_API uint8_t duckdb_decimal_width(duckdb_logical_type type);
+
+/*!
+Retrieves the scale of a decimal type.
+
+* @param type The logical type object
+* @return The scale of the decimal type
+*/
+DUCKDB_C_API uint8_t duckdb_decimal_scale(duckdb_logical_type type);
+
+/*!
+Retrieves the internal storage type of a decimal type.
+
+* @param type The logical type object
+* @return The internal type of the decimal type
+*/
+DUCKDB_C_API duckdb_type duckdb_decimal_internal_type(duckdb_logical_type type);
+
+/*!
+Retrieves the internal storage type of an enum type.
+
+* @param type The logical type object
+* @return The internal type of the enum type
+*/
+DUCKDB_C_API duckdb_type duckdb_enum_internal_type(duckdb_logical_type type);
+
+/*!
+Retrieves the dictionary size of the enum type.
+
+* @param type The logical type object
+* @return The dictionary size of the enum type
+*/
+DUCKDB_C_API uint32_t duckdb_enum_dictionary_size(duckdb_logical_type type);
+
+/*!
+Retrieves the dictionary value at the specified position from the enum.
+
+The result must be freed with `duckdb_free`.
+
+* @param type The logical type object
+* @param index The index in the dictionary
+* @return The string value of the enum type. Must be freed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_enum_dictionary_value(duckdb_logical_type type, idx_t index);
+
+/*!
+Retrieves the child type of the given LIST type. Also accepts MAP types.
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type, either LIST or MAP.
+* @return The child type of the LIST or MAP type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_list_type_child_type(duckdb_logical_type type);
+
+/*!
+Retrieves the child type of the given ARRAY type.
+
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type. Must be ARRAY.
+* @return The child type of the ARRAY type.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_array_type_child_type(duckdb_logical_type type);
+
+/*!
+Retrieves the array size of the given array type.
+
+* @param type The logical type object
+* @return The fixed number of elements the values of this array type can store.
+*/
+DUCKDB_C_API idx_t duckdb_array_type_array_size(duckdb_logical_type type);
+
+/*!
+Retrieves the key type of the given map type.
+
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type object
+* @return The key type of the map type. Must be destroyed with `duckdb_destroy_logical_type`.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_map_type_key_type(duckdb_logical_type type);
+
+/*!
+Retrieves the value type of the given map type.
+
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type object
+* @return The value type of the map type. Must be destroyed with `duckdb_destroy_logical_type`.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_map_type_value_type(duckdb_logical_type type);
+
+/*!
+Returns the number of children of a struct type.
+
+* @param type The logical type object
+* @return The number of children of a struct type.
+*/
+DUCKDB_C_API idx_t duckdb_struct_type_child_count(duckdb_logical_type type);
+
+/*!
+Retrieves the name of the struct child.
+
+The result must be freed with `duckdb_free`.
+
+* @param type The logical type object
+* @param index The child index
+* @return The name of the struct type. Must be freed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_struct_type_child_name(duckdb_logical_type type, idx_t index);
+
+/*!
+Retrieves the child type of the given struct type at the specified index.
+
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type object
+* @param index The child index
+* @return The child type of the struct type. Must be destroyed with `duckdb_destroy_logical_type`.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_struct_type_child_type(duckdb_logical_type type, idx_t index);
+
+/*!
+Returns the number of members that the union type has.
+
+* @param type The logical type (union) object
+* @return The number of members of a union type.
+*/
+DUCKDB_C_API idx_t duckdb_union_type_member_count(duckdb_logical_type type);
+
+/*!
+Retrieves the name of the union member.
+
+The result must be freed with `duckdb_free`.
+
+* @param type The logical type object
+* @param index The child index
+* @return The name of the union member. Must be freed with `duckdb_free`.
+*/
+DUCKDB_C_API char *duckdb_union_type_member_name(duckdb_logical_type type, idx_t index);
+
+/*!
+Retrieves the child type of the given union member at the specified index.
+
+The result must be freed with `duckdb_destroy_logical_type`.
+
+* @param type The logical type object
+* @param index The child index
+* @return The child type of the union member. Must be destroyed with `duckdb_destroy_logical_type`.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_union_type_member_type(duckdb_logical_type type, idx_t index);
+
+/*!
+Destroys the logical type and de-allocates all memory allocated for that type.
+
+* @param type The logical type to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_logical_type(duckdb_logical_type *type);
+
+/*!
+Registers a custom type within the given connection.
+The type must have an alias
+
+* @param con The connection to use
+* @param type The custom type to register
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_logical_type(duckdb_connection con, duckdb_logical_type type,
+                                                       duckdb_create_type_info info);
+
+//===--------------------------------------------------------------------===//
+// Data Chunk Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates an empty data chunk with the specified column types.
+The result must be destroyed with `duckdb_destroy_data_chunk`.
+
+* @param types An array of column types. Column types can not contain ANY and INVALID types.
+* @param column_count The number of columns.
+* @return The data chunk.
+*/
+DUCKDB_C_API duckdb_data_chunk duckdb_create_data_chunk(duckdb_logical_type *types, idx_t column_count);
+
+/*!
+Destroys the data chunk and de-allocates all memory allocated for that chunk.
+
+* @param chunk The data chunk to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_data_chunk(duckdb_data_chunk *chunk);
+
+/*!
+Resets a data chunk, clearing the validity masks and setting the cardinality of the data chunk to 0.
+After calling this method, you must call `duckdb_vector_get_validity` and `duckdb_vector_get_data` to obtain current
+data and validity pointers
+
+* @param chunk The data chunk to reset.
+*/
+DUCKDB_C_API void duckdb_data_chunk_reset(duckdb_data_chunk chunk);
+
+/*!
+Retrieves the number of columns in a data chunk.
+
+* @param chunk The data chunk to get the data from
+* @return The number of columns in the data chunk
+*/
+DUCKDB_C_API idx_t duckdb_data_chunk_get_column_count(duckdb_data_chunk chunk);
+
+/*!
+Retrieves the vector at the specified column index in the data chunk.
+
+The pointer to the vector is valid for as long as the chunk is alive.
+It does NOT need to be destroyed.
+
+* @param chunk The data chunk to get the data from
+* @return The vector
+*/
+DUCKDB_C_API duckdb_vector duckdb_data_chunk_get_vector(duckdb_data_chunk chunk, idx_t col_idx);
+
+/*!
+Retrieves the current number of tuples in a data chunk.
+
+* @param chunk The data chunk to get the data from
+* @return The number of tuples in the data chunk
+*/
+DUCKDB_C_API idx_t duckdb_data_chunk_get_size(duckdb_data_chunk chunk);
+
+/*!
+Sets the current number of tuples in a data chunk.
+
+* @param chunk The data chunk to set the size in
+* @param size The number of tuples in the data chunk
+*/
+DUCKDB_C_API void duckdb_data_chunk_set_size(duckdb_data_chunk chunk, idx_t size);
+
+//===--------------------------------------------------------------------===//
+// Vector Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a flat vector. Must be destroyed with `duckdb_destroy_vector`.
+
+* @param type The logical type of the vector.
+* @param capacity The capacity of the vector.
+* @return The vector.
+*/
+DUCKDB_C_API duckdb_vector duckdb_create_vector(duckdb_logical_type type, idx_t capacity);
+
+/*!
+Destroys the vector and de-allocates its memory.
+
+* @param vector A pointer to the vector.
+*/
+DUCKDB_C_API void duckdb_destroy_vector(duckdb_vector *vector);
+
+/*!
+Retrieves the column type of the specified vector.
+
+The result must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param vector The vector get the data from
+* @return The type of the vector
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_vector_get_column_type(duckdb_vector vector);
+
+/*!
+Retrieves the data pointer of the vector.
+
+The data pointer can be used to read or write values from the vector.
+How to read or write values depends on the type of the vector.
+
+* @param vector The vector to get the data from
+* @return The data pointer
+*/
+DUCKDB_C_API void *duckdb_vector_get_data(duckdb_vector vector);
+
+/*!
+Retrieves the validity mask pointer of the specified vector.
+
+If all values are valid, this function MIGHT return NULL!
+
+The validity mask is a bitset that signifies null-ness within the data chunk.
+It is a series of uint64_t values, where each uint64_t value contains validity for 64 tuples.
+The bit is set to 1 if the value is valid (i.e. not NULL) or 0 if the value is invalid (i.e. NULL).
+
+Validity of a specific value can be obtained like this:
+
+idx_t entry_idx = row_idx / 64;
+idx_t idx_in_entry = row_idx % 64;
+bool is_valid = validity_mask[entry_idx] & (1 << idx_in_entry);
+
+Alternatively, the (slower) duckdb_validity_row_is_valid function can be used.
+
+* @param vector The vector to get the data from
+* @return The pointer to the validity mask, or NULL if no validity mask is present
+*/
+DUCKDB_C_API uint64_t *duckdb_vector_get_validity(duckdb_vector vector);
+
+/*!
+Ensures the validity mask is writable by allocating it.
+
+After this function is called, `duckdb_vector_get_validity` will ALWAYS return non-NULL.
+This allows NULL values to be written to the vector, regardless of whether a validity mask was present before.
+
+* @param vector The vector to alter
+*/
+DUCKDB_C_API void duckdb_vector_ensure_validity_writable(duckdb_vector vector);
+
+/*!
+Assigns a string element in the vector at the specified location.
+
+* @param vector The vector to alter
+* @param index The row position in the vector to assign the string to
+* @param str The null-terminated string
+*/
+DUCKDB_C_API void duckdb_vector_assign_string_element(duckdb_vector vector, idx_t index, const char *str);
+
+/*!
+Assigns a string element in the vector at the specified location. You may also use this function to assign BLOBs.
+
+* @param vector The vector to alter
+* @param index The row position in the vector to assign the string to
+* @param str The string
+* @param str_len The length of the string (in bytes)
+*/
+DUCKDB_C_API void duckdb_vector_assign_string_element_len(duckdb_vector vector, idx_t index, const char *str,
+                                                          idx_t str_len);
+
+/*!
+Retrieves the child vector of a list vector.
+
+The resulting vector is valid as long as the parent vector is valid.
+
+* @param vector The vector
+* @return The child vector
+*/
+DUCKDB_C_API duckdb_vector duckdb_list_vector_get_child(duckdb_vector vector);
+
+/*!
+Returns the size of the child vector of the list.
+
+* @param vector The vector
+* @return The size of the child list
+*/
+DUCKDB_C_API idx_t duckdb_list_vector_get_size(duckdb_vector vector);
+
+/*!
+Sets the total size of the underlying child-vector of a list vector.
+
+* @param vector The list vector.
+* @param size The size of the child list.
+* @return The duckdb state. Returns DuckDBError if the vector is nullptr.
+*/
+DUCKDB_C_API duckdb_state duckdb_list_vector_set_size(duckdb_vector vector, idx_t size);
+
+/*!
+Sets the total capacity of the underlying child-vector of a list.
+
+After calling this method, you must call `duckdb_vector_get_validity` and `duckdb_vector_get_data` to obtain current
+data and validity pointers
+
+* @param vector The list vector.
+* @param required_capacity the total capacity to reserve.
+* @return The duckdb state. Returns DuckDBError if the vector is nullptr.
+*/
+DUCKDB_C_API duckdb_state duckdb_list_vector_reserve(duckdb_vector vector, idx_t required_capacity);
+
+/*!
+Retrieves the child vector of a struct vector.
+The resulting vector is valid as long as the parent vector is valid.
+
+* @param vector The vector
+* @param index The child index
+* @return The child vector
+*/
+DUCKDB_C_API duckdb_vector duckdb_struct_vector_get_child(duckdb_vector vector, idx_t index);
+
+/*!
+Retrieves the child vector of an array vector.
+The resulting vector is valid as long as the parent vector is valid.
+The resulting vector has the size of the parent vector multiplied by the array size.
+
+* @param vector The vector
+* @return The child vector
+*/
+DUCKDB_C_API duckdb_vector duckdb_array_vector_get_child(duckdb_vector vector);
+
+/*!
+Slice a vector with a selection vector.
+The length of the selection vector must be less than or equal to the length of the vector.
+Turns the vector into a dictionary vector.
+
+* @param vector The vector to slice.
+* @param sel The selection vector.
+* @param len The length of the selection vector.
+*/
+DUCKDB_C_API void duckdb_slice_vector(duckdb_vector vector, duckdb_selection_vector sel, idx_t len);
+
+/*!
+Copy the src vector to the dst with a selection vector that identifies which indices to copy.
+
+* @param src The vector to copy from.
+* @param dst The vector to copy to.
+* @param sel The selection vector. The length of the selection vector should not be more than the length of the src
+vector
+* @param src_count The number of entries from selection vector to copy. Think of this as the effective length of the
+selection vector starting from index 0
+* @param src_offset The offset in the selection vector to copy from (important: actual number of items copied =
+src_count - src_offset).
+* @param dst_offset The offset in the dst vector to start copying to.
+*/
+DUCKDB_C_API void duckdb_vector_copy_sel(duckdb_vector src, duckdb_vector dst, duckdb_selection_vector sel,
+                                         idx_t src_count, idx_t src_offset, idx_t dst_offset);
+
+/*!
+Copies the value from `value` to `vector`.
+
+* @param vector The receiving vector.
+* @param value The value to copy into the vector.
+*/
+DUCKDB_C_API void duckdb_vector_reference_value(duckdb_vector vector, duckdb_value value);
+
+/*!
+Changes `to_vector` to reference `from_vector. After, the vectors share ownership of the data.
+
+* @param to_vector The receiving vector.
+* @param from_vector The vector to reference.
+*/
+DUCKDB_C_API void duckdb_vector_reference_vector(duckdb_vector to_vector, duckdb_vector from_vector);
+
+//===--------------------------------------------------------------------===//
+// Validity Mask Functions
+//===--------------------------------------------------------------------===//
+
+/*!
+Returns whether or not a row is valid (i.e. not NULL) in the given validity mask.
+
+* @param validity The validity mask, as obtained through `duckdb_vector_get_validity`
+* @param row The row index
+* @return true if the row is valid, false otherwise
+*/
+DUCKDB_C_API bool duckdb_validity_row_is_valid(uint64_t *validity, idx_t row);
+
+/*!
+In a validity mask, sets a specific row to either valid or invalid.
+
+Note that `duckdb_vector_ensure_validity_writable` should be called before calling `duckdb_vector_get_validity`,
+to ensure that there is a validity mask to write to.
+
+* @param validity The validity mask, as obtained through `duckdb_vector_get_validity`.
+* @param row The row index
+* @param valid Whether or not to set the row to valid, or invalid
+*/
+DUCKDB_C_API void duckdb_validity_set_row_validity(uint64_t *validity, idx_t row, bool valid);
+
+/*!
+In a validity mask, sets a specific row to invalid.
+
+Equivalent to `duckdb_validity_set_row_validity` with valid set to false.
+
+* @param validity The validity mask
+* @param row The row index
+*/
+DUCKDB_C_API void duckdb_validity_set_row_invalid(uint64_t *validity, idx_t row);
+
+/*!
+In a validity mask, sets a specific row to valid.
+
+Equivalent to `duckdb_validity_set_row_validity` with valid set to true.
+
+* @param validity The validity mask
+* @param row The row index
+*/
+DUCKDB_C_API void duckdb_validity_set_row_valid(uint64_t *validity, idx_t row);
+
+//===--------------------------------------------------------------------===//
+// Scalar Functions
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new empty scalar function.
+
+The return value must be destroyed with `duckdb_destroy_scalar_function`.
+
+* @return The scalar function object.
+*/
+DUCKDB_C_API duckdb_scalar_function duckdb_create_scalar_function();
+
+/*!
+Destroys the given scalar function object.
+
+* @param scalar_function The scalar function to destroy
+*/
+DUCKDB_C_API void duckdb_destroy_scalar_function(duckdb_scalar_function *scalar_function);
+
+/*!
+Sets the name of the given scalar function.
+
+* @param scalar_function The scalar function
+* @param name The name of the scalar function
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_name(duckdb_scalar_function scalar_function, const char *name);
+
+/*!
+Sets the parameters of the given scalar function to varargs. Does not require adding parameters with
+duckdb_scalar_function_add_parameter.
+
+* @param scalar_function The scalar function.
+* @param type The type of the arguments.
+* @return The parameter type. Cannot contain INVALID.
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_varargs(duckdb_scalar_function scalar_function, duckdb_logical_type type);
+
+/*!
+Sets the scalar function's null-handling behavior to special.
+
+* @param scalar_function The scalar function.
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_special_handling(duckdb_scalar_function scalar_function);
+
+/*!
+Sets the Function Stability of the scalar function to VOLATILE, indicating the function should be re-run for every row.
+This limits optimization that can be performed for the function.
+
+* @param scalar_function The scalar function.
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_volatile(duckdb_scalar_function scalar_function);
+
+/*!
+Adds a parameter to the scalar function.
+
+* @param scalar_function The scalar function.
+* @param type The parameter type. Cannot contain INVALID.
+*/
+DUCKDB_C_API void duckdb_scalar_function_add_parameter(duckdb_scalar_function scalar_function,
+                                                       duckdb_logical_type type);
+
+/*!
+Sets the return type of the scalar function.
+
+* @param scalar_function The scalar function
+* @param type Cannot contain INVALID or ANY.
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_return_type(duckdb_scalar_function scalar_function,
+                                                         duckdb_logical_type type);
+
+/*!
+Assigns extra information to the scalar function that can be fetched during binding, etc.
+
+* @param scalar_function The scalar function
+* @param extra_info The extra information
+* @param destroy The callback that will be called to destroy the extra information (if any)
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_extra_info(duckdb_scalar_function scalar_function, void *extra_info,
+                                                        duckdb_delete_callback_t destroy);
+
+/*!
+Sets the (optional) bind function of the scalar function.
+
+* @param scalar_function The scalar function.
+* @param bind The bind function.
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_bind(duckdb_scalar_function scalar_function,
+                                                  duckdb_scalar_function_bind_t bind);
+
+/*!
+Sets the user-provided bind data in the bind object of the scalar function.
+The bind data object can be retrieved again during execution.
+In most case, you also need to set the copy-callback of your bind data via duckdb_scalar_function_set_bind_data_copy.
+
+* @param info The bind info of the scalar function.
+* @param bind_data The bind data object.
+* @param destroy The callback to destroy the bind data (if any).
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_bind_data(duckdb_bind_info info, void *bind_data,
+                                                       duckdb_delete_callback_t destroy);
+
+/*!
+Sets the copy-callback for the user-provided bind data in the bind object of the scalar function.
+
+* @param info The bind info of the scalar function.
+* @param copy The callback to copy the bind data (if any).
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_bind_data_copy(duckdb_bind_info info, duckdb_copy_callback_t copy);
+
+/*!
+Report that an error has occurred while calling bind on a scalar function.
+
+* @param info The bind info object.
+* @param error The error message.
+*/
+DUCKDB_C_API void duckdb_scalar_function_bind_set_error(duckdb_bind_info info, const char *error);
+
+/*!
+Sets the main function of the scalar function.
+
+* @param scalar_function The scalar function
+* @param function The function
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_function(duckdb_scalar_function scalar_function,
+                                                      duckdb_scalar_function_t function);
+
+/*!
+Register the scalar function object within the given connection.
+
+The function requires at least a name, a function and a return type.
+
+If the function is incomplete or a function with this name already exists DuckDBError is returned.
+
+* @param con The connection to register it in.
+* @param scalar_function The function pointer
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_scalar_function(duckdb_connection con,
+                                                          duckdb_scalar_function scalar_function);
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_scalar_function_set_extra_info`.
+
+* @param info The info object.
+* @return The extra info.
+*/
+DUCKDB_C_API void *duckdb_scalar_function_get_extra_info(duckdb_function_info info);
+
+/*!
+Retrieves the extra info of the function as set in the bind info.
+
+* @param info The info object.
+* @return The extra info.
+*/
+DUCKDB_C_API void *duckdb_scalar_function_bind_get_extra_info(duckdb_bind_info info);
+
+/*!
+Gets the scalar function's bind data set by `duckdb_scalar_function_set_bind_data`.
+Note that the bind data is read-only.
+
+* @param info The function info.
+* @return The bind data object.
+*/
+DUCKDB_C_API void *duckdb_scalar_function_get_bind_data(duckdb_function_info info);
+
+/*!
+Retrieves the client context of the bind info of a scalar function.
+
+* @param info The bind info object of the scalar function.
+* @param out_context The client context of the bind info. Must be destroyed with `duckdb_destroy_client_context`.
+*/
+DUCKDB_C_API void duckdb_scalar_function_get_client_context(duckdb_bind_info info, duckdb_client_context *out_context);
+
+/*!
+Report that an error has occurred while executing the scalar function.
+
+* @param info The info object.
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_scalar_function_set_error(duckdb_function_info info, const char *error);
+
+/*!
+Creates a new empty scalar function set.
+
+The return value must be destroyed with `duckdb_destroy_scalar_function_set`.
+
+* @return The scalar function set object.
+*/
+DUCKDB_C_API duckdb_scalar_function_set duckdb_create_scalar_function_set(const char *name);
+
+/*!
+Destroys the given scalar function set object.
+
+*/
+DUCKDB_C_API void duckdb_destroy_scalar_function_set(duckdb_scalar_function_set *scalar_function_set);
+
+/*!
+Adds the scalar function as a new overload to the scalar function set.
+
+Returns DuckDBError if the function could not be added, for example if the overload already exists.
+
+* @param set The scalar function set
+* @param function The function to add
+*/
+DUCKDB_C_API duckdb_state duckdb_add_scalar_function_to_set(duckdb_scalar_function_set set,
+                                                            duckdb_scalar_function function);
+
+/*!
+Register the scalar function set within the given connection.
+
+The set requires at least a single valid overload.
+
+If the set is incomplete or a function with this name already exists DuckDBError is returned.
+
+* @param con The connection to register it in.
+* @param set The function set to register
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_scalar_function_set(duckdb_connection con, duckdb_scalar_function_set set);
+
+/*!
+Returns the number of input arguments of the scalar function.
+
+* @param info The bind info.
+* @return The number of input arguments.
+*/
+DUCKDB_C_API idx_t duckdb_scalar_function_bind_get_argument_count(duckdb_bind_info info);
+
+/*!
+Returns the input argument at index of the scalar function.
+
+* @param info The bind info.
+* @param index The argument index.
+* @return The input argument at index. Must be destroyed with `duckdb_destroy_expression`.
+*/
+DUCKDB_C_API duckdb_expression duckdb_scalar_function_bind_get_argument(duckdb_bind_info info, idx_t index);
+
+//===--------------------------------------------------------------------===//
+// Selection Vector Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new selection vector of size `size`.
+Must be destroyed with `duckdb_destroy_selection_vector`.
+
+* @param size The size of the selection vector.
+* @return The selection vector.
+*/
+DUCKDB_C_API duckdb_selection_vector duckdb_create_selection_vector(idx_t size);
+
+/*!
+Destroys the selection vector and de-allocates its memory.
+
+* @param sel The selection vector.
+*/
+DUCKDB_C_API void duckdb_destroy_selection_vector(duckdb_selection_vector sel);
+
+/*!
+Access the data pointer of a selection vector.
+
+* @param sel The selection vector.
+* @return The data pointer.
+*/
+DUCKDB_C_API sel_t *duckdb_selection_vector_get_data_ptr(duckdb_selection_vector sel);
+
+//===--------------------------------------------------------------------===//
+// Aggregate Functions
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new empty aggregate function.
+
+The return value should be destroyed with `duckdb_destroy_aggregate_function`.
+
+* @return The aggregate function object.
+*/
+DUCKDB_C_API duckdb_aggregate_function duckdb_create_aggregate_function();
+
+/*!
+Destroys the given aggregate function object.
+
+*/
+DUCKDB_C_API void duckdb_destroy_aggregate_function(duckdb_aggregate_function *aggregate_function);
+
+/*!
+Sets the name of the given aggregate function.
+
+* @param aggregate_function The aggregate function
+* @param name The name of the aggregate function
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_name(duckdb_aggregate_function aggregate_function, const char *name);
+
+/*!
+Adds a parameter to the aggregate function.
+
+* @param aggregate_function The aggregate function.
+* @param type The parameter type. Cannot contain INVALID.
+*/
+DUCKDB_C_API void duckdb_aggregate_function_add_parameter(duckdb_aggregate_function aggregate_function,
+                                                          duckdb_logical_type type);
+
+/*!
+Sets the return type of the aggregate function.
+
+* @param aggregate_function The aggregate function.
+* @param type The return type. Cannot contain INVALID or ANY.
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_return_type(duckdb_aggregate_function aggregate_function,
+                                                            duckdb_logical_type type);
+
+/*!
+Sets the main functions of the aggregate function.
+
+* @param aggregate_function The aggregate function
+* @param state_size state size
+* @param state_init state init function
+* @param update update states
+* @param combine combine states
+* @param finalize finalize states
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_functions(duckdb_aggregate_function aggregate_function,
+                                                          duckdb_aggregate_state_size state_size,
+                                                          duckdb_aggregate_init_t state_init,
+                                                          duckdb_aggregate_update_t update,
+                                                          duckdb_aggregate_combine_t combine,
+                                                          duckdb_aggregate_finalize_t finalize);
+
+/*!
+Sets the state destructor callback of the aggregate function (optional)
+
+* @param aggregate_function The aggregate function
+* @param destroy state destroy callback
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_destructor(duckdb_aggregate_function aggregate_function,
+                                                           duckdb_aggregate_destroy_t destroy);
+
+/*!
+Register the aggregate function object within the given connection.
+
+The function requires at least a name, functions and a return type.
+
+If the function is incomplete or a function with this name already exists DuckDBError is returned.
+
+* @param con The connection to register it in.
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_aggregate_function(duckdb_connection con,
+                                                             duckdb_aggregate_function aggregate_function);
+
+/*!
+Sets the NULL handling of the aggregate function to SPECIAL_HANDLING.
+
+* @param aggregate_function The aggregate function
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_special_handling(duckdb_aggregate_function aggregate_function);
+
+/*!
+Assigns extra information to the scalar function that can be fetched during binding, etc.
+
+* @param aggregate_function The aggregate function
+* @param extra_info The extra information
+* @param destroy The callback that will be called to destroy the extra information (if any)
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_extra_info(duckdb_aggregate_function aggregate_function,
+                                                           void *extra_info, duckdb_delete_callback_t destroy);
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_aggregate_function_set_extra_info`.
+
+* @param info The info object
+* @return The extra info
+*/
+DUCKDB_C_API void *duckdb_aggregate_function_get_extra_info(duckdb_function_info info);
+
+/*!
+Report that an error has occurred while executing the aggregate function.
+
+* @param info The info object
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_aggregate_function_set_error(duckdb_function_info info, const char *error);
+
+/*!
+Creates a new empty aggregate function set.
+
+The return value should be destroyed with `duckdb_destroy_aggregate_function_set`.
+
+* @return The aggregate function set object.
+*/
+DUCKDB_C_API duckdb_aggregate_function_set duckdb_create_aggregate_function_set(const char *name);
+
+/*!
+Destroys the given aggregate function set object.
+
+*/
+DUCKDB_C_API void duckdb_destroy_aggregate_function_set(duckdb_aggregate_function_set *aggregate_function_set);
+
+/*!
+Adds the aggregate function as a new overload to the aggregate function set.
+
+Returns DuckDBError if the function could not be added, for example if the overload already exists.
+
+* @param set The aggregate function set
+* @param function The function to add
+*/
+DUCKDB_C_API duckdb_state duckdb_add_aggregate_function_to_set(duckdb_aggregate_function_set set,
+                                                               duckdb_aggregate_function function);
+
+/*!
+Register the aggregate function set within the given connection.
+
+The set requires at least a single valid overload.
+
+If the set is incomplete or a function with this name already exists DuckDBError is returned.
+
+* @param con The connection to register it in.
+* @param set The function set to register
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_aggregate_function_set(duckdb_connection con,
+                                                                 duckdb_aggregate_function_set set);
+
+//===--------------------------------------------------------------------===//
+// Table Functions
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new empty table function.
+
+The return value should be destroyed with `duckdb_destroy_table_function`.
+
+* @return The table function object.
+*/
+DUCKDB_C_API duckdb_table_function duckdb_create_table_function();
+
+/*!
+Destroys the given table function object.
+
+* @param table_function The table function to destroy
+*/
+DUCKDB_C_API void duckdb_destroy_table_function(duckdb_table_function *table_function);
+
+/*!
+Sets the name of the given table function.
+
+* @param table_function The table function
+* @param name The name of the table function
+*/
+DUCKDB_C_API void duckdb_table_function_set_name(duckdb_table_function table_function, const char *name);
+
+/*!
+Adds a parameter to the table function.
+
+* @param table_function The table function.
+* @param type The parameter type. Cannot contain INVALID.
+*/
+DUCKDB_C_API void duckdb_table_function_add_parameter(duckdb_table_function table_function, duckdb_logical_type type);
+
+/*!
+Adds a named parameter to the table function.
+
+* @param table_function The table function.
+* @param name The parameter name.
+* @param type The parameter type. Cannot contain INVALID.
+*/
+DUCKDB_C_API void duckdb_table_function_add_named_parameter(duckdb_table_function table_function, const char *name,
+                                                            duckdb_logical_type type);
+
+/*!
+Assigns extra information to the table function that can be fetched during binding, etc.
+
+* @param table_function The table function
+* @param extra_info The extra information
+* @param destroy The callback that will be called to destroy the extra information (if any)
+*/
+DUCKDB_C_API void duckdb_table_function_set_extra_info(duckdb_table_function table_function, void *extra_info,
+                                                       duckdb_delete_callback_t destroy);
+
+/*!
+Sets the bind function of the table function.
+
+* @param table_function The table function
+* @param bind The bind function
+*/
+DUCKDB_C_API void duckdb_table_function_set_bind(duckdb_table_function table_function,
+                                                 duckdb_table_function_bind_t bind);
+
+/*!
+Sets the init function of the table function.
+
+* @param table_function The table function
+* @param init The init function
+*/
+DUCKDB_C_API void duckdb_table_function_set_init(duckdb_table_function table_function,
+                                                 duckdb_table_function_init_t init);
+
+/*!
+Sets the thread-local init function of the table function.
+
+* @param table_function The table function
+* @param init The init function
+*/
+DUCKDB_C_API void duckdb_table_function_set_local_init(duckdb_table_function table_function,
+                                                       duckdb_table_function_init_t init);
+
+/*!
+Sets the main function of the table function.
+
+* @param table_function The table function
+* @param function The function
+*/
+DUCKDB_C_API void duckdb_table_function_set_function(duckdb_table_function table_function,
+                                                     duckdb_table_function_t function);
+
+/*!
+Sets whether or not the given table function supports projection pushdown.
+
+If this is set to true, the system will provide a list of all required columns in the `init` stage through
+the `duckdb_init_get_column_count` and `duckdb_init_get_column_index` functions.
+If this is set to false (the default), the system will expect all columns to be projected.
+
+* @param table_function The table function
+* @param pushdown True if the table function supports projection pushdown, false otherwise.
+*/
+DUCKDB_C_API void duckdb_table_function_supports_projection_pushdown(duckdb_table_function table_function,
+                                                                     bool pushdown);
+
+/*!
+Register the table function object within the given connection.
+
+The function requires at least a name, a bind function, an init function and a main function.
+
+If the function is incomplete or a function with this name already exists DuckDBError is returned.
+
+* @param con The connection to register it in.
+* @param function The function pointer
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_table_function(duckdb_connection con, duckdb_table_function function);
+
+//===--------------------------------------------------------------------===//
+// Table Function Bind
+//===--------------------------------------------------------------------===//
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_table_function_set_extra_info`.
+
+* @param info The info object
+* @return The extra info
+*/
+DUCKDB_C_API void *duckdb_bind_get_extra_info(duckdb_bind_info info);
+
+/*!
+Retrieves the client context of the bind info of a table function.
+
+* @param info The bind info object of the table function.
+* @param out_context The client context of the bind info. Must be destroyed with `duckdb_destroy_client_context`.
+*/
+DUCKDB_C_API void duckdb_table_function_get_client_context(duckdb_bind_info info, duckdb_client_context *out_context);
+
+/*!
+Adds a result column to the output of the table function.
+
+* @param info The table function's bind info.
+* @param name The column name.
+* @param type The logical column type.
+*/
+DUCKDB_C_API void duckdb_bind_add_result_column(duckdb_bind_info info, const char *name, duckdb_logical_type type);
+
+/*!
+Retrieves the number of regular (non-named) parameters to the function.
+
+* @param info The info object
+* @return The number of parameters
+*/
+DUCKDB_C_API idx_t duckdb_bind_get_parameter_count(duckdb_bind_info info);
+
+/*!
+Retrieves the parameter at the given index.
+
+The result must be destroyed with `duckdb_destroy_value`.
+
+* @param info The info object
+* @param index The index of the parameter to get
+* @return The value of the parameter. Must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_bind_get_parameter(duckdb_bind_info info, idx_t index);
+
+/*!
+Retrieves a named parameter with the given name.
+
+The result must be destroyed with `duckdb_destroy_value`.
+
+* @param info The info object
+* @param name The name of the parameter
+* @return The value of the parameter. Must be destroyed with `duckdb_destroy_value`.
+*/
+DUCKDB_C_API duckdb_value duckdb_bind_get_named_parameter(duckdb_bind_info info, const char *name);
+
+/*!
+Sets the user-provided bind data in the bind object of the table function.
+This object can be retrieved again during execution.
+
+* @param info The bind info of the table function.
+* @param bind_data The bind data object.
+* @param destroy The callback to destroy the bind data (if any).
+*/
+DUCKDB_C_API void duckdb_bind_set_bind_data(duckdb_bind_info info, void *bind_data, duckdb_delete_callback_t destroy);
+
+/*!
+Sets the cardinality estimate for the table function, used for optimization.
+
+* @param info The bind data object.
+* @param is_exact Whether or not the cardinality estimate is exact, or an approximation
+*/
+DUCKDB_C_API void duckdb_bind_set_cardinality(duckdb_bind_info info, idx_t cardinality, bool is_exact);
+
+/*!
+Report that an error has occurred while calling bind on a table function.
+
+* @param info The info object
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_bind_set_error(duckdb_bind_info info, const char *error);
+
+//===--------------------------------------------------------------------===//
+// Table Function Init
+//===--------------------------------------------------------------------===//
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_table_function_set_extra_info`.
+
+* @param info The info object
+* @return The extra info
+*/
+DUCKDB_C_API void *duckdb_init_get_extra_info(duckdb_init_info info);
+
+/*!
+Gets the bind data set by `duckdb_bind_set_bind_data` during the bind.
+
+Note that the bind data should be considered as read-only.
+For tracking state, use the init data instead.
+
+* @param info The info object
+* @return The bind data object
+*/
+DUCKDB_C_API void *duckdb_init_get_bind_data(duckdb_init_info info);
+
+/*!
+Sets the user-provided init data in the init object. This object can be retrieved again during execution.
+
+* @param info The info object
+* @param init_data The init data object.
+* @param destroy The callback that will be called to destroy the init data (if any)
+*/
+DUCKDB_C_API void duckdb_init_set_init_data(duckdb_init_info info, void *init_data, duckdb_delete_callback_t destroy);
+
+/*!
+Returns the number of projected columns.
+
+This function must be used if projection pushdown is enabled to figure out which columns to emit.
+
+* @param info The info object
+* @return The number of projected columns.
+*/
+DUCKDB_C_API idx_t duckdb_init_get_column_count(duckdb_init_info info);
+
+/*!
+Returns the column index of the projected column at the specified position.
+
+This function must be used if projection pushdown is enabled to figure out which columns to emit.
+
+* @param info The info object
+* @param column_index The index at which to get the projected column index, from 0..duckdb_init_get_column_count(info)
+* @return The column index of the projected column.
+*/
+DUCKDB_C_API idx_t duckdb_init_get_column_index(duckdb_init_info info, idx_t column_index);
+
+/*!
+Sets how many threads can process this table function in parallel (default: 1)
+
+* @param info The info object
+* @param max_threads The maximum amount of threads that can process this table function
+*/
+DUCKDB_C_API void duckdb_init_set_max_threads(duckdb_init_info info, idx_t max_threads);
+
+/*!
+Report that an error has occurred while calling init.
+
+* @param info The info object
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_init_set_error(duckdb_init_info info, const char *error);
+
+//===--------------------------------------------------------------------===//
+// Table Function
+//===--------------------------------------------------------------------===//
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_table_function_set_extra_info`.
+
+* @param info The info object
+* @return The extra info
+*/
+DUCKDB_C_API void *duckdb_function_get_extra_info(duckdb_function_info info);
+
+/*!
+Gets the table function's bind data set by `duckdb_bind_set_bind_data`.
+
+Note that the bind data is read-only.
+For tracking state, use the init data instead.
+
+* @param info The function info object.
+* @return The bind data object.
+*/
+DUCKDB_C_API void *duckdb_function_get_bind_data(duckdb_function_info info);
+
+/*!
+Gets the init data set by `duckdb_init_set_init_data` during the init.
+
+* @param info The info object
+* @return The init data object
+*/
+DUCKDB_C_API void *duckdb_function_get_init_data(duckdb_function_info info);
+
+/*!
+Gets the thread-local init data set by `duckdb_init_set_init_data` during the local_init.
+
+* @param info The info object
+* @return The init data object
+*/
+DUCKDB_C_API void *duckdb_function_get_local_init_data(duckdb_function_info info);
+
+/*!
+Report that an error has occurred while executing the function.
+
+* @param info The info object
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_function_set_error(duckdb_function_info info, const char *error);
+
+//===--------------------------------------------------------------------===//
+// Replacement Scans
+//===--------------------------------------------------------------------===//
+
+/*!
+Add a replacement scan definition to the specified database.
+
+* @param db The database object to add the replacement scan to
+* @param replacement The replacement scan callback
+* @param extra_data Extra data that is passed back into the specified callback
+* @param delete_callback The delete callback to call on the extra data, if any
+*/
+DUCKDB_C_API void duckdb_add_replacement_scan(duckdb_database db, duckdb_replacement_callback_t replacement,
+                                              void *extra_data, duckdb_delete_callback_t delete_callback);
+
+/*!
+Sets the replacement function name. If this function is called in the replacement callback,
+the replacement scan is performed. If it is not called, the replacement callback is not performed.
+
+* @param info The info object
+* @param function_name The function name to substitute.
+*/
+DUCKDB_C_API void duckdb_replacement_scan_set_function_name(duckdb_replacement_scan_info info,
+                                                            const char *function_name);
+
+/*!
+Adds a parameter to the replacement scan function.
+
+* @param info The info object
+* @param parameter The parameter to add.
+*/
+DUCKDB_C_API void duckdb_replacement_scan_add_parameter(duckdb_replacement_scan_info info, duckdb_value parameter);
+
+/*!
+Report that an error has occurred while executing the replacement scan.
+
+* @param info The info object
+* @param error The error message
+*/
+DUCKDB_C_API void duckdb_replacement_scan_set_error(duckdb_replacement_scan_info info, const char *error);
+
+//===--------------------------------------------------------------------===//
+// Profiling Info
+//===--------------------------------------------------------------------===//
+
+/*!
+Returns the root node of the profiling information. Returns nullptr, if profiling is not enabled.
+
+* @param connection A connection object.
+* @return A profiling information object.
+*/
+DUCKDB_C_API duckdb_profiling_info duckdb_get_profiling_info(duckdb_connection connection);
+
+/*!
+Returns the value of the metric of the current profiling info node. Returns nullptr, if the metric does
+ not exist or is not enabled. Currently, the value holds a string, and you can retrieve the string
+ by calling the corresponding function: char *duckdb_get_varchar(duckdb_value value).
+
+* @param info A profiling information object.
+* @param key The name of the requested metric.
+* @return The value of the metric. Must be freed with `duckdb_destroy_value`
+*/
+DUCKDB_C_API duckdb_value duckdb_profiling_info_get_value(duckdb_profiling_info info, const char *key);
+
+/*!
+Returns the key-value metric map of this profiling node as a MAP duckdb_value.
+The individual elements are accessible via the duckdb_value MAP functions.
+
+* @param info A profiling information object.
+* @return The key-value metric map as a MAP duckdb_value.
+*/
+DUCKDB_C_API duckdb_value duckdb_profiling_info_get_metrics(duckdb_profiling_info info);
+
+/*!
+Returns the number of children in the current profiling info node.
+
+* @param info A profiling information object.
+* @return The number of children in the current node.
+*/
+DUCKDB_C_API idx_t duckdb_profiling_info_get_child_count(duckdb_profiling_info info);
+
+/*!
+Returns the child node at the specified index.
+
+* @param info A profiling information object.
+* @param index The index of the child node.
+* @return The child node at the specified index.
+*/
+DUCKDB_C_API duckdb_profiling_info duckdb_profiling_info_get_child(duckdb_profiling_info info, idx_t index);
+
+//===--------------------------------------------------------------------===//
+// Appender
+//===--------------------------------------------------------------------===//
+
+// Appenders are the most efficient way of loading data into DuckDB from within the C API.
+// They are recommended for fast data loading as they perform better than prepared statements or individual `INSERT
+// INTO` statements.
+
+// Appends are possible in row-wise format, and by appending entire data chunks.
+
+// Row-wise: for every column, a `duckdb_append_[type]` call should be made. After finishing all appends to a row, call
+// `duckdb_appender_end_row`.
+
+// Chunk-wise: Consecutively call `duckdb_append_data_chunk` until all chunks have been appended.
+
+// After all data has been appended, call `duckdb_appender_close` to finalize the appender followed by
+// `duckdb_appender_destroy` to clean up the memory.
+
+/*!
+Creates an appender object.
+
+Note that the object must be destroyed with `duckdb_appender_destroy`.
+
+* @param connection The connection context to create the appender in.
+* @param schema The schema of the table to append to, or `nullptr` for the default schema.
+* @param table The table name to append to.
+* @param out_appender The resulting appender object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_create(duckdb_connection connection, const char *schema, const char *table,
+                                                 duckdb_appender *out_appender);
+
+/*!
+Creates an appender object.
+
+Note that the object must be destroyed with `duckdb_appender_destroy`.
+
+* @param connection The connection context to create the appender in.
+* @param catalog The catalog of the table to append to, or `nullptr` for the default catalog.
+* @param schema The schema of the table to append to, or `nullptr` for the default schema.
+* @param table The table name to append to.
+* @param out_appender The resulting appender object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_create_ext(duckdb_connection connection, const char *catalog,
+                                                     const char *schema, const char *table,
+                                                     duckdb_appender *out_appender);
+
+/*!
+Creates an appender object that executes the given query with any data appended to it.
+
+Note that the object must be destroyed with `duckdb_appender_destroy`.
+
+* @param connection The connection context to create the appender in.
+* @param query The query to execute, can be an INSERT, DELETE, UPDATE or MERGE INTO statement.
+* @param column_count The number of columns to append.
+* @param types The types of the columns to append.
+* @param table_name (optionally) the table name used to refer to the appended data, defaults to "appended_data".
+* @param column_names (optionally) the list of column names, defaults to "col1", "col2", ...
+* @param out_appender The resulting appender object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_create_query(duckdb_connection connection, const char *query,
+                                                       idx_t column_count, duckdb_logical_type *types,
+                                                       const char *table_name, const char **column_names,
+                                                       duckdb_appender *out_appender);
+
+/*!
+Returns the number of columns that belong to the appender.
+If there is no active column list, then this equals the table's physical columns.
+
+* @param appender The appender to get the column count from.
+* @return The number of columns in the data chunks.
+*/
+DUCKDB_C_API idx_t duckdb_appender_column_count(duckdb_appender appender);
+
+/*!
+Returns the type of the column at the specified index. This is either a type in the active column list, or the same type
+as a column in the receiving table.
+
+Note: The resulting type must be destroyed with `duckdb_destroy_logical_type`.
+
+* @param appender The appender to get the column type from.
+* @param col_idx The index of the column to get the type of.
+* @return The `duckdb_logical_type` of the column.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_appender_column_type(duckdb_appender appender, idx_t col_idx);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+Use duckdb_appender_error_data instead.
+
+Returns the error message associated with the appender.
+If the appender has no error message, this returns `nullptr` instead.
+
+The error message should not be freed. It will be de-allocated when `duckdb_appender_destroy` is called.
+
+* @param appender The appender to get the error from.
+* @return The error message, or `nullptr` if there is none.
+*/
+DUCKDB_C_API const char *duckdb_appender_error(duckdb_appender appender);
+
+#endif
+
+/*!
+Returns the error data associated with the appender.
+Must be destroyed with duckdb_destroy_error_data.
+
+* @param appender The appender to get the error data from.
+* @return The error data.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_appender_error_data(duckdb_appender appender);
+
+/*!
+Flush the appender to the table, forcing the cache of the appender to be cleared. If flushing the data triggers a
+constraint violation or any other error, then all data is invalidated, and this function returns DuckDBError.
+It is not possible to append more values. Call duckdb_appender_error_data to obtain the error data followed by
+duckdb_appender_destroy to destroy the invalidated appender.
+
+* @param appender The appender to flush.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_flush(duckdb_appender appender);
+
+/*!
+Closes the appender by flushing all intermediate states and closing it for further appends. If flushing the data
+triggers a constraint violation or any other error, then all data is invalidated, and this function returns DuckDBError.
+Call duckdb_appender_error_data to obtain the error data followed by duckdb_appender_destroy to destroy the invalidated
+appender.
+
+* @param appender The appender to flush and close.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_close(duckdb_appender appender);
+
+/*!
+Closes the appender by flushing all intermediate states to the table and destroying it. By destroying it, this function
+de-allocates all memory associated with the appender. If flushing the data triggers a constraint violation,
+then all data is invalidated, and this function returns DuckDBError. Due to the destruction of the appender, it is no
+longer possible to obtain the specific error message with duckdb_appender_error. Therefore, call duckdb_appender_close
+before destroying the appender, if you need insights into the specific error.
+
+* @param appender The appender to flush, close and destroy.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_destroy(duckdb_appender *appender);
+
+/*!
+Appends a column to the active column list of the appender. Immediately flushes all previous data.
+
+The active column list specifies all columns that are expected when flushing the data. Any non-active columns are filled
+with their default values, or NULL.
+
+* @param appender The appender to add the column to.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_add_column(duckdb_appender appender, const char *name);
+
+/*!
+Removes all columns from the active column list of the appender, resetting the appender to treat all columns as active.
+Immediately flushes all previous data.
+
+* @param appender The appender to clear the columns from.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_clear_columns(duckdb_appender appender);
+
+/*!
+A nop function, provided for backwards compatibility reasons. Does nothing. Only `duckdb_appender_end_row` is required.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_begin_row(duckdb_appender appender);
+
+/*!
+Finish the current row of appends. After end_row is called, the next row can be appended.
+
+* @param appender The appender.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_appender_end_row(duckdb_appender appender);
+
+/*!
+Append a DEFAULT value (NULL if DEFAULT not available for column) to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_default(duckdb_appender appender);
+
+/*!
+Append a DEFAULT value, at the specified row and column, (NULL if DEFAULT not available for column) to the chunk created
+from the specified appender. The default value of the column must be a constant value. Non-deterministic expressions
+like nextval('seq') or random() are not supported.
+
+* @param appender The appender to get the default value from.
+* @param chunk The data chunk to append the default value to.
+* @param col The chunk column index to append the default value to.
+* @param row The chunk row index to append the default value to.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_default_to_chunk(duckdb_appender appender, duckdb_data_chunk chunk, idx_t col,
+                                                         idx_t row);
+
+/*!
+Append a bool value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_bool(duckdb_appender appender, bool value);
+
+/*!
+Append an int8_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_int8(duckdb_appender appender, int8_t value);
+
+/*!
+Append an int16_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_int16(duckdb_appender appender, int16_t value);
+
+/*!
+Append an int32_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_int32(duckdb_appender appender, int32_t value);
+
+/*!
+Append an int64_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_int64(duckdb_appender appender, int64_t value);
+
+/*!
+Append a duckdb_hugeint value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_hugeint(duckdb_appender appender, duckdb_hugeint value);
+
+/*!
+Append a uint8_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_uint8(duckdb_appender appender, uint8_t value);
+
+/*!
+Append a uint16_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_uint16(duckdb_appender appender, uint16_t value);
+
+/*!
+Append a uint32_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_uint32(duckdb_appender appender, uint32_t value);
+
+/*!
+Append a uint64_t value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_uint64(duckdb_appender appender, uint64_t value);
+
+/*!
+Append a duckdb_uhugeint value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_uhugeint(duckdb_appender appender, duckdb_uhugeint value);
+
+/*!
+Append a float value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_float(duckdb_appender appender, float value);
+
+/*!
+Append a double value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_double(duckdb_appender appender, double value);
+
+/*!
+Append a duckdb_date value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_date(duckdb_appender appender, duckdb_date value);
+
+/*!
+Append a duckdb_time value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_time(duckdb_appender appender, duckdb_time value);
+
+/*!
+Append a duckdb_timestamp value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_timestamp(duckdb_appender appender, duckdb_timestamp value);
+
+/*!
+Append a duckdb_interval value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_interval(duckdb_appender appender, duckdb_interval value);
+
+/*!
+Append a varchar value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_varchar(duckdb_appender appender, const char *val);
+
+/*!
+Append a varchar value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_varchar_length(duckdb_appender appender, const char *val, idx_t length);
+
+/*!
+Append a blob value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_blob(duckdb_appender appender, const void *data, idx_t length);
+
+/*!
+Append a NULL value to the appender (of any type).
+*/
+DUCKDB_C_API duckdb_state duckdb_append_null(duckdb_appender appender);
+
+/*!
+Append a duckdb_value to the appender.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_value(duckdb_appender appender, duckdb_value value);
+
+/*!
+Appends a pre-filled data chunk to the specified appender.
+ Attempts casting, if the data chunk types do not match the active appender types.
+
+* @param appender The appender to append to.
+* @param chunk The data chunk to append.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_append_data_chunk(duckdb_appender appender, duckdb_data_chunk chunk);
+
+//===--------------------------------------------------------------------===//
+// Table Description
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a table description object. Note that `duckdb_table_description_destroy` should always be called on the
+resulting table_description, even if the function returns `DuckDBError`.
+
+* @param connection The connection context.
+* @param schema The schema of the table, or `nullptr` for the default schema.
+* @param table The table name.
+* @param out The resulting table description object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_table_description_create(duckdb_connection connection, const char *schema,
+                                                          const char *table, duckdb_table_description *out);
+
+/*!
+Creates a table description object. Note that `duckdb_table_description_destroy` must be called on the resulting
+table_description, even if the function returns `DuckDBError`.
+
+* @param connection The connection context.
+* @param catalog The catalog (database) name of the table, or `nullptr` for the default catalog.
+* @param schema The schema of the table, or `nullptr` for the default schema.
+* @param table The table name.
+* @param out The resulting table description object.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_table_description_create_ext(duckdb_connection connection, const char *catalog,
+                                                              const char *schema, const char *table,
+                                                              duckdb_table_description *out);
+
+/*!
+Destroy the TableDescription object.
+
+* @param table_description The table_description to destroy.
+*/
+DUCKDB_C_API void duckdb_table_description_destroy(duckdb_table_description *table_description);
+
+/*!
+Returns the error message associated with the given table_description.
+If the table_description has no error message, this returns `nullptr` instead.
+The error message should not be freed. It will be de-allocated when `duckdb_table_description_destroy` is called.
+
+* @param table_description The table_description to get the error from.
+* @return The error message, or `nullptr` if there is none.
+*/
+DUCKDB_C_API const char *duckdb_table_description_error(duckdb_table_description table_description);
+
+/*!
+Check if the column at 'index' index of the table has a DEFAULT expression.
+
+* @param table_description The table_description to query.
+* @param index The index of the column to query.
+* @param out The out-parameter used to store the result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_column_has_default(duckdb_table_description table_description, idx_t index, bool *out);
+
+/*!
+Obtain the column name at 'index'.
+The out result must be destroyed with `duckdb_free`.
+
+* @param table_description The table_description to query.
+* @param index The index of the column to query.
+* @return The column name.
+*/
+DUCKDB_C_API char *duckdb_table_description_get_column_name(duckdb_table_description table_description, idx_t index);
+
+//===--------------------------------------------------------------------===//
+// Arrow Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Transforms a DuckDB Schema into an Arrow Schema
+
+* @param arrow_options The Arrow settings used to produce arrow.
+* @param types The DuckDB logical types for each column in the schema.
+* @param names The names for each column in the schema.
+* @param column_count The number of columns that exist in the schema.
+* @param out_schema The resulting arrow schema. Must be destroyed with `out_schema->release(out_schema)`.
+* @return The error data. Must be destroyed with `duckdb_destroy_error_data`.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_to_arrow_schema(duckdb_arrow_options arrow_options, duckdb_logical_type *types,
+                                                      const char **names, idx_t column_count,
+                                                      struct ArrowSchema *out_schema);
+
+/*!
+Transforms a DuckDB data chunk into an Arrow array.
+
+* @param arrow_options The Arrow settings used to produce arrow.
+* @param chunk The DuckDB data chunk to convert.
+* @param out_arrow_array The output Arrow structure that will hold the converted data. Must be released with
+`out_arrow_array->release(out_arrow_array)`
+* @return The error data. Must be destroyed with `duckdb_destroy_error_data`.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_data_chunk_to_arrow(duckdb_arrow_options arrow_options, duckdb_data_chunk chunk,
+                                                          struct ArrowArray *out_arrow_array);
+
+/*!
+Transforms an Arrow Schema into a DuckDB Schema.
+
+* @param connection The connection to get the transformation settings from.
+* @param schema The input Arrow schema. Must be released with `schema->release(schema)`.
+* @param out_types The Arrow converted schema with extra information about the arrow types. Must be destroyed with
+`duckdb_destroy_arrow_converted_schema`.
+* @return The error data. Must be destroyed with `duckdb_destroy_error_data`.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_schema_from_arrow(duckdb_connection connection, struct ArrowSchema *schema,
+                                                        duckdb_arrow_converted_schema *out_types);
+
+/*!
+Transforms an Arrow array into a DuckDB data chunk. The data chunk will retain ownership of the underlying Arrow data.
+
+* @param connection The connection to get the transformation settings from.
+* @param arrow_array The input Arrow array. Data ownership is passed on to DuckDB's DataChunk, the underlying object
+does not need to be released and won't have ownership of the data.
+* @param converted_schema The Arrow converted schema with extra information about the arrow types.
+* @param out_chunk The resulting DuckDB data chunk. Must be destroyed by duckdb_destroy_data_chunk.
+* @return The error data. Must be destroyed with `duckdb_destroy_error_data`.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_data_chunk_from_arrow(duckdb_connection connection,
+                                                            struct ArrowArray *arrow_array,
+                                                            duckdb_arrow_converted_schema converted_schema,
+                                                            duckdb_data_chunk *out_chunk);
+
+/*!
+Destroys the arrow converted schema and de-allocates all memory allocated for that arrow converted schema.
+
+* @param arrow_converted_schema The arrow converted schema to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_arrow_converted_schema(duckdb_arrow_converted_schema *arrow_converted_schema);
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Executes a SQL query within a connection and stores the full (materialized) result in an arrow structure.
+If the query fails to execute, DuckDBError is returned and the error message can be retrieved by calling
+`duckdb_query_arrow_error`.
+
+Note that after running `duckdb_query_arrow`, `duckdb_destroy_arrow` must be called on the result object even if the
+query fails, otherwise the error stored within the result will not be freed correctly.
+
+* @param connection The connection to perform the query in.
+* @param query The SQL query to run.
+* @param out_result The query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_query_arrow(duckdb_connection connection, const char *query, duckdb_arrow *out_result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Fetch the internal arrow schema from the arrow result. Remember to call release on the respective
+ArrowSchema object.
+
+* @param result The result to fetch the schema from.
+* @param out_schema The output schema.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_query_arrow_schema(duckdb_arrow result, duckdb_arrow_schema *out_schema);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Fetch the internal arrow schema from the prepared statement. Remember to call release on the respective
+ArrowSchema object.
+
+* @param prepared The prepared statement to fetch the schema from.
+* @param out_schema The output schema.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_prepared_arrow_schema(duckdb_prepared_statement prepared,
+                                                       duckdb_arrow_schema *out_schema);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Convert a data chunk into an arrow struct array. Remember to call release on the respective
+ArrowArray object.
+
+* @param result The result object the data chunk have been fetched from.
+* @param chunk The data chunk to convert.
+* @param out_array The output array.
+*/
+DUCKDB_C_API void duckdb_result_arrow_array(duckdb_result result, duckdb_data_chunk chunk,
+                                            duckdb_arrow_array *out_array);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Fetch an internal arrow struct array from the arrow result. Remember to call release on the respective
+ArrowArray object.
+
+This function can be called multiple time to get next chunks, which will free the previous out_array.
+So consume the out_array before calling this function again.
+
+* @param result The result to fetch the array from.
+* @param out_array The output array.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_query_arrow_array(duckdb_arrow result, duckdb_arrow_array *out_array);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Returns the number of columns present in the arrow result object.
+
+* @param result The result object.
+* @return The number of columns present in the result object.
+*/
+DUCKDB_C_API idx_t duckdb_arrow_column_count(duckdb_arrow result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Returns the number of rows present in the arrow result object.
+
+* @param result The result object.
+* @return The number of rows present in the result object.
+*/
+DUCKDB_C_API idx_t duckdb_arrow_row_count(duckdb_arrow result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Returns the number of rows changed by the query stored in the arrow result. This is relevant only for
+INSERT/UPDATE/DELETE queries. For other queries the rows_changed will be 0.
+
+* @param result The result object.
+* @return The number of rows changed.
+*/
+DUCKDB_C_API idx_t duckdb_arrow_rows_changed(duckdb_arrow result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+ Returns the error message contained within the result. The error is only set if `duckdb_query_arrow` returns
+`DuckDBError`.
+
+The error message should not be freed. It will be de-allocated when `duckdb_destroy_arrow` is called.
+
+* @param result The result object to fetch the error from.
+* @return The error of the result.
+*/
+DUCKDB_C_API const char *duckdb_query_arrow_error(duckdb_arrow result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Closes the result and de-allocates all memory allocated for the arrow result.
+
+* @param result The result to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_arrow(duckdb_arrow *result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Releases the arrow array stream and de-allocates its memory.
+
+* @param stream_p The arrow array stream to destroy.
+*/
+DUCKDB_C_API void duckdb_destroy_arrow_stream(duckdb_arrow_stream *stream_p);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Executes the prepared statement with the given bound parameters, and returns an arrow query result.
+Note that after running `duckdb_execute_prepared_arrow`, `duckdb_destroy_arrow` must be called on the result object.
+
+* @param prepared_statement The prepared statement to execute.
+* @param out_result The query result.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_execute_prepared_arrow(duckdb_prepared_statement prepared_statement,
+                                                        duckdb_arrow *out_result);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Scans the Arrow stream and creates a view with the given name.
+
+* @param connection The connection on which to execute the scan.
+* @param table_name Name of the temporary view to create.
+* @param arrow Arrow stream wrapper.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_arrow_scan(duckdb_connection connection, const char *table_name,
+                                            duckdb_arrow_stream arrow);
+
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Scans the Arrow array and creates a view with the given name.
+Note that after running `duckdb_arrow_array_scan`, `duckdb_destroy_arrow_stream` must be called on the out stream.
+
+* @param connection The connection on which to execute the scan.
+* @param table_name Name of the temporary view to create.
+* @param arrow_schema Arrow schema wrapper.
+* @param arrow_array Arrow array wrapper.
+* @param out_stream Output array stream that wraps around the passed schema, for releasing/deleting once done.
+* @return `DuckDBSuccess` on success or `DuckDBError` on failure.
+*/
+DUCKDB_C_API duckdb_state duckdb_arrow_array_scan(duckdb_connection connection, const char *table_name,
+                                                  duckdb_arrow_schema arrow_schema, duckdb_arrow_array arrow_array,
+                                                  duckdb_arrow_stream *out_stream);
+
+#endif
+
+//===--------------------------------------------------------------------===//
+// Threading Information
+//===--------------------------------------------------------------------===//
+
+/*!
+Execute DuckDB tasks on this thread.
+
+Will return after `max_tasks` have been executed, or if there are no more tasks present.
+
+* @param database The database object to execute tasks for
+* @param max_tasks The maximum amount of tasks to execute
+*/
+DUCKDB_C_API void duckdb_execute_tasks(duckdb_database database, idx_t max_tasks);
+
+/*!
+Creates a task state that can be used with duckdb_execute_tasks_state to execute tasks until
+`duckdb_finish_execution` is called on the state.
+
+`duckdb_destroy_state` must be called on the result.
+
+* @param database The database object to create the task state for
+* @return The task state that can be used with duckdb_execute_tasks_state.
+*/
+DUCKDB_C_API duckdb_task_state duckdb_create_task_state(duckdb_database database);
+
+/*!
+Execute DuckDB tasks on this thread.
+
+The thread will keep on executing tasks forever, until duckdb_finish_execution is called on the state.
+Multiple threads can share the same duckdb_task_state.
+
+* @param state The task state of the executor
+*/
+DUCKDB_C_API void duckdb_execute_tasks_state(duckdb_task_state state);
+
+/*!
+Execute DuckDB tasks on this thread.
+
+The thread will keep on executing tasks until either duckdb_finish_execution is called on the state,
+max_tasks tasks have been executed or there are no more tasks to be executed.
+
+Multiple threads can share the same duckdb_task_state.
+
+* @param state The task state of the executor
+* @param max_tasks The maximum amount of tasks to execute
+* @return The amount of tasks that have actually been executed
+*/
+DUCKDB_C_API idx_t duckdb_execute_n_tasks_state(duckdb_task_state state, idx_t max_tasks);
+
+/*!
+Finish execution on a specific task.
+
+* @param state The task state to finish execution
+*/
+DUCKDB_C_API void duckdb_finish_execution(duckdb_task_state state);
+
+/*!
+Check if the provided duckdb_task_state has finished execution
+
+* @param state The task state to inspect
+* @return Whether or not duckdb_finish_execution has been called on the task state
+*/
+DUCKDB_C_API bool duckdb_task_state_is_finished(duckdb_task_state state);
+
+/*!
+Destroys the task state returned from duckdb_create_task_state.
+
+Note that this should not be called while there is an active duckdb_execute_tasks_state running
+on the task state.
+
+* @param state The task state to clean up
+*/
+DUCKDB_C_API void duckdb_destroy_task_state(duckdb_task_state state);
+
+/*!
+Returns true if the execution of the current query is finished.
+
+* @param con The connection on which to check
+*/
+DUCKDB_C_API bool duckdb_execution_is_finished(duckdb_connection con);
+
+//===--------------------------------------------------------------------===//
+// Streaming Result Interface
+//===--------------------------------------------------------------------===//
+
+#ifndef DUCKDB_API_NO_DEPRECATED
+/*!
+**DEPRECATION NOTICE**: This method is scheduled for removal in a future release.
+
+Fetches a data chunk from the (streaming) duckdb_result. This function should be called repeatedly until the result is
+exhausted.
+
+The result must be destroyed with `duckdb_destroy_data_chunk`.
+
+This function can only be used on duckdb_results created with 'duckdb_pending_prepared_streaming'
+
+If this function is used, none of the other result functions can be used and vice versa (i.e. this function cannot be
+mixed with the legacy result functions or the materialized result functions).
+
+It is not known beforehand how many chunks will be returned by this result.
+
+* @param result The result object to fetch the data chunk from.
+* @return The resulting data chunk. Returns `NULL` if the result has an error.
+*/
+DUCKDB_C_API duckdb_data_chunk duckdb_stream_fetch_chunk(duckdb_result result);
+
+#endif
+
+/*!
+Fetches a data chunk from a duckdb_result. This function should be called repeatedly until the result is exhausted.
+
+The result must be destroyed with `duckdb_destroy_data_chunk`.
+
+It is not known beforehand how many chunks will be returned by this result.
+
+* @param result The result object to fetch the data chunk from.
+* @return The resulting data chunk. Returns `NULL` if the result has an error.
+*/
+DUCKDB_C_API duckdb_data_chunk duckdb_fetch_chunk(duckdb_result result);
+
+//===--------------------------------------------------------------------===//
+// Cast Functions
+//===--------------------------------------------------------------------===//
+
+/*!
+Creates a new cast function object.
+
+* @return The cast function object.
+*/
+DUCKDB_C_API duckdb_cast_function duckdb_create_cast_function();
+
+/*!
+Sets the source type of the cast function.
+
+* @param cast_function The cast function object.
+* @param source_type The source type to set.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_source_type(duckdb_cast_function cast_function,
+                                                       duckdb_logical_type source_type);
+
+/*!
+Sets the target type of the cast function.
+
+* @param cast_function The cast function object.
+* @param target_type The target type to set.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_target_type(duckdb_cast_function cast_function,
+                                                       duckdb_logical_type target_type);
+
+/*!
+Sets the "cost" of implicitly casting the source type to the target type using this function.
+
+* @param cast_function The cast function object.
+* @param cost The cost to set.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_implicit_cast_cost(duckdb_cast_function cast_function, int64_t cost);
+
+/*!
+Sets the actual cast function to use.
+
+* @param cast_function The cast function object.
+* @param function The function to set.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_function(duckdb_cast_function cast_function,
+                                                    duckdb_cast_function_t function);
+
+/*!
+Assigns extra information to the cast function that can be fetched during execution, etc.
+
+* @param extra_info The extra information
+* @param destroy The callback that will be called to destroy the extra information (if any)
+*/
+DUCKDB_C_API void duckdb_cast_function_set_extra_info(duckdb_cast_function cast_function, void *extra_info,
+                                                      duckdb_delete_callback_t destroy);
+
+/*!
+Retrieves the extra info of the function as set in `duckdb_cast_function_set_extra_info`.
+
+* @param info The info object.
+* @return The extra info.
+*/
+DUCKDB_C_API void *duckdb_cast_function_get_extra_info(duckdb_function_info info);
+
+/*!
+Get the cast execution mode from the given function info.
+
+* @param info The info object.
+* @return The cast mode.
+*/
+DUCKDB_C_API duckdb_cast_mode duckdb_cast_function_get_cast_mode(duckdb_function_info info);
+
+/*!
+Report that an error has occurred while executing the cast function.
+
+* @param info The info object.
+* @param error The error message.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_error(duckdb_function_info info, const char *error);
+
+/*!
+Report that an error has occurred while executing the cast function, setting the corresponding output row to NULL.
+
+* @param info The info object.
+* @param error The error message.
+* @param row The index of the row within the output vector to set to NULL.
+* @param output The output vector.
+*/
+DUCKDB_C_API void duckdb_cast_function_set_row_error(duckdb_function_info info, const char *error, idx_t row,
+                                                     duckdb_vector output);
+
+/*!
+Registers a cast function within the given connection.
+
+* @param con The connection to use.
+* @param cast_function The cast function to register.
+* @return Whether or not the registration was successful.
+*/
+DUCKDB_C_API duckdb_state duckdb_register_cast_function(duckdb_connection con, duckdb_cast_function cast_function);
+
+/*!
+Destroys the cast function object.
+
+* @param cast_function The cast function object.
+*/
+DUCKDB_C_API void duckdb_destroy_cast_function(duckdb_cast_function *cast_function);
+
+//===--------------------------------------------------------------------===//
+// Expression Interface
+//===--------------------------------------------------------------------===//
+
+/*!
+Destroys the expression and de-allocates its memory.
+
+* @param expr A pointer to the expression.
+*/
+DUCKDB_C_API void duckdb_destroy_expression(duckdb_expression *expr);
+
+/*!
+Returns the return type of an expression.
+
+* @param expr The expression.
+* @return The return type. Must be destroyed with `duckdb_destroy_logical_type`.
+*/
+DUCKDB_C_API duckdb_logical_type duckdb_expression_return_type(duckdb_expression expr);
+
+/*!
+Returns whether the expression is foldable into a value or not.
+
+* @param expr The expression.
+* @return True, if the expression is foldable, else false.
+*/
+DUCKDB_C_API bool duckdb_expression_is_foldable(duckdb_expression expr);
+
+/*!
+Folds an expression creating a folded value.
+
+* @param context The client context.
+* @param expr The expression. Must be foldable.
+* @param out_value The folded value, if folding was successful. Must be destroyed with `duckdb_destroy_value`.
+* @return The error data. Must be destroyed with `duckdb_destroy_error_data`.
+*/
+DUCKDB_C_API duckdb_error_data duckdb_expression_fold(duckdb_client_context context, duckdb_expression expr,
+                                                      duckdb_value *out_value);
+
+#endif
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/cbits/duckdb_stub.c b/cbits/duckdb_stub.c
new file mode 100644
--- /dev/null
+++ b/cbits/duckdb_stub.c
@@ -0,0 +1,546 @@
+#include "duckdb.h"
+#include <stdint.h>
+
+// duckdb_query_progress returns a struct by value; expose a pointer-based version
+// so the Haskell FFI can marshal the result and so we can gracefully handle NULL.
+void wrapped_duckdb_query_progress(duckdb_connection connection, duckdb_query_progress_type *out_progress) {
+  if (!out_progress) {
+    return;
+  }
+  *out_progress = duckdb_query_progress(connection);
+}
+
+// duckdb_result_statement_type consumes a duckdb_result by value; this wrapper lets
+// us pass a pointer from Haskell and returns a safe default when NULL is supplied.
+duckdb_statement_type wrapped_duckdb_result_statement_type(const duckdb_result *result) {
+  if (!result) {
+    return DUCKDB_STATEMENT_TYPE_INVALID;
+  }
+  return duckdb_result_statement_type(*result);
+}
+
+// duckdb_result_return_type also expects the result struct by value; wrap it so we
+// can call it from Haskell while avoiding undefined behaviour on NULL.
+duckdb_result_type wrapped_duckdb_result_return_type(const duckdb_result *result) {
+  if (!result) {
+    return DUCKDB_RESULT_TYPE_INVALID;
+  }
+  return duckdb_result_return_type(*result);
+}
+
+// duckdb_result_get_arrow_options mutates the result structure; we pass the pointer
+// through after checking for NULL and casting away const for the FFI surface.
+duckdb_arrow_options wrapped_duckdb_result_get_arrow_options(const duckdb_result *result) {
+  if (!result) {
+    return NULL;
+  }
+  return duckdb_result_get_arrow_options((duckdb_result *)result);
+}
+
+// duckdb_result_get_chunk takes the result by value; we provide a pointer-friendly
+// wrapper that also protects against accidental NULL dereferences.
+duckdb_data_chunk wrapped_duckdb_result_get_chunk(const duckdb_result *result, idx_t chunk_index) {
+  if (!result) {
+    return NULL;
+  }
+  return duckdb_result_get_chunk(*result, chunk_index);
+}
+
+// duckdb_result_chunk_count takes a duckdb_result by value; mirror it with pointer
+// semantics for the Haskell FFI.
+idx_t wrapped_duckdb_result_chunk_count(const duckdb_result *result) {
+  if (!result) {
+    return 0;
+  }
+  return duckdb_result_chunk_count(*result);
+}
+
+// duckdb_result_is_streaming takes the result by value; add a pointer-based shim
+// so we can pass NULL safely from the managed side.
+bool wrapped_duckdb_result_is_streaming(const duckdb_result *result) {
+  if (!result) {
+    return false;
+  }
+  return duckdb_result_is_streaming(*result);
+}
+
+// duckdb_value_hugeint returns a struct by value; expose an out-parameter variant
+// with NULL guards for easier use from Haskell.
+void wrapped_duckdb_value_hugeint(duckdb_result *result, idx_t col, idx_t row, duckdb_hugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_value_hugeint(result, col, row);
+}
+
+// duckdb_value_uhugeint returns a struct by value; wrap it to write into caller-
+// supplied storage and to tolerate NULL pointers.
+void wrapped_duckdb_value_uhugeint(duckdb_result *result, idx_t col, idx_t row, duckdb_uhugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_value_uhugeint(result, col, row);
+}
+
+// duckdb_value_decimal returns a struct by value; provide an out-parameter version
+// so we can marshal it easily.
+void wrapped_duckdb_value_decimal(duckdb_result *result, idx_t col, idx_t row, duckdb_decimal *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_value_decimal(result, col, row);
+}
+
+// duckdb_value_string returns a struct by value; expose a wrapper that fills a
+// duckdb_string for the caller if the output pointer is non-NULL.
+void wrapped_duckdb_value_string(duckdb_result *result, idx_t col, idx_t row, duckdb_string *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_value_string(result, col, row);
+}
+
+// duckdb_value_string_internal also returns a struct by value; same rationale as
+// wrapped_duckdb_value_string.
+void wrapped_duckdb_value_string_internal(duckdb_result *result, idx_t col, idx_t row, duckdb_string *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_value_string_internal(result, col, row);
+}
+
+// duckdb_value_blob returns a struct by value; copy into caller storage and guard
+// against NULL in the wrapper.
+void wrapped_duckdb_value_blob(duckdb_result *result, idx_t col, idx_t row, duckdb_blob *out_blob) {
+  if (!out_blob) {
+    return;
+  }
+  *out_blob = duckdb_value_blob(result, col, row);
+}
+
+// duckdb_value_interval returns a struct by value; expose pointer-friendly variant
+// for the managed bindings.
+void wrapped_duckdb_value_interval(duckdb_result *result, idx_t col, idx_t row, duckdb_interval *out_interval) {
+  if (!out_interval) {
+    return;
+  }
+  *out_interval = duckdb_value_interval(result, col, row);
+}
+
+// duckdb_string_is_inlined expects a struct by value; we pass a pointer and guard
+// against NULL before delegating.
+bool wrapped_duckdb_string_is_inlined(const duckdb_string_t *string) {
+  if (!string) {
+    return false;
+  }
+  return duckdb_string_is_inlined(*string);
+}
+
+// duckdb_string_t_length reads from duckdb_string_t by value; provide a pointer
+// based wrapper that handles NULL safely.
+uint32_t wrapped_duckdb_string_t_length(const duckdb_string_t *string) {
+  if (!string) {
+    return 0;
+  }
+  return duckdb_string_t_length(*string);
+}
+
+// duckdb_result_arrow_array modifies the arrow array; we add NULL checks so FFI
+// callers can omit either pointer without crashing the process.
+void wrapped_duckdb_result_arrow_array(const duckdb_result *result, duckdb_data_chunk chunk, duckdb_arrow_array *out_array) {
+  if (!result || !chunk || !out_array) {
+    return;
+  }
+  duckdb_result_arrow_array(*result, chunk, out_array);
+}
+
+void *wrapped_duckdb_arrow_schema_internal_ptr(duckdb_arrow_schema schema) {
+  if (!schema) {
+    return NULL;
+  }
+  return schema->internal_ptr;
+}
+
+void wrapped_duckdb_arrow_schema_clear_internal_ptr(duckdb_arrow_schema schema) {
+  if (!schema) {
+    return;
+  }
+  schema->internal_ptr = NULL;
+}
+
+void *wrapped_duckdb_arrow_array_internal_ptr(duckdb_arrow_array array) {
+  if (!array) {
+    return NULL;
+  }
+  return array->internal_ptr;
+}
+
+void wrapped_duckdb_arrow_array_clear_internal_ptr(duckdb_arrow_array array) {
+  if (!array) {
+    return;
+  }
+  array->internal_ptr = NULL;
+}
+
+void *wrapped_duckdb_arrow_stream_internal_ptr(duckdb_arrow_stream stream) {
+  if (!stream) {
+    return NULL;
+  }
+  return stream->internal_ptr;
+}
+
+void wrapped_duckdb_arrow_stream_clear_internal_ptr(duckdb_arrow_stream stream) {
+  if (!stream) {
+    return;
+  }
+  stream->internal_ptr = NULL;
+}
+
+// duckdb_stream_fetch_chunk takes duckdb_result by value; provide a pointer form
+// so we can handle NULLs and avoid struct copies in the FFI.
+duckdb_data_chunk wrapped_duckdb_stream_fetch_chunk(const duckdb_result *result) {
+  if (!result) {
+    return NULL;
+  }
+  return duckdb_stream_fetch_chunk(*result);
+}
+
+// duckdb_fetch_chunk also expects duckdb_result by value; same pointer-friendly wrap
+// as above.
+duckdb_data_chunk wrapped_duckdb_fetch_chunk(const duckdb_result *result) {
+  if (!result) {
+    return NULL;
+  }
+  return duckdb_fetch_chunk(*result);
+}
+
+// duckdb_bind_interval consumes duckdb_interval by value; we copy from the pointer
+// so Haskell can pass NULL and avoid struct marshalling complexities.
+duckdb_state wrapped_duckdb_bind_interval(duckdb_prepared_statement statement, idx_t param_idx, const duckdb_interval *val) {
+  duckdb_interval interval_value = {0};
+  if (val) {
+    interval_value = *val;
+  }
+  return duckdb_bind_interval(statement, param_idx, interval_value);
+}
+
+// duckdb_bind_hugeint requires a struct by value; wrap to accept a pointer and
+// provide a zero-initialised default when NULL.
+duckdb_state wrapped_duckdb_bind_hugeint(duckdb_prepared_statement statement, idx_t param_idx, const duckdb_hugeint *val) {
+  duckdb_hugeint hugeint_value = {0};
+  if (val) {
+    hugeint_value = *val;
+  }
+  return duckdb_bind_hugeint(statement, param_idx, hugeint_value);
+}
+
+// duckdb_bind_uhugeint requires a struct by value; same pointer-based shim as for
+// duckdb_bind_hugeint.
+duckdb_state wrapped_duckdb_bind_uhugeint(duckdb_prepared_statement statement, idx_t param_idx, const duckdb_uhugeint *val) {
+  duckdb_uhugeint uhugeint_value = {0};
+  if (val) {
+    uhugeint_value = *val;
+  }
+  return duckdb_bind_uhugeint(statement, param_idx, uhugeint_value);
+}
+
+// duckdb_bind_decimal consumes duckdb_decimal by value; wrap it so we can accept a
+// pointer and default to zero when NULL arrives from Haskell.
+duckdb_state wrapped_duckdb_bind_decimal(duckdb_prepared_statement statement, idx_t param_idx, const duckdb_decimal *val) {
+  duckdb_decimal decimal_value = {0};
+  if (val) {
+    decimal_value = *val;
+  }
+  return duckdb_bind_decimal(statement, param_idx, decimal_value);
+}
+
+// duckdb_create_hugeint takes the struct by value; the wrapper copies from the
+// user pointer so Haskell can choose to pass NULL.
+duckdb_value wrapped_duckdb_create_hugeint(const duckdb_hugeint *input) {
+  duckdb_hugeint value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_hugeint(value);
+}
+
+// duckdb_create_uhugeint takes the struct by value; wrap to accept optional pointer
+// input from the FFI.
+duckdb_value wrapped_duckdb_create_uhugeint(const duckdb_uhugeint *input) {
+  duckdb_uhugeint value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_uhugeint(value);
+}
+
+// duckdb_create_bignum consumes duckdb_bignum by value; copy from pointer so the
+// managed caller can omit it.
+duckdb_value wrapped_duckdb_create_bignum(const duckdb_bignum *input) {
+  duckdb_bignum value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_bignum(value);
+}
+
+// duckdb_create_decimal expects duckdb_decimal by value; pointer wrapper allows us
+// to default the argument and plays nicely with the FFI.
+duckdb_value wrapped_duckdb_create_decimal(const duckdb_decimal *input) {
+  duckdb_decimal value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_decimal(value);
+}
+
+// duckdb_create_interval expects duckdb_interval by value; copy from pointer so
+// the managed caller can pass NULL and avoid struct marshalling.
+duckdb_value wrapped_duckdb_create_interval(const duckdb_interval *input) {
+  duckdb_interval value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_interval(value);
+}
+
+// duckdb_create_bit requires duckdb_bit by value; copy from pointer for ease of use
+// on the Haskell side.
+duckdb_value wrapped_duckdb_create_bit(const duckdb_bit *input) {
+  duckdb_bit value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_bit(value);
+}
+
+// duckdb_create_uuid expects a duckdb_uhugeint by value; wrap it so NULL pointers
+// are handled and the FFI can reuse the helper.
+duckdb_value wrapped_duckdb_create_uuid(const duckdb_uhugeint *input) {
+  duckdb_uhugeint value = {0};
+  if (input) {
+    value = *input;
+  }
+  return duckdb_create_uuid(value);
+}
+
+// duckdb_append_hugeint consumes a struct by value; copy from pointer before
+// delegating to avoid marshalling issues from Haskell.
+duckdb_state wrapped_duckdb_append_hugeint(duckdb_appender appender, const duckdb_hugeint *value) {
+  duckdb_hugeint tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_append_hugeint(appender, tmp);
+}
+
+// duckdb_append_uhugeint also needs the value struct by copy; pointer wrapper keeps
+// the FFI simple and guards NULL.
+duckdb_state wrapped_duckdb_append_uhugeint(duckdb_appender appender, const duckdb_uhugeint *value) {
+  duckdb_uhugeint tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_append_uhugeint(appender, tmp);
+}
+
+// duckdb_append_interval expects a duckdb_interval by value; use a pointer wrapper
+// to appease the Haskell FFI and handle absent values.
+duckdb_state wrapped_duckdb_append_interval(duckdb_appender appender, const duckdb_interval *value) {
+  duckdb_interval tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_append_interval(appender, tmp);
+}
+
+// duckdb_get_hugeint returns a struct by value; write the result into caller
+// storage to avoid struct-return ABI issues in Haskell.
+void wrapped_duckdb_get_hugeint(duckdb_value val, duckdb_hugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_hugeint(val);
+}
+
+// duckdb_get_uhugeint returns a struct by value; same pointer-based extraction as
+// wrapped_duckdb_get_hugeint.
+void wrapped_duckdb_get_uhugeint(duckdb_value val, duckdb_uhugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_uhugeint(val);
+}
+
+// duckdb_get_bignum returns a struct by value; write the result into an out pointer
+// for the managed caller.
+void wrapped_duckdb_get_bignum(duckdb_value val, duckdb_bignum *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_bignum(val);
+}
+
+// duckdb_get_decimal returns a struct by value; provide the same pointer-based
+// access pattern as the other get wrappers.
+void wrapped_duckdb_get_decimal(duckdb_value val, duckdb_decimal *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_decimal(val);
+}
+
+// duckdb_get_interval returns a struct by value; wrap it to write into caller
+// memory.
+void wrapped_duckdb_get_interval(duckdb_value val, duckdb_interval *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_interval(val);
+}
+
+// duckdb_get_blob returns duckdb_blob by value; expose an out-parameter wrapper for
+// the FFI.
+void wrapped_duckdb_get_blob(duckdb_value val, duckdb_blob *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_blob(val);
+}
+
+// duckdb_get_bit returns duckdb_bit by value; wrap it to fill caller-provided
+// storage.
+void wrapped_duckdb_get_bit(duckdb_value val, duckdb_bit *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_bit(val);
+}
+
+// duckdb_get_uuid returns duckdb_uhugeint by value; write into an out pointer for
+// the managed binding.
+void wrapped_duckdb_get_uuid(duckdb_value val, duckdb_uhugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_get_uuid(val);
+}
+
+// duckdb_hugeint_to_double consumes its argument by value; copy from pointer so we
+// can call it from Haskell without struct marshalling.
+double wrapped_duckdb_hugeint_to_double(const duckdb_hugeint *value) {
+  duckdb_hugeint tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_hugeint_to_double(tmp);
+}
+
+// duckdb_double_to_hugeint returns a struct by value; provide an out pointer for
+// the managed side.
+void wrapped_duckdb_double_to_hugeint(double input, duckdb_hugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_double_to_hugeint(input);
+}
+
+// duckdb_double_to_decimal returns duckdb_decimal by value; write into caller
+// storage instead.
+void wrapped_duckdb_double_to_decimal(double input, uint8_t width, uint8_t scale, duckdb_decimal *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_double_to_decimal(input, width, scale);
+}
+
+// duckdb_decimal_to_double consumes its argument by value; copy from pointer so
+// the FFI can pass nullable inputs.
+double wrapped_duckdb_decimal_to_double(const duckdb_decimal *value) {
+  duckdb_decimal tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_decimal_to_double(tmp);
+}
+
+// duckdb_from_date returns a struct by value; emit the result through an
+// out-parameter to avoid struct-return plumbing on the Haskell side.
+void wrapped_duckdb_from_date(duckdb_date date, duckdb_date_struct *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_from_date(date);
+}
+
+// duckdb_to_date expects a struct by value; copy from pointer so NULL can be passed
+// and so the ABI stays simple.
+duckdb_date wrapped_duckdb_to_date(const duckdb_date_struct *input) {
+  duckdb_date_struct tmp = {0};
+  if (input) {
+    tmp = *input;
+  }
+  return duckdb_to_date(tmp);
+}
+
+// duckdb_from_time returns a struct by value; wrap to emit via an out pointer.
+void wrapped_duckdb_from_time(duckdb_time time, duckdb_time_struct *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_from_time(time);
+}
+
+// duckdb_from_time_tz returns a struct by value; provide pointer-based output.
+void wrapped_duckdb_from_time_tz(duckdb_time_tz micros, duckdb_time_tz_struct *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_from_time_tz(micros);
+}
+
+// duckdb_to_time expects a struct by value; copy from pointer for the bindings.
+duckdb_time wrapped_duckdb_to_time(const duckdb_time_struct *input) {
+  duckdb_time_struct tmp = {0};
+  if (input) {
+    tmp = *input;
+  }
+  return duckdb_to_time(tmp);
+}
+
+// duckdb_from_timestamp returns a struct by value; write into caller-provided
+// storage instead.
+void wrapped_duckdb_from_timestamp(duckdb_timestamp ts, duckdb_timestamp_struct *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_from_timestamp(ts);
+}
+
+// duckdb_to_timestamp expects a struct by value; copy from pointer before calling.
+duckdb_timestamp wrapped_duckdb_to_timestamp(const duckdb_timestamp_struct *input) {
+  duckdb_timestamp_struct tmp = {0};
+  if (input) {
+    tmp = *input;
+  }
+  return duckdb_to_timestamp(tmp);
+}
+
+// duckdb_uhugeint_to_double consumes its argument by value; copy from pointer to
+// make it callable from Haskell.
+double wrapped_duckdb_uhugeint_to_double(const duckdb_uhugeint *value) {
+  duckdb_uhugeint tmp = {0};
+  if (value) {
+    tmp = *value;
+  }
+  return duckdb_uhugeint_to_double(tmp);
+}
+
+// duckdb_double_to_uhugeint returns a struct by value; write the result into caller
+// storage instead of relying on struct-return ABI support.
+void wrapped_duckdb_double_to_uhugeint(double input, duckdb_uhugeint *out_value) {
+  if (!out_value) {
+    return;
+  }
+  *out_value = duckdb_double_to_uhugeint(input);
+}
diff --git a/duckdb-ffi.cabal b/duckdb-ffi.cabal
new file mode 100644
--- /dev/null
+++ b/duckdb-ffi.cabal
@@ -0,0 +1,148 @@
+cabal-version:      3.4
+name:               duckdb-ffi
+version:            0.1.4.1
+author:             Matthias Pall Gissurarson
+maintainer:         mpg@mpg.is
+category:           Database
+build-type:         Custom
+license:            MPL-2.0
+license-file:       LICENSE
+synopsis:           Haskell FFI bindings for DuckDB
+homepage:       https://github.com/Tritlo/duckdb-haskell
+bug-reports:    https://github.com/Tritlo/duckdb-haskell/issues
+description:
+    This library provides low-level FFI bindings to DuckDB.
+    Tested with DuckDB version 1.4.0, and 1.4.0.
+    Exposes the full DuckDB C API.
+    Note: requires libduckdb and duckdb.h to be installed on the system.
+extra-doc-files:    CHANGELOG.md
+
+source-repository head
+  type:     git
+  location: https://github.com/Tritlo/duckdb-haskell.git
+  subdir:  duckdb-ffi
+
+
+custom-setup
+  setup-depends: base >=4.18 && <4.21,
+                 Cabal >=3.4 && <3.16,
+                 directory >= 1.3 && <1.4,
+                 filepath >= 1.4 && <1.5,
+                 process >= 1.6 && <1.7,
+
+library
+  default-language:
+    Haskell2010
+  default-extensions:
+        AllowAmbiguousTypes
+      , DataKinds
+      , GADTs
+      , GeneralizedNewtypeDeriving
+      , ImportQualifiedPost
+      , LambdaCase
+      , MultiParamTypeClasses
+      , OverloadedStrings
+      , RankNTypes
+      , TypeApplications
+      , ScopedTypeVariables
+  hs-source-dirs:
+    src
+  exposed-modules:
+    Database.DuckDB.FFI
+  other-modules:
+    Database.DuckDB.FFI.AggregateFunctions
+    Database.DuckDB.FFI.Appender
+    Database.DuckDB.FFI.Arrow
+    Database.DuckDB.FFI.BindValues
+    Database.DuckDB.FFI.CastFunctions
+    Database.DuckDB.FFI.Configuration
+    Database.DuckDB.FFI.DataChunk
+    Database.DuckDB.FFI.ErrorData
+    Database.DuckDB.FFI.ExecutePrepared
+    Database.DuckDB.FFI.Expression
+    Database.DuckDB.FFI.ExtractStatements
+    Database.DuckDB.FFI.Helpers
+    Database.DuckDB.FFI.LogicalTypes
+    Database.DuckDB.FFI.OpenConnect
+    Database.DuckDB.FFI.PendingResult
+    Database.DuckDB.FFI.PreparedStatements
+    Database.DuckDB.FFI.ProfilingInfo
+    Database.DuckDB.FFI.QueryExecution
+    Database.DuckDB.FFI.ReplacementScans
+    Database.DuckDB.FFI.ResultFunctions
+    Database.DuckDB.FFI.SafeFetch
+    Database.DuckDB.FFI.ScalarFunctions
+    Database.DuckDB.FFI.SelectionVector
+    Database.DuckDB.FFI.StreamingResult
+    Database.DuckDB.FFI.TableDescription
+    Database.DuckDB.FFI.TableFunctions
+    Database.DuckDB.FFI.Threading
+    Database.DuckDB.FFI.Types
+    Database.DuckDB.FFI.Validity
+    Database.DuckDB.FFI.ValueInterface
+    Database.DuckDB.FFI.Vector
+  include-dirs: cbits/
+  install-includes: duckdb.h
+  extra-libraries: duckdb
+
+  c-sources: cbits/duckdb_stub.c
+  build-depends:
+      base >=4.18 && <4.19
+    , bytestring >= 0.11 && <0.12
+    , containers >= 0.6 && <0.7
+    , exceptions >= 0.10 && <0.11
+    , mtl >= 2.3 && <2.4
+    , text >= 2.0 && <2.1
+    , transformers >= 0.6 && <0.7
+    , time >= 1.12 && <1.13
+  if os(linux)
+    ghc-options:    -optl-Wl,-rpath,'$ORIGIN/../cbits/duckdb'
+    ld-options:       -Wl,-rpath,'$ORIGIN/../cbits/duckdb'
+
+test-suite duckdb-ffi-tests
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Main.hs
+  other-modules:
+    ConfigurationTest
+    ErrorDataTest
+    OpenConnectTest
+    PreparedStatementsTest
+    BindValuesTest
+    ExecutePreparedStatementsTest
+    ExtractStatementsTest
+    LogicalTypesTest
+    DataChunkTest
+    VectorTest
+    ValidityMaskTest
+    ScalarFunctionsTest
+    TableFunctionsTest
+    SelectionVectorTest
+    AggregateFunctionsTest
+    PendingResultTest
+    ValueInterfaceTest
+    QueryExecutionTest
+    ResultFunctionsTest
+    SafeFetchTest
+    HelpersTest
+    TableDescriptionTest
+    AppenderTest
+    ProfilingInfoTest
+    CastFunctionsTest
+    ExpressionTest
+    ReplacementScansTest
+    ArrowInterfaceDeprecatedTests
+    ArrowInterfaceTest
+    StreamingResultTest
+    ThreadingTest
+    Utils
+  ghc-options:        -threaded
+  default-language:    Haskell2010
+  build-depends:
+      base >=4.18 && <4.19
+    , duckdb-ffi
+    , tasty >=1.5 && <1.6
+    , tasty-hunit >=0.10 && <0.11
+    , tasty-expected-failure >= 0.12 && < 0.13
+    , text >= 2.0 && <2.1
+    , time >= 1.12 && <1.13
diff --git a/src/Database/DuckDB/FFI.hs b/src/Database/DuckDB/FFI.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI.hs
@@ -0,0 +1,73 @@
+{- |
+Module      : Database.DuckDB.FFI
+Description : Aggregated re-exports for the DuckDB C API bindings
+
+The individual modules under @Database.DuckDB.FFI.*@ group the raw FFI imports
+by the sections documented in @api.md@.  This module preserves the historic
+surface area by re-exporting all of those symbols.
+-}
+module Database.DuckDB.FFI (
+    module Database.DuckDB.FFI.Types,
+    module Database.DuckDB.FFI.OpenConnect,
+    module Database.DuckDB.FFI.Configuration,
+    module Database.DuckDB.FFI.ErrorData,
+    module Database.DuckDB.FFI.QueryExecution,
+    module Database.DuckDB.FFI.ResultFunctions,
+    module Database.DuckDB.FFI.SafeFetch,
+    module Database.DuckDB.FFI.Helpers,
+    module Database.DuckDB.FFI.PreparedStatements,
+    module Database.DuckDB.FFI.BindValues,
+    module Database.DuckDB.FFI.ExecutePrepared,
+    module Database.DuckDB.FFI.ExtractStatements,
+    module Database.DuckDB.FFI.PendingResult,
+    module Database.DuckDB.FFI.ValueInterface,
+    module Database.DuckDB.FFI.LogicalTypes,
+    module Database.DuckDB.FFI.DataChunk,
+    module Database.DuckDB.FFI.Vector,
+    module Database.DuckDB.FFI.Validity,
+    module Database.DuckDB.FFI.ScalarFunctions,
+    module Database.DuckDB.FFI.TableFunctions,
+    module Database.DuckDB.FFI.SelectionVector,
+    module Database.DuckDB.FFI.AggregateFunctions,
+    module Database.DuckDB.FFI.ReplacementScans,
+    module Database.DuckDB.FFI.ProfilingInfo,
+    module Database.DuckDB.FFI.Appender,
+    module Database.DuckDB.FFI.TableDescription,
+    module Database.DuckDB.FFI.Arrow,
+    module Database.DuckDB.FFI.Threading,
+    module Database.DuckDB.FFI.StreamingResult,
+    module Database.DuckDB.FFI.CastFunctions,
+    module Database.DuckDB.FFI.Expression,
+) where
+
+import Database.DuckDB.FFI.AggregateFunctions
+import Database.DuckDB.FFI.Appender
+import Database.DuckDB.FFI.Arrow
+import Database.DuckDB.FFI.BindValues
+import Database.DuckDB.FFI.CastFunctions
+import Database.DuckDB.FFI.Configuration
+import Database.DuckDB.FFI.DataChunk
+import Database.DuckDB.FFI.ErrorData
+import Database.DuckDB.FFI.ExecutePrepared
+import Database.DuckDB.FFI.Expression
+import Database.DuckDB.FFI.ExtractStatements
+import Database.DuckDB.FFI.Helpers
+import Database.DuckDB.FFI.LogicalTypes
+import Database.DuckDB.FFI.OpenConnect
+import Database.DuckDB.FFI.PendingResult
+import Database.DuckDB.FFI.PreparedStatements
+import Database.DuckDB.FFI.ProfilingInfo
+import Database.DuckDB.FFI.QueryExecution
+import Database.DuckDB.FFI.ReplacementScans
+import Database.DuckDB.FFI.ResultFunctions
+import Database.DuckDB.FFI.SafeFetch
+import Database.DuckDB.FFI.ScalarFunctions
+import Database.DuckDB.FFI.SelectionVector
+import Database.DuckDB.FFI.StreamingResult
+import Database.DuckDB.FFI.TableDescription
+import Database.DuckDB.FFI.TableFunctions
+import Database.DuckDB.FFI.Threading
+import Database.DuckDB.FFI.Types
+import Database.DuckDB.FFI.Validity
+import Database.DuckDB.FFI.ValueInterface
+import Database.DuckDB.FFI.Vector
diff --git a/src/Database/DuckDB/FFI/AggregateFunctions.hs b/src/Database/DuckDB/FFI/AggregateFunctions.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/AggregateFunctions.hs
@@ -0,0 +1,182 @@
+module Database.DuckDB.FFI.AggregateFunctions (
+    c_duckdb_create_aggregate_function,
+    c_duckdb_destroy_aggregate_function,
+    c_duckdb_aggregate_function_set_name,
+    c_duckdb_aggregate_function_add_parameter,
+    c_duckdb_aggregate_function_set_return_type,
+    c_duckdb_aggregate_function_set_functions,
+    c_duckdb_aggregate_function_set_destructor,
+    c_duckdb_register_aggregate_function,
+    c_duckdb_aggregate_function_set_special_handling,
+    c_duckdb_aggregate_function_set_extra_info,
+    c_duckdb_aggregate_function_get_extra_info,
+    c_duckdb_aggregate_function_set_error,
+    c_duckdb_create_aggregate_function_set,
+    c_duckdb_destroy_aggregate_function_set,
+    c_duckdb_add_aggregate_function_to_set,
+    c_duckdb_register_aggregate_function_set,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new empty aggregate function.
+
+The return value should be destroyed with @duckdb_destroy_aggregate_function@.
+
+Returns The aggregate function object.
+-}
+foreign import ccall unsafe "duckdb_create_aggregate_function"
+    c_duckdb_create_aggregate_function :: IO DuckDBAggregateFunction
+
+-- | Destroys the given aggregate function object.
+foreign import ccall unsafe "duckdb_destroy_aggregate_function"
+    c_duckdb_destroy_aggregate_function :: Ptr DuckDBAggregateFunction -> IO ()
+
+{- | Sets the name of the given aggregate function.
+
+Parameters:
+* @aggregate_function@: The aggregate function
+* @name@: The name of the aggregate function
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_name"
+    c_duckdb_aggregate_function_set_name :: DuckDBAggregateFunction -> CString -> IO ()
+
+{- | Adds a parameter to the aggregate function.
+
+Parameters:
+* @aggregate_function@: The aggregate function.
+* @type@: The parameter type. Cannot contain INVALID.
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_add_parameter"
+    c_duckdb_aggregate_function_add_parameter :: DuckDBAggregateFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the return type of the aggregate function.
+
+Parameters:
+* @aggregate_function@: The aggregate function.
+* @type@: The return type. Cannot contain INVALID or ANY.
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_return_type"
+    c_duckdb_aggregate_function_set_return_type :: DuckDBAggregateFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the main functions of the aggregate function.
+
+Parameters:
+* @aggregate_function@: The aggregate function
+* @state_size@: state size
+* @state_init@: state init function
+* @update@: update states
+* @combine@: combine states
+* @finalize@: finalize states
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_functions"
+    c_duckdb_aggregate_function_set_functions :: DuckDBAggregateFunction -> DuckDBAggregateStateSizeFun -> DuckDBAggregateInitFun -> DuckDBAggregateUpdateFun -> DuckDBAggregateCombineFun -> DuckDBAggregateFinalizeFun -> IO ()
+
+{- | Sets the state destructor callback of the aggregate function (optional)
+
+Parameters:
+* @aggregate_function@: The aggregate function
+* @destroy@: state destroy callback
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_destructor"
+    c_duckdb_aggregate_function_set_destructor :: DuckDBAggregateFunction -> DuckDBAggregateDestroyFun -> IO ()
+
+{- | Register the aggregate function object within the given connection.
+
+The function requires at least a name, functions and a return type.
+
+If the function is incomplete or a function with this name already exists
+DuckDBError is returned.
+
+Parameters:
+* @con@: The connection to register it in.
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_aggregate_function"
+    c_duckdb_register_aggregate_function :: DuckDBConnection -> DuckDBAggregateFunction -> IO DuckDBState
+
+{- | Sets the NULL handling of the aggregate function to SPECIAL_HANDLING.
+
+Parameters:
+* @aggregate_function@: The aggregate function
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_special_handling"
+    c_duckdb_aggregate_function_set_special_handling :: DuckDBAggregateFunction -> IO ()
+
+{- | Assigns extra information to the scalar function that can be fetched during
+binding, etc.
+
+Parameters:
+* @aggregate_function@: The aggregate function
+* @extra_info@: The extra information
+* @destroy@: The callback that will be called to destroy the extra information
+  (if any)
+-}
+foreign import ccall unsafe "duckdb_aggregate_function_set_extra_info"
+    c_duckdb_aggregate_function_set_extra_info :: DuckDBAggregateFunction -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_aggregate_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object
+
+Returns The extra info
+-}
+foreign import ccall safe "duckdb_aggregate_function_get_extra_info"
+    c_duckdb_aggregate_function_get_extra_info :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Report that an error has occurred while executing the aggregate function.
+
+Parameters:
+* @info@: The info object
+* @error@: The error message
+-}
+foreign import ccall safe "duckdb_aggregate_function_set_error"
+    c_duckdb_aggregate_function_set_error :: DuckDBFunctionInfo -> CString -> IO ()
+
+{- | Creates a new empty aggregate function set.
+
+The return value should be destroyed with
+@duckdb_destroy_aggregate_function_set@.
+
+Returns The aggregate function set object.
+-}
+foreign import ccall unsafe "duckdb_create_aggregate_function_set"
+    c_duckdb_create_aggregate_function_set :: CString -> IO DuckDBAggregateFunctionSet
+
+-- | Destroys the given aggregate function set object.
+foreign import ccall unsafe "duckdb_destroy_aggregate_function_set"
+    c_duckdb_destroy_aggregate_function_set :: Ptr DuckDBAggregateFunctionSet -> IO ()
+
+{- | Adds the aggregate function as a new overload to the aggregate function set.
+
+Returns DuckDBError if the function could not be added, for example if the
+overload already exists.
+
+Parameters:
+* @set@: The aggregate function set
+* @function@: The function to add
+-}
+foreign import ccall unsafe "duckdb_add_aggregate_function_to_set"
+    c_duckdb_add_aggregate_function_to_set :: DuckDBAggregateFunctionSet -> DuckDBAggregateFunction -> IO DuckDBState
+
+{- | Register the aggregate function set within the given connection.
+
+The set requires at least a single valid overload.
+
+If the set is incomplete or a function with this name already exists
+DuckDBError is returned.
+
+Parameters:
+* @con@: The connection to register it in.
+* @set@: The function set to register
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_aggregate_function_set"
+    c_duckdb_register_aggregate_function_set :: DuckDBConnection -> DuckDBAggregateFunctionSet -> IO DuckDBState
diff --git a/src/Database/DuckDB/FFI/Appender.hs b/src/Database/DuckDB/FFI/Appender.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Appender.hs
@@ -0,0 +1,382 @@
+module Database.DuckDB.FFI.Appender (
+    c_duckdb_appender_create,
+    c_duckdb_appender_create_ext,
+    c_duckdb_appender_create_query,
+    c_duckdb_appender_column_count,
+    c_duckdb_appender_column_type,
+    c_duckdb_appender_error,
+    c_duckdb_appender_error_data,
+    c_duckdb_appender_flush,
+    c_duckdb_appender_close,
+    c_duckdb_appender_destroy,
+    c_duckdb_appender_add_column,
+    c_duckdb_appender_clear_columns,
+    c_duckdb_appender_begin_row,
+    c_duckdb_appender_end_row,
+    c_duckdb_append_default,
+    c_duckdb_append_default_to_chunk,
+    c_duckdb_append_bool,
+    c_duckdb_append_int8,
+    c_duckdb_append_int16,
+    c_duckdb_append_int32,
+    c_duckdb_append_int64,
+    c_duckdb_append_hugeint,
+    c_duckdb_append_uint8,
+    c_duckdb_append_uint16,
+    c_duckdb_append_uint32,
+    c_duckdb_append_uint64,
+    c_duckdb_append_uhugeint,
+    c_duckdb_append_float,
+    c_duckdb_append_double,
+    c_duckdb_append_date,
+    c_duckdb_append_time,
+    c_duckdb_append_timestamp,
+    c_duckdb_append_interval,
+    c_duckdb_append_varchar,
+    c_duckdb_append_varchar_length,
+    c_duckdb_append_blob,
+    c_duckdb_append_null,
+    c_duckdb_append_value,
+    c_duckdb_append_data_chunk,
+) where
+
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates an appender object.
+
+Note that the object must be destroyed with @duckdb_appender_destroy@.
+
+Parameters:
+* @connection@: The connection context to create the appender in.
+* @schema@: The schema of the table to append to, or @nullptr@ for the default
+  schema.
+* @table@: The table name to append to.
+* @out_appender@: The resulting appender object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_create"
+    c_duckdb_appender_create :: DuckDBConnection -> CString -> CString -> Ptr DuckDBAppender -> IO DuckDBState
+
+{- | Creates an appender object.
+
+Note that the object must be destroyed with @duckdb_appender_destroy@.
+
+Parameters:
+* @connection@: The connection context to create the appender in.
+* @catalog@: The catalog of the table to append to, or @nullptr@ for the
+  default catalog.
+* @schema@: The schema of the table to append to, or @nullptr@ for the default
+  schema.
+* @table@: The table name to append to.
+* @out_appender@: The resulting appender object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_create_ext"
+    c_duckdb_appender_create_ext :: DuckDBConnection -> CString -> CString -> CString -> Ptr DuckDBAppender -> IO DuckDBState
+
+{- | Creates an appender object that executes the given query with any data
+appended to it.
+
+Note that the object must be destroyed with @duckdb_appender_destroy@.
+
+Parameters:
+* @connection@: The connection context to create the appender in.
+* @query@: The query to execute, can be an INSERT, DELETE, UPDATE or MERGE
+  INTO statement.
+* @column_count@: The number of columns to append.
+* @types@: The types of the columns to append.
+* @table_name@: (optionally) the table name used to refer to the appended
+  data, defaults to "appended_data".
+* @column_names@: (optionally) the list of column names, defaults to "col1",
+  "col2", ...
+* @out_appender@: The resulting appender object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_create_query"
+    c_duckdb_appender_create_query :: DuckDBConnection -> CString -> DuckDBIdx -> Ptr DuckDBLogicalType -> CString -> Ptr CString -> Ptr DuckDBAppender -> IO DuckDBState
+
+{- | Returns the number of columns that belong to the appender. If there is no
+active column list, then this equals the table's physical columns.
+
+Parameters:
+* @appender@: The appender to get the column count from.
+
+Returns The number of columns in the data chunks.
+-}
+foreign import ccall safe "duckdb_appender_column_count"
+    c_duckdb_appender_column_count :: DuckDBAppender -> IO DuckDBIdx
+
+{- | Returns the type of the column at the specified index. This is either a type
+in the active column list, or the same type as a column in the receiving
+table.
+
+Note: The resulting type must be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @appender@: The appender to get the column type from.
+* @col_idx@: The index of the column to get the type of.
+
+Returns The @duckdb_logical_type@ of the column.
+-}
+foreign import ccall safe "duckdb_appender_column_type"
+    c_duckdb_appender_column_type :: DuckDBAppender -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release. Use duckdb_appender_error_data instead.
+
+Returns the error message associated with the appender. If the appender has no
+error message, this returns @nullptr@ instead.
+
+The error message should not be freed. It will be de-allocated when
+@duckdb_appender_destroy@ is called.
+
+Parameters:
+* @appender@: The appender to get the error from.
+
+Returns The error message, or @nullptr@ if there is none.
+-}
+foreign import ccall safe "duckdb_appender_error"
+    c_duckdb_appender_error :: DuckDBAppender -> IO CString
+
+{- | Returns the error data associated with the appender. Must be destroyed with
+duckdb_destroy_error_data.
+
+Parameters:
+* @appender@: The appender to get the error data from.
+
+Returns The error data.
+-}
+foreign import ccall safe "duckdb_appender_error_data"
+    c_duckdb_appender_error_data :: DuckDBAppender -> IO DuckDBErrorData
+
+{- | Flush the appender to the table, forcing the cache of the appender to be
+cleared. If flushing the data triggers a constraint violation or any other
+error, then all data is invalidated, and this function returns DuckDBError. It
+is not possible to append more values. Call duckdb_appender_error_data to
+obtain the error data followed by duckdb_appender_destroy to destroy the
+invalidated appender.
+
+Parameters:
+* @appender@: The appender to flush.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_flush"
+    c_duckdb_appender_flush :: DuckDBAppender -> IO DuckDBState
+
+{- | Closes the appender by flushing all intermediate states and closing it for
+further appends. If flushing the data triggers a constraint violation or any
+other error, then all data is invalidated, and this function returns
+DuckDBError. Call duckdb_appender_error_data to obtain the error data followed
+by duckdb_appender_destroy to destroy the invalidated appender.
+
+Parameters:
+* @appender@: The appender to flush and close.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_close"
+    c_duckdb_appender_close :: DuckDBAppender -> IO DuckDBState
+
+{- | Closes the appender by flushing all intermediate states to the table and
+destroying it. By destroying it, this function de-allocates all memory
+associated with the appender. If flushing the data triggers a constraint
+violation, then all data is invalidated, and this function returns
+DuckDBError. Due to the destruction of the appender, it is no longer possible
+to obtain the specific error message with duckdb_appender_error. Therefore,
+call duckdb_appender_close before destroying the appender, if you need
+insights into the specific error.
+
+Parameters:
+* @appender@: The appender to flush, close and destroy.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_destroy"
+    c_duckdb_appender_destroy :: Ptr DuckDBAppender -> IO DuckDBState
+
+{- | Appends a column to the active column list of the appender. Immediately
+flushes all previous data.
+
+The active column list specifies all columns that are expected when flushing
+the data. Any non-active columns are filled with their default values, or
+NULL.
+
+Parameters:
+* @appender@: The appender to add the column to.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_add_column"
+    c_duckdb_appender_add_column :: DuckDBAppender -> CString -> IO DuckDBState
+
+{- | Removes all columns from the active column list of the appender, resetting the
+appender to treat all columns as active. Immediately flushes all previous
+data.
+
+Parameters:
+* @appender@: The appender to clear the columns from.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_clear_columns"
+    c_duckdb_appender_clear_columns :: DuckDBAppender -> IO DuckDBState
+
+{- | A nop function, provided for backwards compatibility reasons. Does nothing.
+Only @duckdb_appender_end_row@ is required.
+-}
+foreign import ccall safe "duckdb_appender_begin_row"
+    c_duckdb_appender_begin_row :: DuckDBAppender -> IO DuckDBState
+
+{- | Finish the current row of appends. After end_row is called, the next row can
+be appended.
+
+Parameters:
+* @appender@: The appender.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_appender_end_row"
+    c_duckdb_appender_end_row :: DuckDBAppender -> IO DuckDBState
+
+{- | Append a DEFAULT value (NULL if DEFAULT not available for column) to the
+appender.
+-}
+foreign import ccall safe "duckdb_append_default"
+    c_duckdb_append_default :: DuckDBAppender -> IO DuckDBState
+
+{- | Append a DEFAULT value, at the specified row and column, (NULL if DEFAULT not
+available for column) to the chunk created from the specified appender. The
+default value of the column must be a constant value. Non-deterministic
+expressions like nextval(@seq@) or random() are not supported.
+
+Parameters:
+* @appender@: The appender to get the default value from.
+* @chunk@: The data chunk to append the default value to.
+* @col@: The chunk column index to append the default value to.
+* @row@: The chunk row index to append the default value to.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_append_default_to_chunk"
+    c_duckdb_append_default_to_chunk :: DuckDBAppender -> DuckDBDataChunk -> DuckDBIdx -> DuckDBIdx -> IO DuckDBState
+
+-- | Append a bool value to the appender.
+foreign import ccall safe "duckdb_append_bool"
+    c_duckdb_append_bool :: DuckDBAppender -> CBool -> IO DuckDBState
+
+-- | Append an int8_t value to the appender.
+foreign import ccall safe "duckdb_append_int8"
+    c_duckdb_append_int8 :: DuckDBAppender -> Int8 -> IO DuckDBState
+
+-- | Append an int16_t value to the appender.
+foreign import ccall safe "duckdb_append_int16"
+    c_duckdb_append_int16 :: DuckDBAppender -> Int16 -> IO DuckDBState
+
+-- | Append an int32_t value to the appender.
+foreign import ccall safe "duckdb_append_int32"
+    c_duckdb_append_int32 :: DuckDBAppender -> Int32 -> IO DuckDBState
+
+-- | Append an int64_t value to the appender.
+foreign import ccall safe "duckdb_append_int64"
+    c_duckdb_append_int64 :: DuckDBAppender -> Int64 -> IO DuckDBState
+
+{- | Append a duckdb_hugeint value to the appender.
+
+These bindings call the wrapper symbol @wrapped_duckdb_append_hugeint@
+but mirror the DuckDB C API semantics of @duckdb_append_hugeint@.
+-}
+foreign import ccall safe "wrapped_duckdb_append_hugeint"
+    c_duckdb_append_hugeint :: DuckDBAppender -> Ptr DuckDBHugeInt -> IO DuckDBState
+
+-- | Append a uint8_t value to the appender.
+foreign import ccall safe "duckdb_append_uint8"
+    c_duckdb_append_uint8 :: DuckDBAppender -> Word8 -> IO DuckDBState
+
+-- | Append a uint16_t value to the appender.
+foreign import ccall safe "duckdb_append_uint16"
+    c_duckdb_append_uint16 :: DuckDBAppender -> Word16 -> IO DuckDBState
+
+-- | Append a uint32_t value to the appender.
+foreign import ccall safe "duckdb_append_uint32"
+    c_duckdb_append_uint32 :: DuckDBAppender -> Word32 -> IO DuckDBState
+
+-- | Append a uint64_t value to the appender.
+foreign import ccall safe "duckdb_append_uint64"
+    c_duckdb_append_uint64 :: DuckDBAppender -> Word64 -> IO DuckDBState
+
+{- | Append a duckdb_uhugeint value to the appender.
+
+These bindings call the wrapper symbol @wrapped_duckdb_append_uhugeint@
+but mirror the DuckDB C API semantics of @duckdb_append_uhugeint@.
+-}
+foreign import ccall safe "wrapped_duckdb_append_uhugeint"
+    c_duckdb_append_uhugeint :: DuckDBAppender -> Ptr DuckDBUHugeInt -> IO DuckDBState
+
+-- | Append a float value to the appender.
+foreign import ccall safe "duckdb_append_float"
+    c_duckdb_append_float :: DuckDBAppender -> CFloat -> IO DuckDBState
+
+-- | Append a double value to the appender.
+foreign import ccall safe "duckdb_append_double"
+    c_duckdb_append_double :: DuckDBAppender -> CDouble -> IO DuckDBState
+
+-- | Append a duckdb_date value to the appender.
+foreign import ccall safe "duckdb_append_date"
+    c_duckdb_append_date :: DuckDBAppender -> DuckDBDate -> IO DuckDBState
+
+-- | Append a duckdb_time value to the appender.
+foreign import ccall safe "duckdb_append_time"
+    c_duckdb_append_time :: DuckDBAppender -> DuckDBTime -> IO DuckDBState
+
+-- | Append a duckdb_timestamp value to the appender.
+foreign import ccall safe "duckdb_append_timestamp"
+    c_duckdb_append_timestamp :: DuckDBAppender -> DuckDBTimestamp -> IO DuckDBState
+
+{- | Append a duckdb_interval value to the appender.
+
+These bindings call the wrapper symbol @wrapped_duckdb_append_interval@
+but mirror the DuckDB C API semantics of @duckdb_append_interval@.
+-}
+foreign import ccall safe "wrapped_duckdb_append_interval"
+    c_duckdb_append_interval :: DuckDBAppender -> Ptr DuckDBInterval -> IO DuckDBState
+
+-- | Append a varchar value to the appender.
+foreign import ccall safe "duckdb_append_varchar"
+    c_duckdb_append_varchar :: DuckDBAppender -> CString -> IO DuckDBState
+
+-- | Append a varchar value to the appender.
+foreign import ccall safe "duckdb_append_varchar_length"
+    c_duckdb_append_varchar_length :: DuckDBAppender -> CString -> DuckDBIdx -> IO DuckDBState
+
+-- | Append a blob value to the appender.
+foreign import ccall safe "duckdb_append_blob"
+    c_duckdb_append_blob :: DuckDBAppender -> Ptr () -> DuckDBIdx -> IO DuckDBState
+
+-- | Append a NULL value to the appender (of any type).
+foreign import ccall safe "duckdb_append_null"
+    c_duckdb_append_null :: DuckDBAppender -> IO DuckDBState
+
+-- | Append a duckdb_value to the appender.
+foreign import ccall safe "duckdb_append_value"
+    c_duckdb_append_value :: DuckDBAppender -> DuckDBValue -> IO DuckDBState
+
+{- | Appends a pre-filled data chunk to the specified appender. Attempts casting,
+if the data chunk types do not match the active appender types.
+
+Parameters:
+* @appender@: The appender to append to.
+* @chunk@: The data chunk to append.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_append_data_chunk"
+    c_duckdb_append_data_chunk :: DuckDBAppender -> DuckDBDataChunk -> IO DuckDBState
diff --git a/src/Database/DuckDB/FFI/Arrow.hs b/src/Database/DuckDB/FFI/Arrow.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Arrow.hs
@@ -0,0 +1,395 @@
+module Database.DuckDB.FFI.Arrow (
+    c_duckdb_to_arrow_schema,
+    c_duckdb_data_chunk_to_arrow,
+    c_duckdb_schema_from_arrow,
+    c_duckdb_data_chunk_from_arrow,
+    c_duckdb_destroy_arrow_converted_schema,
+    c_duckdb_query_arrow,
+    c_duckdb_query_arrow_schema,
+    c_duckdb_prepared_arrow_schema,
+    c_duckdb_result_arrow_array,
+    c_duckdb_query_arrow_array,
+    c_duckdb_arrow_column_count,
+    c_duckdb_arrow_row_count,
+    c_duckdb_arrow_rows_changed,
+    c_duckdb_query_arrow_error,
+    c_duckdb_destroy_arrow,
+    c_duckdb_destroy_arrow_stream,
+    c_duckdb_execute_prepared_arrow,
+    c_duckdb_arrow_scan,
+    c_duckdb_arrow_array_scan,
+    duckdbArrowSchemaInternal,
+    duckdbArrowSchemaClear,
+    duckdbArrowArrayInternal,
+    duckdbArrowArrayClear,
+    duckdbArrowStreamInternal,
+    duckdbArrowStreamClear,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+
+{- | Transforms a DuckDB Schema into an Arrow Schema
+
+Parameters:
+* @arrow_options@: The Arrow settings used to produce arrow.
+* @types@: The DuckDB logical types for each column in the schema.
+* @names@: The names for each column in the schema.
+* @column_count@: The number of columns that exist in the schema.
+* @out_schema@: The resulting arrow schema. Must be destroyed with
+  @out_schema->release(out_schema)@.
+
+Returns The error data. Must be destroyed with @duckdb_destroy_error_data@.
+-}
+foreign import ccall unsafe "duckdb_to_arrow_schema"
+    c_duckdb_to_arrow_schema :: DuckDBArrowOptions -> Ptr DuckDBLogicalType -> Ptr CString -> DuckDBIdx -> Ptr ArrowSchema -> IO DuckDBErrorData
+
+{- | Transforms a DuckDB data chunk into an Arrow array.
+
+Parameters:
+* @arrow_options@: The Arrow settings used to produce arrow.
+* @chunk@: The DuckDB data chunk to convert.
+* @out_arrow_array@: The output Arrow structure that will hold the converted
+  data. Must be released with @out_arrow_array->release(out_arrow_array)@
+
+Returns The error data. Must be destroyed with @duckdb_destroy_error_data@.
+-}
+foreign import ccall unsafe "duckdb_data_chunk_to_arrow"
+    c_duckdb_data_chunk_to_arrow :: DuckDBArrowOptions -> DuckDBDataChunk -> Ptr ArrowArray -> IO DuckDBErrorData
+
+{- | Transforms an Arrow Schema into a DuckDB Schema.
+
+Parameters:
+* @connection@: The connection to get the transformation settings from.
+* @schema@: The input Arrow schema. Must be released with
+  @schema->release(schema)@.
+* @out_types@: The Arrow converted schema with extra information about the
+  arrow types. Must be destroyed with @duckdb_destroy_arrow_converted_schema@.
+
+Returns The error data. Must be destroyed with @duckdb_destroy_error_data@.
+-}
+foreign import ccall unsafe "duckdb_schema_from_arrow"
+    c_duckdb_schema_from_arrow :: DuckDBConnection -> Ptr ArrowSchema -> Ptr DuckDBArrowConvertedSchema -> IO DuckDBErrorData
+
+{- | Transforms an Arrow array into a DuckDB data chunk. The data chunk will retain
+ownership of the underlying Arrow data.
+
+Parameters:
+* @connection@: The connection to get the transformation settings from.
+* @arrow_array@: The input Arrow array. Data ownership is passed on to
+  DuckDB's DataChunk, the underlying object does not need to be released and
+  won't have ownership of the data.
+* @converted_schema@: The Arrow converted schema with extra information about
+  the arrow types.
+* @out_chunk@: The resulting DuckDB data chunk. Must be destroyed by
+  duckdb_destroy_data_chunk.
+
+Returns The error data. Must be destroyed with @duckdb_destroy_error_data@.
+-}
+foreign import ccall unsafe "duckdb_data_chunk_from_arrow"
+    c_duckdb_data_chunk_from_arrow :: DuckDBConnection -> Ptr ArrowArray -> DuckDBArrowConvertedSchema -> Ptr DuckDBDataChunk -> IO DuckDBErrorData
+
+{- | Destroys the arrow converted schema and de-allocates all memory allocated for
+that arrow converted schema.
+
+Parameters:
+* @arrow_converted_schema@: The arrow converted schema to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_arrow_converted_schema"
+    c_duckdb_destroy_arrow_converted_schema :: Ptr DuckDBArrowConvertedSchema -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Executes a SQL query within a connection and stores the full (materialized)
+result in an arrow structure. If the query fails to execute, DuckDBError is
+returned and the error message can be retrieved by calling
+@duckdb_query_arrow_error@.
+
+Note that after running @duckdb_query_arrow@, @duckdb_destroy_arrow@ must be
+called on the result object even if the query fails, otherwise the error
+stored within the result will not be freed correctly.
+
+Parameters:
+* @connection@: The connection to perform the query in.
+* @query@: The SQL query to run.
+* @out_result@: The query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_query_arrow"
+    c_duckdb_query_arrow :: DuckDBConnection -> CString -> Ptr DuckDBArrow -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Fetch the internal arrow schema from the arrow result. Remember to call
+release on the respective ArrowSchema object.
+
+Parameters:
+* @result@: The result to fetch the schema from.
+* @out_schema@: The output schema.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_query_arrow_schema"
+    c_duckdb_query_arrow_schema :: DuckDBArrow -> Ptr DuckDBArrowSchema -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Fetch the internal arrow schema from the prepared statement. Remember to call
+release on the respective ArrowSchema object.
+
+Parameters:
+* @prepared@: The prepared statement to fetch the schema from.
+* @out_schema@: The output schema.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_prepared_arrow_schema"
+    c_duckdb_prepared_arrow_schema :: DuckDBPreparedStatement -> Ptr DuckDBArrowSchema -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Convert a data chunk into an arrow struct array. Remember to call release on
+the respective ArrowArray object.
+
+Parameters:
+* @result@: The result object the data chunk have been fetched from.
+* @chunk@: The data chunk to convert.
+* @out_array@: The output array.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_arrow_array@ but mirror the DuckDB C API semantics of
+@duckdb_result_arrow_array@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_arrow_array"
+    c_duckdb_result_arrow_array :: Ptr DuckDBResult -> DuckDBDataChunk -> Ptr DuckDBArrowArray -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Fetch an internal arrow struct array from the arrow result. Remember to call
+release on the respective ArrowArray object.
+
+This function can be called multiple time to get next chunks, which will free
+the previous out_array. So consume the out_array before calling this function
+again.
+
+Parameters:
+* @result@: The result to fetch the array from.
+* @out_array@: The output array.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_query_arrow_array"
+    c_duckdb_query_arrow_array :: DuckDBArrow -> Ptr DuckDBArrowArray -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the number of columns present in the arrow result object.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of columns present in the result object.
+-}
+foreign import ccall unsafe "duckdb_arrow_column_count"
+    c_duckdb_arrow_column_count :: DuckDBArrow -> IO DuckDBIdx
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the number of rows present in the arrow result object.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of rows present in the result object.
+-}
+foreign import ccall unsafe "duckdb_arrow_row_count"
+    c_duckdb_arrow_row_count :: DuckDBArrow -> IO DuckDBIdx
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the number of rows changed by the query stored in the arrow result.
+This is relevant only for INSERT/UPDATE/DELETE queries. For other queries the
+rows_changed will be 0.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of rows changed.
+-}
+foreign import ccall unsafe "duckdb_arrow_rows_changed"
+    c_duckdb_arrow_rows_changed :: DuckDBArrow -> IO DuckDBIdx
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the error message contained within the result. The error is only set
+if @duckdb_query_arrow@ returns @DuckDBError@.
+
+The error message should not be freed. It will be de-allocated when
+@duckdb_destroy_arrow@ is called.
+
+Parameters:
+* @result@: The result object to fetch the error from.
+
+Returns The error of the result.
+-}
+foreign import ccall unsafe "duckdb_query_arrow_error"
+    c_duckdb_query_arrow_error :: DuckDBArrow -> IO CString
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Closes the result and de-allocates all memory allocated for the arrow result.
+
+Parameters:
+* @result@: The result to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_arrow"
+    c_duckdb_destroy_arrow :: Ptr DuckDBArrow -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Releases the arrow array stream and de-allocates its memory.
+
+Parameters:
+* @stream_p@: The arrow array stream to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_arrow_stream"
+    c_duckdb_destroy_arrow_stream :: Ptr DuckDBArrowStream -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Executes the prepared statement with the given bound parameters, and returns
+an arrow query result. Note that after running
+@duckdb_execute_prepared_arrow@, @duckdb_destroy_arrow@ must be called on the
+result object.
+
+Parameters:
+* @prepared_statement@: The prepared statement to execute.
+* @out_result@: The query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_execute_prepared_arrow"
+    c_duckdb_execute_prepared_arrow :: DuckDBPreparedStatement -> Ptr DuckDBArrow -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Scans the Arrow stream and creates a view with the given name.
+
+Parameters:
+* @connection@: The connection on which to execute the scan.
+* @table_name@: Name of the temporary view to create.
+* @arrow@: Arrow stream wrapper.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_arrow_scan"
+    c_duckdb_arrow_scan :: DuckDBConnection -> CString -> DuckDBArrowStream -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Scans the Arrow array and creates a view with the given name. Note that after
+running @duckdb_arrow_array_scan@, @duckdb_destroy_arrow_stream@ must be
+called on the out stream.
+
+Parameters:
+* @connection@: The connection on which to execute the scan.
+* @table_name@: Name of the temporary view to create.
+* @arrow_schema@: Arrow schema wrapper.
+* @arrow_array@: Arrow array wrapper.
+* @out_stream@: Output array stream that wraps around the passed schema, for
+  releasing/deleting once done.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_arrow_array_scan"
+    c_duckdb_arrow_array_scan :: DuckDBConnection -> CString -> DuckDBArrowSchema -> DuckDBArrowArray -> Ptr DuckDBArrowStream -> IO DuckDBState
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_schema_internal_ptr"
+    c_duckdb_arrow_schema_internal_ptr :: DuckDBArrowSchema -> IO (Ptr ())
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_schema_clear_internal_ptr"
+    c_duckdb_arrow_schema_clear_internal_ptr :: DuckDBArrowSchema -> IO ()
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_array_internal_ptr"
+    c_duckdb_arrow_array_internal_ptr :: DuckDBArrowArray -> IO (Ptr ())
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_array_clear_internal_ptr"
+    c_duckdb_arrow_array_clear_internal_ptr :: DuckDBArrowArray -> IO ()
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_stream_internal_ptr"
+    c_duckdb_arrow_stream_internal_ptr :: DuckDBArrowStream -> IO (Ptr ())
+
+foreign import ccall unsafe "wrapped_duckdb_arrow_stream_clear_internal_ptr"
+    c_duckdb_arrow_stream_clear_internal_ptr :: DuckDBArrowStream -> IO ()
+
+{- | Read the @internal_ptr@ field of a deprecated Arrow schema wrapper.
+Returns 'Nothing' when the wrapper is null or DuckDB has already cleared the
+pointer.
+-}
+duckdbArrowSchemaInternal :: DuckDBArrowSchema -> IO (Maybe ArrowSchemaPtr)
+duckdbArrowSchemaInternal schema = do
+    raw <- c_duckdb_arrow_schema_internal_ptr schema
+    pure $
+        if raw == nullPtr
+            then Nothing
+            else Just (ArrowSchemaPtr (castPtr raw))
+
+{- | Clear the @internal_ptr@ field on a deprecated Arrow schema wrapper.
+Useful after copying the schema to application-managed storage.
+-}
+duckdbArrowSchemaClear :: DuckDBArrowSchema -> IO ()
+duckdbArrowSchemaClear = c_duckdb_arrow_schema_clear_internal_ptr
+
+{- | Read the @internal_ptr@ field of a deprecated Arrow array wrapper.
+Returns 'Nothing' when the wrapper is null or the internal pointer has been
+cleared by DuckDB.
+-}
+duckdbArrowArrayInternal :: DuckDBArrowArray -> IO (Maybe ArrowArrayPtr)
+duckdbArrowArrayInternal array = do
+    raw <- c_duckdb_arrow_array_internal_ptr array
+    pure $
+        if raw == nullPtr
+            then Nothing
+            else Just (ArrowArrayPtr (castPtr raw))
+
+{- | Clear the @internal_ptr@ field on a deprecated Arrow array wrapper.
+After this call DuckDB treats the wrapper as null.
+-}
+duckdbArrowArrayClear :: DuckDBArrowArray -> IO ()
+duckdbArrowArrayClear = c_duckdb_arrow_array_clear_internal_ptr
+
+{- | Read the @internal_ptr@ field of a deprecated Arrow stream wrapper.
+Returns 'Nothing' when the wrapper is null or the stream has already been
+released.
+-}
+duckdbArrowStreamInternal :: DuckDBArrowStream -> IO (Maybe ArrowStreamPtr)
+duckdbArrowStreamInternal stream = do
+    raw <- c_duckdb_arrow_stream_internal_ptr stream
+    pure $
+        if raw == nullPtr
+            then Nothing
+            else Just (ArrowStreamPtr raw)
+
+{- | Clear the @internal_ptr@ field on a deprecated Arrow stream wrapper.
+Use this after consuming the stream via @duckdb_arrow_scan@ to avoid stale
+pointers on the DuckDB side.
+-}
+duckdbArrowStreamClear :: DuckDBArrowStream -> IO ()
+duckdbArrowStreamClear = c_duckdb_arrow_stream_clear_internal_ptr
diff --git a/src/Database/DuckDB/FFI/BindValues.hs b/src/Database/DuckDB/FFI/BindValues.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/BindValues.hs
@@ -0,0 +1,160 @@
+module Database.DuckDB.FFI.BindValues (
+    c_duckdb_bind_value,
+    c_duckdb_bind_parameter_index,
+    c_duckdb_bind_boolean,
+    c_duckdb_bind_int8,
+    c_duckdb_bind_int16,
+    c_duckdb_bind_int32,
+    c_duckdb_bind_int64,
+    c_duckdb_bind_hugeint,
+    c_duckdb_bind_uhugeint,
+    c_duckdb_bind_decimal,
+    c_duckdb_bind_uint8,
+    c_duckdb_bind_uint16,
+    c_duckdb_bind_uint32,
+    c_duckdb_bind_uint64,
+    c_duckdb_bind_float,
+    c_duckdb_bind_double,
+    c_duckdb_bind_date,
+    c_duckdb_bind_time,
+    c_duckdb_bind_timestamp,
+    c_duckdb_bind_timestamp_tz,
+    c_duckdb_bind_interval,
+    c_duckdb_bind_varchar,
+    c_duckdb_bind_varchar_length,
+    c_duckdb_bind_blob,
+    c_duckdb_bind_null,
+) where
+
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+-- | Binds a value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_value"
+    c_duckdb_bind_value :: DuckDBPreparedStatement -> DuckDBIdx -> DuckDBValue -> IO DuckDBState
+
+{- | Retrieve the index of the parameter for the prepared statement, identified by
+name
+-}
+foreign import ccall unsafe "duckdb_bind_parameter_index"
+    c_duckdb_bind_parameter_index :: DuckDBPreparedStatement -> Ptr DuckDBIdx -> CString -> IO DuckDBState
+
+-- | Binds a bool value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_boolean"
+    c_duckdb_bind_boolean :: DuckDBPreparedStatement -> DuckDBIdx -> CBool -> IO DuckDBState
+
+-- | Binds an int8_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_int8"
+    c_duckdb_bind_int8 :: DuckDBPreparedStatement -> DuckDBIdx -> Int8 -> IO DuckDBState
+
+-- | Binds an int16_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_int16"
+    c_duckdb_bind_int16 :: DuckDBPreparedStatement -> DuckDBIdx -> Int16 -> IO DuckDBState
+
+-- | Binds an int32_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_int32"
+    c_duckdb_bind_int32 :: DuckDBPreparedStatement -> DuckDBIdx -> Int32 -> IO DuckDBState
+
+-- | Binds an int64_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_int64"
+    c_duckdb_bind_int64 :: DuckDBPreparedStatement -> DuckDBIdx -> Int64 -> IO DuckDBState
+
+{- | Binds a duckdb_hugeint value to the prepared statement at the specified index.
+
+These bindings call the wrapper symbol @wrapped_duckdb_bind_hugeint@
+but mirror the DuckDB C API semantics of @duckdb_bind_hugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_bind_hugeint"
+    c_duckdb_bind_hugeint :: DuckDBPreparedStatement -> DuckDBIdx -> Ptr DuckDBHugeInt -> IO DuckDBState
+
+{- | Binds a duckdb_uhugeint value to the prepared statement at the specified
+index.
+
+These bindings call the wrapper symbol @wrapped_duckdb_bind_uhugeint@
+but mirror the DuckDB C API semantics of @duckdb_bind_uhugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_bind_uhugeint"
+    c_duckdb_bind_uhugeint :: DuckDBPreparedStatement -> DuckDBIdx -> Ptr DuckDBUHugeInt -> IO DuckDBState
+
+{- | Binds a duckdb_decimal value to the prepared statement at the specified index.
+
+These bindings call the wrapper symbol @wrapped_duckdb_bind_decimal@
+but mirror the DuckDB C API semantics of @duckdb_bind_decimal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_bind_decimal"
+    c_duckdb_bind_decimal :: DuckDBPreparedStatement -> DuckDBIdx -> Ptr DuckDBDecimal -> IO DuckDBState
+
+-- | Binds a uint8_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_uint8"
+    c_duckdb_bind_uint8 :: DuckDBPreparedStatement -> DuckDBIdx -> Word8 -> IO DuckDBState
+
+-- | Binds a uint16_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_uint16"
+    c_duckdb_bind_uint16 :: DuckDBPreparedStatement -> DuckDBIdx -> Word16 -> IO DuckDBState
+
+-- | Binds a uint32_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_uint32"
+    c_duckdb_bind_uint32 :: DuckDBPreparedStatement -> DuckDBIdx -> Word32 -> IO DuckDBState
+
+-- | Binds a uint64_t value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_uint64"
+    c_duckdb_bind_uint64 :: DuckDBPreparedStatement -> DuckDBIdx -> Word64 -> IO DuckDBState
+
+-- | Binds a float value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_float"
+    c_duckdb_bind_float :: DuckDBPreparedStatement -> DuckDBIdx -> CFloat -> IO DuckDBState
+
+-- | Binds a double value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_double"
+    c_duckdb_bind_double :: DuckDBPreparedStatement -> DuckDBIdx -> CDouble -> IO DuckDBState
+
+-- | Binds a duckdb_date value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_date"
+    c_duckdb_bind_date :: DuckDBPreparedStatement -> DuckDBIdx -> DuckDBDate -> IO DuckDBState
+
+-- | Binds a duckdb_time value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_time"
+    c_duckdb_bind_time :: DuckDBPreparedStatement -> DuckDBIdx -> DuckDBTime -> IO DuckDBState
+
+{- | Binds a duckdb_timestamp value to the prepared statement at the specified
+index.
+-}
+foreign import ccall unsafe "duckdb_bind_timestamp"
+    c_duckdb_bind_timestamp :: DuckDBPreparedStatement -> DuckDBIdx -> DuckDBTimestamp -> IO DuckDBState
+
+{- | Binds a duckdb_timestamp value to the prepared statement at the specified
+index.
+-}
+foreign import ccall unsafe "duckdb_bind_timestamp_tz"
+    c_duckdb_bind_timestamp_tz :: DuckDBPreparedStatement -> DuckDBIdx -> DuckDBTimestamp -> IO DuckDBState
+
+{- | Binds a duckdb_interval value to the prepared statement at the specified
+index.
+
+These bindings call the wrapper symbol @wrapped_duckdb_bind_interval@
+but mirror the DuckDB C API semantics of @duckdb_bind_interval@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_bind_interval"
+    c_duckdb_bind_interval :: DuckDBPreparedStatement -> DuckDBIdx -> Ptr DuckDBInterval -> IO DuckDBState
+
+{- | Binds a null-terminated varchar value to the prepared statement at the
+specified index.
+-}
+foreign import ccall unsafe "duckdb_bind_varchar"
+    c_duckdb_bind_varchar :: DuckDBPreparedStatement -> DuckDBIdx -> CString -> IO DuckDBState
+
+-- | Binds a varchar value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_varchar_length"
+    c_duckdb_bind_varchar_length :: DuckDBPreparedStatement -> DuckDBIdx -> CString -> DuckDBIdx -> IO DuckDBState
+
+-- | Binds a blob value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_blob"
+    c_duckdb_bind_blob :: DuckDBPreparedStatement -> DuckDBIdx -> Ptr () -> DuckDBIdx -> IO DuckDBState
+
+-- | Binds a NULL value to the prepared statement at the specified index.
+foreign import ccall unsafe "duckdb_bind_null"
+    c_duckdb_bind_null :: DuckDBPreparedStatement -> DuckDBIdx -> IO DuckDBState
diff --git a/src/Database/DuckDB/FFI/CastFunctions.hs b/src/Database/DuckDB/FFI/CastFunctions.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/CastFunctions.hs
@@ -0,0 +1,136 @@
+module Database.DuckDB.FFI.CastFunctions (
+    c_duckdb_create_cast_function,
+    c_duckdb_cast_function_set_source_type,
+    c_duckdb_cast_function_set_target_type,
+    c_duckdb_cast_function_set_implicit_cast_cost,
+    c_duckdb_cast_function_set_function,
+    c_duckdb_cast_function_set_extra_info,
+    c_duckdb_cast_function_get_extra_info,
+    c_duckdb_cast_function_get_cast_mode,
+    c_duckdb_cast_function_set_error,
+    c_duckdb_cast_function_set_row_error,
+    c_duckdb_register_cast_function,
+    c_duckdb_destroy_cast_function,
+) where
+
+import Data.Int (Int64)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new cast function object.
+
+Returns The cast function object.
+-}
+foreign import ccall unsafe "duckdb_create_cast_function"
+    c_duckdb_create_cast_function :: IO DuckDBCastFunction
+
+{- | Sets the source type of the cast function.
+
+Parameters:
+* @cast_function@: The cast function object.
+* @source_type@: The source type to set.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_source_type"
+    c_duckdb_cast_function_set_source_type :: DuckDBCastFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the target type of the cast function.
+
+Parameters:
+* @cast_function@: The cast function object.
+* @target_type@: The target type to set.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_target_type"
+    c_duckdb_cast_function_set_target_type :: DuckDBCastFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the "cost" of implicitly casting the source type to the target type using
+this function.
+
+Parameters:
+* @cast_function@: The cast function object.
+* @cost@: The cost to set.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_implicit_cast_cost"
+    c_duckdb_cast_function_set_implicit_cast_cost :: DuckDBCastFunction -> Int64 -> IO ()
+
+{- | Sets the actual cast function to use.
+
+Parameters:
+* @cast_function@: The cast function object.
+* @function@: The function to set.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_function"
+    c_duckdb_cast_function_set_function :: DuckDBCastFunction -> DuckDBCastFunctionFun -> IO ()
+
+{- | Assigns extra information to the cast function that can be fetched during
+execution, etc.
+
+Parameters:
+* @extra_info@: The extra information
+* @destroy@: The callback that will be called to destroy the extra information
+  (if any)
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_extra_info"
+    c_duckdb_cast_function_set_extra_info :: DuckDBCastFunction -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_cast_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object.
+
+Returns The extra info.
+-}
+foreign import ccall unsafe "duckdb_cast_function_get_extra_info"
+    c_duckdb_cast_function_get_extra_info :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Get the cast execution mode from the given function info.
+
+Parameters:
+* @info@: The info object.
+
+Returns The cast mode.
+-}
+foreign import ccall unsafe "duckdb_cast_function_get_cast_mode"
+    c_duckdb_cast_function_get_cast_mode :: DuckDBFunctionInfo -> IO DuckDBCastMode
+
+{- | Report that an error has occurred while executing the cast function.
+
+Parameters:
+* @info@: The info object.
+* @error@: The error message.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_error"
+    c_duckdb_cast_function_set_error :: DuckDBFunctionInfo -> CString -> IO ()
+
+{- | Report that an error has occurred while executing the cast function, setting
+the corresponding output row to NULL.
+
+Parameters:
+* @info@: The info object.
+* @error@: The error message.
+* @row@: The index of the row within the output vector to set to NULL.
+* @output@: The output vector.
+-}
+foreign import ccall unsafe "duckdb_cast_function_set_row_error"
+    c_duckdb_cast_function_set_row_error :: DuckDBFunctionInfo -> CString -> DuckDBIdx -> DuckDBVector -> IO ()
+
+{- | Registers a cast function within the given connection.
+
+Parameters:
+* @con@: The connection to use.
+* @cast_function@: The cast function to register.
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_cast_function"
+    c_duckdb_register_cast_function :: DuckDBConnection -> DuckDBCastFunction -> IO DuckDBState
+
+{- | Destroys the cast function object.
+
+Parameters:
+* @cast_function@: The cast function object.
+-}
+foreign import ccall unsafe "duckdb_destroy_cast_function"
+    c_duckdb_destroy_cast_function :: Ptr DuckDBCastFunction -> IO ()
diff --git a/src/Database/DuckDB/FFI/Configuration.hs b/src/Database/DuckDB/FFI/Configuration.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Configuration.hs
@@ -0,0 +1,85 @@
+module Database.DuckDB.FFI.Configuration (
+    c_duckdb_create_config,
+    c_duckdb_config_count,
+    c_duckdb_get_config_flag,
+    c_duckdb_set_config,
+    c_duckdb_destroy_config,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..), CSize (..))
+import Foreign.Ptr (Ptr)
+
+{- | Initializes an empty configuration object that can be used to provide start-up
+options for the DuckDB instance through @duckdb_open_ext@. The duckdb_config
+must be destroyed using @duckdb_destroy_config@
+
+This will always succeed unless there is a malloc failure.
+
+Note that @duckdb_destroy_config@ should always be called on the resulting
+config, even if the function returns @DuckDBError@.
+
+Parameters:
+* @out_config@: The result configuration object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_create_config"
+    c_duckdb_create_config :: Ptr DuckDBConfig -> IO DuckDBState
+
+{- | This returns the total amount of configuration options available for usage
+with @duckdb_get_config_flag@.
+
+This should not be called in a loop as it internally loops over all the
+options.
+
+Returns The amount of config options available.
+-}
+foreign import ccall unsafe "duckdb_config_count"
+    c_duckdb_config_count :: IO CSize
+
+{- | Obtains a human-readable name and description of a specific configuration
+option. This can be used to e.g. display configuration options. This will
+succeed unless @index@ is out of range (i.e., @>= duckdb_config_count@).
+
+The result name or description MUST NOT be freed.
+
+Parameters:
+* @index@: The index of the configuration option (between 0 and
+  @duckdb_config_count@)
+* @out_name@: A name of the configuration flag.
+* @out_description@: A description of the configuration flag.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_get_config_flag"
+    c_duckdb_get_config_flag :: CSize -> Ptr CString -> Ptr CString -> IO DuckDBState
+
+{- | Sets the specified option for the specified configuration. The configuration
+option is indicated by name. To obtain a list of config options, see
+@duckdb_get_config_flag@.
+
+In the source code, configuration options are defined in @config.cpp@.
+
+This can fail if either the name is invalid, or if the value provided for the
+option is invalid.
+
+Parameters:
+* @config@: The configuration object to set the option on.
+* @name@: The name of the configuration flag to set.
+* @option@: The value to set the configuration flag to.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_set_config"
+    c_duckdb_set_config :: DuckDBConfig -> CString -> CString -> IO DuckDBState
+
+{- | Destroys the specified configuration object and de-allocates all memory
+allocated for the object.
+
+Parameters:
+* @config@: The configuration object to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_config"
+    c_duckdb_destroy_config :: Ptr DuckDBConfig -> IO ()
diff --git a/src/Database/DuckDB/FFI/DataChunk.hs b/src/Database/DuckDB/FFI/DataChunk.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/DataChunk.hs
@@ -0,0 +1,86 @@
+module Database.DuckDB.FFI.DataChunk (
+    c_duckdb_create_data_chunk,
+    c_duckdb_destroy_data_chunk,
+    c_duckdb_data_chunk_reset,
+    c_duckdb_data_chunk_get_column_count,
+    c_duckdb_data_chunk_get_vector,
+    c_duckdb_data_chunk_get_size,
+    c_duckdb_data_chunk_set_size,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.Ptr (Ptr)
+
+{- | Creates an empty data chunk with the specified column types. The result must
+be destroyed with @duckdb_destroy_data_chunk@.
+
+Parameters:
+* @types@: An array of column types. Column types can not contain ANY and
+  INVALID types.
+* @column_count@: The number of columns.
+
+Returns The data chunk.
+-}
+foreign import ccall unsafe "duckdb_create_data_chunk"
+    c_duckdb_create_data_chunk :: Ptr DuckDBLogicalType -> DuckDBIdx -> IO DuckDBDataChunk
+
+{- | Destroys the data chunk and de-allocates all memory allocated for that chunk.
+
+Parameters:
+* @chunk@: The data chunk to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_data_chunk"
+    c_duckdb_destroy_data_chunk :: Ptr DuckDBDataChunk -> IO ()
+
+{- | Resets a data chunk, clearing the validity masks and setting the cardinality
+of the data chunk to 0. After calling this method, you must call
+@duckdb_vector_get_validity@ and @duckdb_vector_get_data@ to obtain current
+data and validity pointers
+
+Parameters:
+* @chunk@: The data chunk to reset.
+-}
+foreign import ccall unsafe "duckdb_data_chunk_reset"
+    c_duckdb_data_chunk_reset :: DuckDBDataChunk -> IO ()
+
+{- | Retrieves the number of columns in a data chunk.
+
+Parameters:
+* @chunk@: The data chunk to get the data from
+
+Returns The number of columns in the data chunk
+-}
+foreign import ccall unsafe "duckdb_data_chunk_get_column_count"
+    c_duckdb_data_chunk_get_column_count :: DuckDBDataChunk -> IO DuckDBIdx
+
+{- | Retrieves the vector at the specified column index in the data chunk.
+
+The pointer to the vector is valid for as long as the chunk is alive. It does
+NOT need to be destroyed.
+
+Parameters:
+* @chunk@: The data chunk to get the data from
+
+Returns The vector
+-}
+foreign import ccall unsafe "duckdb_data_chunk_get_vector"
+    c_duckdb_data_chunk_get_vector :: DuckDBDataChunk -> DuckDBIdx -> IO DuckDBVector
+
+{- | Retrieves the current number of tuples in a data chunk.
+
+Parameters:
+* @chunk@: The data chunk to get the data from
+
+Returns The number of tuples in the data chunk
+-}
+foreign import ccall unsafe "duckdb_data_chunk_get_size"
+    c_duckdb_data_chunk_get_size :: DuckDBDataChunk -> IO DuckDBIdx
+
+{- | Sets the current number of tuples in a data chunk.
+
+Parameters:
+* @chunk@: The data chunk to set the size in
+* @size@: The number of tuples in the data chunk
+-}
+foreign import ccall unsafe "duckdb_data_chunk_set_size"
+    c_duckdb_data_chunk_set_size :: DuckDBDataChunk -> DuckDBIdx -> IO ()
diff --git a/src/Database/DuckDB/FFI/ErrorData.hs b/src/Database/DuckDB/FFI/ErrorData.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ErrorData.hs
@@ -0,0 +1,61 @@
+module Database.DuckDB.FFI.ErrorData (
+    c_duckdb_create_error_data,
+    c_duckdb_destroy_error_data,
+    c_duckdb_error_data_error_type,
+    c_duckdb_error_data_message,
+    c_duckdb_error_data_has_error,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates duckdb_error_data. Must be destroyed with @duckdb_destroy_error_data@.
+
+Parameters:
+* @type@: The error type.
+* @message@: The error message.
+
+Returns The error data.
+-}
+foreign import ccall unsafe "duckdb_create_error_data"
+    c_duckdb_create_error_data :: DuckDBErrorType -> CString -> IO DuckDBErrorData
+
+{- | Destroys the error data and deallocates its memory.
+
+Parameters:
+* @error_data@: The error data to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_error_data"
+    c_duckdb_destroy_error_data :: Ptr DuckDBErrorData -> IO ()
+
+{- | Returns the duckdb_error_type of the error data.
+
+Parameters:
+* @error_data@: The error data.
+
+Returns The error type.
+-}
+foreign import ccall unsafe "duckdb_error_data_error_type"
+    c_duckdb_error_data_error_type :: DuckDBErrorData -> IO DuckDBErrorType
+
+{- | Returns the error message of the error data. Must not be freed.
+
+Parameters:
+* @error_data@: The error data.
+
+Returns The error message.
+-}
+foreign import ccall unsafe "duckdb_error_data_message"
+    c_duckdb_error_data_message :: DuckDBErrorData -> IO CString
+
+{- | Returns whether the error data contains an error or not.
+
+Parameters:
+* @error_data@: The error data.
+
+Returns True, if the error data contains an exception, else false.
+-}
+foreign import ccall unsafe "duckdb_error_data_has_error"
+    c_duckdb_error_data_has_error :: DuckDBErrorData -> IO CBool
diff --git a/src/Database/DuckDB/FFI/ExecutePrepared.hs b/src/Database/DuckDB/FFI/ExecutePrepared.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ExecutePrepared.hs
@@ -0,0 +1,46 @@
+module Database.DuckDB.FFI.ExecutePrepared (
+    c_duckdb_execute_prepared,
+    c_duckdb_execute_prepared_streaming,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Executes the prepared statement with the given bound parameters, and returns a
+materialized query result.
+
+This method can be called multiple times for each prepared statement, and the
+parameters can be modified between calls to this function.
+
+Note that the result must be freed with @duckdb_destroy_result@.
+
+Parameters:
+* @prepared_statement@: The prepared statement to execute.
+* @out_result@: The query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_execute_prepared"
+    c_duckdb_execute_prepared :: DuckDBPreparedStatement -> Ptr DuckDBResult -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Executes the prepared statement with the given bound parameters, and returns
+an optionally-streaming query result. To determine if the resulting query was
+in fact streamed, use @duckdb_result_is_streaming@
+
+This method can be called multiple times for each prepared statement, and the
+parameters can be modified between calls to this function.
+
+Note that the result must be freed with @duckdb_destroy_result@.
+
+Parameters:
+* @prepared_statement@: The prepared statement to execute.
+* @out_result@: The query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_execute_prepared_streaming"
+    c_duckdb_execute_prepared_streaming :: DuckDBPreparedStatement -> Ptr DuckDBResult -> IO DuckDBState
diff --git a/src/Database/DuckDB/FFI/Expression.hs b/src/Database/DuckDB/FFI/Expression.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Expression.hs
@@ -0,0 +1,51 @@
+module Database.DuckDB.FFI.Expression (
+    c_duckdb_destroy_expression,
+    c_duckdb_expression_return_type,
+    c_duckdb_expression_is_foldable,
+    c_duckdb_expression_fold,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.Types (CBool (..))
+import Foreign.Ptr (Ptr)
+
+{- | Destroys the expression and de-allocates its memory.
+
+Parameters:
+* @expr@: A pointer to the expression.
+-}
+foreign import ccall unsafe "duckdb_destroy_expression"
+    c_duckdb_destroy_expression :: Ptr DuckDBExpression -> IO ()
+
+{- | Returns the return type of an expression.
+
+Parameters:
+* @expr@: The expression.
+
+Returns The return type. Must be destroyed with @duckdb_destroy_logical_type@.
+-}
+foreign import ccall unsafe "duckdb_expression_return_type"
+    c_duckdb_expression_return_type :: DuckDBExpression -> IO DuckDBLogicalType
+
+{- | Returns whether the expression is foldable into a value or not.
+
+Parameters:
+* @expr@: The expression.
+
+Returns True, if the expression is foldable, else false.
+-}
+foreign import ccall unsafe "duckdb_expression_is_foldable"
+    c_duckdb_expression_is_foldable :: DuckDBExpression -> IO CBool
+
+{- | Folds an expression creating a folded value.
+
+Parameters:
+* @context@: The client context.
+* @expr@: The expression. Must be foldable.
+* @out_value@: The folded value, if folding was successful. Must be destroyed
+  with @duckdb_destroy_value@.
+
+Returns The error data. Must be destroyed with @duckdb_destroy_error_data@.
+-}
+foreign import ccall safe "duckdb_expression_fold"
+    c_duckdb_expression_fold :: DuckDBClientContext -> DuckDBExpression -> Ptr DuckDBValue -> IO DuckDBErrorData
diff --git a/src/Database/DuckDB/FFI/ExtractStatements.hs b/src/Database/DuckDB/FFI/ExtractStatements.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ExtractStatements.hs
@@ -0,0 +1,67 @@
+module Database.DuckDB.FFI.ExtractStatements (
+    c_duckdb_extract_statements,
+    c_duckdb_prepare_extracted_statement,
+    c_duckdb_extract_statements_error,
+    c_duckdb_destroy_extracted,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Extract all statements from a query. Note that after calling
+@duckdb_extract_statements@, the extracted statements should always be
+destroyed using @duckdb_destroy_extracted@, even if no statements were
+extracted.
+
+If the extract fails, @duckdb_extract_statements_error@ can be called to
+obtain the reason why the extract failed.
+
+Parameters:
+* @connection@: The connection object
+* @query@: The SQL query to extract
+* @out_extracted_statements@: The resulting extracted statements object
+
+Returns The number of extracted statements or 0 on failure.
+-}
+foreign import ccall safe "duckdb_extract_statements"
+    c_duckdb_extract_statements :: DuckDBConnection -> CString -> Ptr DuckDBExtractedStatements -> IO DuckDBIdx
+
+{- | Prepare an extracted statement. Note that after calling
+@duckdb_prepare_extracted_statement@, the prepared statement should always be
+destroyed using @duckdb_destroy_prepare@, even if the prepare fails.
+
+If the prepare fails, @duckdb_prepare_error@ can be called to obtain the
+reason why the prepare failed.
+
+Parameters:
+* @connection@: The connection object
+* @extracted_statements@: The extracted statements object
+* @index@: The index of the extracted statement to prepare
+* @out_prepared_statement@: The resulting prepared statement object
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_prepare_extracted_statement"
+    c_duckdb_prepare_extracted_statement :: DuckDBConnection -> DuckDBExtractedStatements -> DuckDBIdx -> Ptr DuckDBPreparedStatement -> IO DuckDBState
+
+{- | Returns the error message contained within the extracted statements. The
+result of this function must not be freed. It will be cleaned up when
+@duckdb_destroy_extracted@ is called.
+
+Parameters:
+* @extracted_statements@: The extracted statements to fetch the error from.
+
+Returns The error of the extracted statements.
+-}
+foreign import ccall unsafe "duckdb_extract_statements_error"
+    c_duckdb_extract_statements_error :: DuckDBExtractedStatements -> IO CString
+
+{- | De-allocates all memory allocated for the extracted statements.
+
+Parameters:
+* @extracted_statements@: The extracted statements to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_extracted"
+    c_duckdb_destroy_extracted :: Ptr DuckDBExtractedStatements -> IO ()
diff --git a/src/Database/DuckDB/FFI/Helpers.hs b/src/Database/DuckDB/FFI/Helpers.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Helpers.hs
@@ -0,0 +1,351 @@
+module Database.DuckDB.FFI.Helpers (
+    c_duckdb_malloc,
+    c_duckdb_free,
+    c_duckdb_vector_size,
+    c_duckdb_string_is_inlined,
+    c_duckdb_string_t_length,
+    c_duckdb_string_t_data,
+    c_duckdb_from_date,
+    c_duckdb_to_date,
+    c_duckdb_is_finite_date,
+    c_duckdb_from_time,
+    c_duckdb_create_time_tz,
+    c_duckdb_from_time_tz,
+    c_duckdb_to_time,
+    c_duckdb_from_timestamp,
+    c_duckdb_to_timestamp,
+    c_duckdb_is_finite_timestamp,
+    c_duckdb_is_finite_timestamp_s,
+    c_duckdb_is_finite_timestamp_ms,
+    c_duckdb_is_finite_timestamp_ns,
+    c_duckdb_hugeint_to_double,
+    c_duckdb_double_to_hugeint,
+    c_duckdb_uhugeint_to_double,
+    c_duckdb_double_to_uhugeint,
+    c_duckdb_double_to_decimal,
+    c_duckdb_decimal_to_double,
+) where
+
+import Data.Int (Int32, Int64)
+import Data.Word (Word32, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CDouble (..), CSize (..))
+import Foreign.Ptr (Ptr)
+
+{- | Allocate @size@ bytes of memory using the duckdb internal malloc function. Any
+memory allocated in this manner should be freed using @duckdb_free@.
+
+Parameters:
+* @size@: The number of bytes to allocate.
+
+Returns A pointer to the allocated memory region.
+-}
+foreign import ccall unsafe "duckdb_malloc"
+    c_duckdb_malloc :: CSize -> IO (Ptr ())
+
+{- | Free a value returned from @duckdb_malloc@, @duckdb_value_varchar@,
+@duckdb_value_blob@, or @duckdb_value_string@.
+
+Parameters:
+* @ptr@: The memory region to de-allocate.
+-}
+foreign import ccall unsafe "duckdb_free"
+    c_duckdb_free :: Ptr () -> IO ()
+
+{- | The internal vector size used by DuckDB. This is the amount of tuples that
+will fit into a data chunk created by @duckdb_create_data_chunk@.
+
+Returns The vector size.
+-}
+foreign import ccall unsafe "duckdb_vector_size"
+    c_duckdb_vector_size :: IO DuckDBIdx
+
+{- | Whether or not the duckdb_string_t value is inlined. This means that the data
+of the string does not have a separate allocation.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_string_is_inlined@ but mirror the DuckDB C API semantics of
+@duckdb_string_is_inlined@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_string_is_inlined"
+    c_duckdb_string_is_inlined :: Ptr DuckDBStringT -> IO CBool
+
+{- | Get the string length of a string_t
+
+Parameters:
+* @string@: The string to get the length of.
+
+Returns The length.
+
+These bindings call the wrapper symbol @wrapped_duckdb_string_t_length@
+but mirror the DuckDB C API semantics of @duckdb_string_t_length@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_string_t_length"
+    c_duckdb_string_t_length :: Ptr DuckDBStringT -> IO Word32
+
+{- | Get a pointer to the string data of a string_t
+
+Parameters:
+* @string@: The string to get the pointer to.
+
+Returns The pointer.
+-}
+foreign import ccall unsafe "duckdb_string_t_data"
+    c_duckdb_string_t_data :: Ptr DuckDBStringT -> IO CString
+
+{- | Decompose a @duckdb_date@ object into year, month and date (stored as
+@duckdb_date_struct@).
+
+Parameters:
+* @date@: The date object, as obtained from a @DUCKDB_TYPE_DATE@ column.
+
+Returns The @duckdb_date_struct@ with the decomposed elements.
+
+These bindings call the wrapper symbol @wrapped_duckdb_from_date@ but
+mirror the DuckDB C API semantics of @duckdb_from_date@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_from_date"
+    c_duckdb_from_date :: DuckDBDate -> Ptr DuckDBDateStruct -> IO ()
+
+{- | Re-compose a @duckdb_date@ from year, month and date (@duckdb_date_struct@).
+
+Parameters:
+* @date@: The year, month and date stored in a @duckdb_date_struct@.
+
+Returns The @duckdb_date@ element.
+
+These bindings call the wrapper symbol @wrapped_duckdb_to_date@ but
+mirror the DuckDB C API semantics of @duckdb_to_date@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_to_date"
+    c_duckdb_to_date :: Ptr DuckDBDateStruct -> IO DuckDBDate
+
+{- | Test a @duckdb_date@ to see if it is a finite value.
+
+Parameters:
+* @date@: The date object, as obtained from a @DUCKDB_TYPE_DATE@ column.
+
+Returns True if the date is finite, false if it is ±infinity.
+-}
+foreign import ccall unsafe "duckdb_is_finite_date"
+    c_duckdb_is_finite_date :: DuckDBDate -> IO CBool
+
+{- | Decompose a @duckdb_time@ object into hour, minute, second and microsecond
+(stored as @duckdb_time_struct@).
+
+Parameters:
+* @time@: The time object, as obtained from a @DUCKDB_TYPE_TIME@ column.
+
+Returns The @duckdb_time_struct@ with the decomposed elements.
+
+These bindings call the wrapper symbol @wrapped_duckdb_from_time@ but
+mirror the DuckDB C API semantics of @duckdb_from_time@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_from_time"
+    c_duckdb_from_time :: DuckDBTime -> Ptr DuckDBTimeStruct -> IO ()
+
+{- | Create a @duckdb_time_tz@ object from micros and a timezone offset.
+
+Parameters:
+* @micros@: The microsecond component of the time.
+* @offset@: The timezone offset component of the time.
+
+Returns The @duckdb_time_tz@ element.
+-}
+foreign import ccall unsafe "duckdb_create_time_tz"
+    c_duckdb_create_time_tz :: Int64 -> Int32 -> IO DuckDBTimeTz
+
+{- | Decompose a TIME_TZ objects into micros and a timezone offset.
+
+Use @duckdb_from_time@ to further decompose the micros into hour, minute,
+second and microsecond.
+
+Parameters:
+* @micros@: The time object, as obtained from a @DUCKDB_TYPE_TIME_TZ@ column.
+
+These bindings call the wrapper symbol @wrapped_duckdb_from_time_tz@
+but mirror the DuckDB C API semantics of @duckdb_from_time_tz@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_from_time_tz"
+    c_duckdb_from_time_tz :: DuckDBTimeTz -> Ptr DuckDBTimeTzStruct -> IO ()
+
+{- | Re-compose a @duckdb_time@ from hour, minute, second and microsecond
+(@duckdb_time_struct@).
+
+Parameters:
+* @time@: The hour, minute, second and microsecond in a @duckdb_time_struct@.
+
+Returns The @duckdb_time@ element.
+
+These bindings call the wrapper symbol @wrapped_duckdb_to_time@ but
+mirror the DuckDB C API semantics of @duckdb_to_time@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_to_time"
+    c_duckdb_to_time :: Ptr DuckDBTimeStruct -> IO DuckDBTime
+
+{- | Decompose a @duckdb_timestamp@ object into a @duckdb_timestamp_struct@.
+
+Parameters:
+* @ts@: The ts object, as obtained from a @DUCKDB_TYPE_TIMESTAMP@ column.
+
+Returns The @duckdb_timestamp_struct@ with the decomposed elements.
+
+These bindings call the wrapper symbol @wrapped_duckdb_from_timestamp@
+but mirror the DuckDB C API semantics of @duckdb_from_timestamp@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_from_timestamp"
+    c_duckdb_from_timestamp :: DuckDBTimestamp -> Ptr DuckDBTimestampStruct -> IO ()
+
+{- | Re-compose a @duckdb_timestamp@ from a duckdb_timestamp_struct.
+
+Parameters:
+* @ts@: The de-composed elements in a @duckdb_timestamp_struct@.
+
+Returns The @duckdb_timestamp@ element.
+
+These bindings call the wrapper symbol @wrapped_duckdb_to_timestamp@
+but mirror the DuckDB C API semantics of @duckdb_to_timestamp@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_to_timestamp"
+    c_duckdb_to_timestamp :: Ptr DuckDBTimestampStruct -> IO DuckDBTimestamp
+
+{- | Test a @duckdb_timestamp@ to see if it is a finite value.
+
+Parameters:
+* @ts@: The duckdb_timestamp object, as obtained from a
+  @DUCKDB_TYPE_TIMESTAMP@ column.
+
+Returns True if the timestamp is finite, false if it is ±infinity.
+-}
+foreign import ccall unsafe "duckdb_is_finite_timestamp"
+    c_duckdb_is_finite_timestamp :: DuckDBTimestamp -> IO CBool
+
+{- | Test a @duckdb_timestamp_s@ to see if it is a finite value.
+
+Parameters:
+* @ts@: The duckdb_timestamp_s object, as obtained from a
+  @DUCKDB_TYPE_TIMESTAMP_S@ column.
+
+Returns True if the timestamp is finite, false if it is ±infinity.
+-}
+foreign import ccall unsafe "duckdb_is_finite_timestamp_s"
+    c_duckdb_is_finite_timestamp_s :: DuckDBTimestampS -> IO CBool
+
+{- | Test a @duckdb_timestamp_ms@ to see if it is a finite value.
+
+Parameters:
+* @ts@: The duckdb_timestamp_ms object, as obtained from a
+  @DUCKDB_TYPE_TIMESTAMP_MS@ column.
+
+Returns True if the timestamp is finite, false if it is ±infinity.
+-}
+foreign import ccall unsafe "duckdb_is_finite_timestamp_ms"
+    c_duckdb_is_finite_timestamp_ms :: DuckDBTimestampMs -> IO CBool
+
+{- | Test a @duckdb_timestamp_ns@ to see if it is a finite value.
+
+Parameters:
+* @ts@: The duckdb_timestamp_ns object, as obtained from a
+  @DUCKDB_TYPE_TIMESTAMP_NS@ column.
+
+Returns True if the timestamp is finite, false if it is ±infinity.
+-}
+foreign import ccall unsafe "duckdb_is_finite_timestamp_ns"
+    c_duckdb_is_finite_timestamp_ns :: DuckDBTimestampNs -> IO CBool
+
+{- | Converts a duckdb_hugeint object (as obtained from a @DUCKDB_TYPE_HUGEINT@
+column) into a double.
+
+Parameters:
+* @val@: The hugeint value.
+
+Returns The converted @double@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_hugeint_to_double@ but mirror the DuckDB C API semantics of
+@duckdb_hugeint_to_double@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_hugeint_to_double"
+    c_duckdb_hugeint_to_double :: Ptr DuckDBHugeInt -> IO CDouble
+
+{- | Converts a double value to a duckdb_hugeint object.
+
+If the conversion fails because the double value is too big the result will be
+0.
+
+Parameters:
+* @val@: The double value.
+
+Returns The converted @duckdb_hugeint@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_double_to_hugeint@ but mirror the DuckDB C API semantics of
+@duckdb_double_to_hugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_double_to_hugeint"
+    c_duckdb_double_to_hugeint :: CDouble -> Ptr DuckDBHugeInt -> IO ()
+
+{- | Converts a duckdb_uhugeint object (as obtained from a @DUCKDB_TYPE_UHUGEINT@
+column) into a double.
+
+Parameters:
+* @val@: The uhugeint value.
+
+Returns The converted @double@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_uhugeint_to_double@ but mirror the DuckDB C API semantics of
+@duckdb_uhugeint_to_double@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_uhugeint_to_double"
+    c_duckdb_uhugeint_to_double :: Ptr DuckDBUHugeInt -> IO CDouble
+
+{- | Converts a double value to a duckdb_uhugeint object.
+
+If the conversion fails because the double value is too big the result will be
+0.
+
+Parameters:
+* @val@: The double value.
+
+Returns The converted @duckdb_uhugeint@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_double_to_uhugeint@ but mirror the DuckDB C API semantics of
+@duckdb_double_to_uhugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_double_to_uhugeint"
+    c_duckdb_double_to_uhugeint :: CDouble -> Ptr DuckDBUHugeInt -> IO ()
+
+{- | Converts a double value to a duckdb_decimal object.
+
+If the conversion fails because the double value is too big, or the
+width/scale are invalid the result will be 0.
+
+Parameters:
+* @val@: The double value.
+
+Returns The converted @duckdb_decimal@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_double_to_decimal@ but mirror the DuckDB C API semantics of
+@duckdb_double_to_decimal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_double_to_decimal"
+    c_duckdb_double_to_decimal :: CDouble -> Word8 -> Word8 -> Ptr DuckDBDecimal -> IO ()
+
+{- | Converts a duckdb_decimal object (as obtained from a @DUCKDB_TYPE_DECIMAL@
+column) into a double.
+
+Parameters:
+* @val@: The decimal value.
+
+Returns The converted @double@ element.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_decimal_to_double@ but mirror the DuckDB C API semantics of
+@duckdb_decimal_to_double@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_decimal_to_double"
+    c_duckdb_decimal_to_double :: Ptr DuckDBDecimal -> IO CDouble
diff --git a/src/Database/DuckDB/FFI/LogicalTypes.hs b/src/Database/DuckDB/FFI/LogicalTypes.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/LogicalTypes.hs
@@ -0,0 +1,385 @@
+module Database.DuckDB.FFI.LogicalTypes (
+    c_duckdb_create_logical_type,
+    c_duckdb_logical_type_get_alias,
+    c_duckdb_logical_type_set_alias,
+    c_duckdb_create_list_type,
+    c_duckdb_create_array_type,
+    c_duckdb_create_map_type,
+    c_duckdb_create_union_type,
+    c_duckdb_create_struct_type,
+    c_duckdb_create_enum_type,
+    c_duckdb_create_decimal_type,
+    c_duckdb_get_type_id,
+    c_duckdb_decimal_width,
+    c_duckdb_decimal_scale,
+    c_duckdb_decimal_internal_type,
+    c_duckdb_enum_internal_type,
+    c_duckdb_enum_dictionary_size,
+    c_duckdb_enum_dictionary_value,
+    c_duckdb_list_type_child_type,
+    c_duckdb_array_type_child_type,
+    c_duckdb_array_type_array_size,
+    c_duckdb_map_type_key_type,
+    c_duckdb_map_type_value_type,
+    c_duckdb_struct_type_child_count,
+    c_duckdb_struct_type_child_name,
+    c_duckdb_struct_type_child_type,
+    c_duckdb_union_type_member_count,
+    c_duckdb_union_type_member_name,
+    c_duckdb_union_type_member_type,
+    c_duckdb_destroy_logical_type,
+    c_duckdb_register_logical_type,
+) where
+
+import Data.Word (Word32, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a @duckdb_logical_type@ from a primitive type. The resulting logical
+type must be destroyed with @duckdb_destroy_logical_type@.
+
+Returns an invalid logical type, if type is: @DUCKDB_TYPE_INVALID@,
+@DUCKDB_TYPE_DECIMAL@, @DUCKDB_TYPE_ENUM@, @DUCKDB_TYPE_LIST@,
+@DUCKDB_TYPE_STRUCT@, @DUCKDB_TYPE_MAP@, @DUCKDB_TYPE_ARRAY@, or
+@DUCKDB_TYPE_UNION@.
+
+Parameters:
+* @type@: The primitive type to create.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_logical_type"
+    c_duckdb_create_logical_type :: DuckDBType -> IO DuckDBLogicalType
+
+{- | Returns the alias of a duckdb_logical_type, if set, else @nullptr@. The result
+must be destroyed with @duckdb_free@.
+
+Parameters:
+* @type@: The logical type
+
+Returns The alias or @nullptr@
+-}
+foreign import ccall unsafe "duckdb_logical_type_get_alias"
+    c_duckdb_logical_type_get_alias :: DuckDBLogicalType -> IO CString
+
+{- | Sets the alias of a duckdb_logical_type.
+
+Parameters:
+* @type@: The logical type
+* @alias@: The alias to set
+-}
+foreign import ccall unsafe "duckdb_logical_type_set_alias"
+    c_duckdb_logical_type_set_alias :: DuckDBLogicalType -> CString -> IO ()
+
+{- | Creates a LIST type from its child type. The return type must be destroyed
+with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The child type of the list
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_list_type"
+    c_duckdb_create_list_type :: DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Creates an ARRAY type from its child type. The return type must be destroyed
+with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The child type of the array.
+* @array_size@: The number of elements in the array.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_array_type"
+    c_duckdb_create_array_type :: DuckDBLogicalType -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Creates a MAP type from its key type and value type. The return type must be
+destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @key_type@: The map's key type.
+* @value_type@: The map's value type.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_map_type"
+    c_duckdb_create_map_type :: DuckDBLogicalType -> DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Creates a UNION type from the passed arrays. The return type must be destroyed
+with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @member_types@: The array of union member types.
+* @member_names@: The union member names.
+* @member_count@: The number of union members.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_union_type"
+    c_duckdb_create_union_type :: Ptr DuckDBLogicalType -> Ptr CString -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Creates a STRUCT type based on the member types and names. The resulting type
+must be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @member_types@: The array of types of the struct members.
+* @member_names@: The array of names of the struct members.
+* @member_count@: The number of members of the struct.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_struct_type"
+    c_duckdb_create_struct_type :: Ptr DuckDBLogicalType -> Ptr CString -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Creates an ENUM type from the passed member name array. The resulting type
+should be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @member_names@: The array of names that the enum should consist of.
+* @member_count@: The number of elements that were specified in the array.
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_enum_type"
+    c_duckdb_create_enum_type :: Ptr CString -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Creates a DECIMAL type with the specified width and scale. The resulting type
+should be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @width@: The width of the decimal type
+* @scale@: The scale of the decimal type
+
+Returns The logical type.
+-}
+foreign import ccall unsafe "duckdb_create_decimal_type"
+    c_duckdb_create_decimal_type :: Word8 -> Word8 -> IO DuckDBLogicalType
+
+{- | Retrieves the enum @duckdb_type@ of a @duckdb_logical_type@.
+
+Parameters:
+* @type@: The logical type.
+
+Returns The @duckdb_type@ id.
+-}
+foreign import ccall unsafe "duckdb_get_type_id"
+    c_duckdb_get_type_id :: DuckDBLogicalType -> IO DuckDBType
+
+{- | Retrieves the width of a decimal type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The width of the decimal type
+-}
+foreign import ccall unsafe "duckdb_decimal_width"
+    c_duckdb_decimal_width :: DuckDBLogicalType -> IO Word8
+
+{- | Retrieves the scale of a decimal type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The scale of the decimal type
+-}
+foreign import ccall unsafe "duckdb_decimal_scale"
+    c_duckdb_decimal_scale :: DuckDBLogicalType -> IO Word8
+
+{- | Retrieves the internal storage type of a decimal type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The internal type of the decimal type
+-}
+foreign import ccall unsafe "duckdb_decimal_internal_type"
+    c_duckdb_decimal_internal_type :: DuckDBLogicalType -> IO DuckDBType
+
+{- | Retrieves the internal storage type of an enum type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The internal type of the enum type
+-}
+foreign import ccall unsafe "duckdb_enum_internal_type"
+    c_duckdb_enum_internal_type :: DuckDBLogicalType -> IO DuckDBType
+
+{- | Retrieves the dictionary size of the enum type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The dictionary size of the enum type
+-}
+foreign import ccall unsafe "duckdb_enum_dictionary_size"
+    c_duckdb_enum_dictionary_size :: DuckDBLogicalType -> IO Word32
+
+{- | Retrieves the dictionary value at the specified position from the enum.
+
+The result must be freed with @duckdb_free@.
+
+Parameters:
+* @type@: The logical type object
+* @index@: The index in the dictionary
+
+Returns The string value of the enum type. Must be freed with @duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_enum_dictionary_value"
+    c_duckdb_enum_dictionary_value :: DuckDBLogicalType -> DuckDBIdx -> IO CString
+
+{- | Retrieves the child type of the given LIST type. Also accepts MAP types. The
+result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type, either LIST or MAP.
+
+Returns The child type of the LIST or MAP type.
+-}
+foreign import ccall unsafe "duckdb_list_type_child_type"
+    c_duckdb_list_type_child_type :: DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Retrieves the child type of the given ARRAY type.
+
+The result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type. Must be ARRAY.
+
+Returns The child type of the ARRAY type.
+-}
+foreign import ccall unsafe "duckdb_array_type_child_type"
+    c_duckdb_array_type_child_type :: DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Retrieves the array size of the given array type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The fixed number of elements the values of this array type can store.
+-}
+foreign import ccall unsafe "duckdb_array_type_array_size"
+    c_duckdb_array_type_array_size :: DuckDBLogicalType -> IO DuckDBIdx
+
+{- | Retrieves the key type of the given map type.
+
+The result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The key type of the map type. Must be destroyed with
+@duckdb_destroy_logical_type@.
+-}
+foreign import ccall unsafe "duckdb_map_type_key_type"
+    c_duckdb_map_type_key_type :: DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Retrieves the value type of the given map type.
+
+The result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The value type of the map type. Must be destroyed with
+@duckdb_destroy_logical_type@.
+-}
+foreign import ccall unsafe "duckdb_map_type_value_type"
+    c_duckdb_map_type_value_type :: DuckDBLogicalType -> IO DuckDBLogicalType
+
+{- | Returns the number of children of a struct type.
+
+Parameters:
+* @type@: The logical type object
+
+Returns The number of children of a struct type.
+-}
+foreign import ccall unsafe "duckdb_struct_type_child_count"
+    c_duckdb_struct_type_child_count :: DuckDBLogicalType -> IO DuckDBIdx
+
+{- | Retrieves the name of the struct child.
+
+The result must be freed with @duckdb_free@.
+
+Parameters:
+* @type@: The logical type object
+* @index@: The child index
+
+Returns The name of the struct type. Must be freed with @duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_struct_type_child_name"
+    c_duckdb_struct_type_child_name :: DuckDBLogicalType -> DuckDBIdx -> IO CString
+
+{- | Retrieves the child type of the given struct type at the specified index.
+
+The result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type object
+* @index@: The child index
+
+Returns The child type of the struct type. Must be destroyed with
+@duckdb_destroy_logical_type@.
+-}
+foreign import ccall unsafe "duckdb_struct_type_child_type"
+    c_duckdb_struct_type_child_type :: DuckDBLogicalType -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Returns the number of members that the union type has.
+
+Parameters:
+* @type@: The logical type (union) object
+
+Returns The number of members of a union type.
+-}
+foreign import ccall unsafe "duckdb_union_type_member_count"
+    c_duckdb_union_type_member_count :: DuckDBLogicalType -> IO DuckDBIdx
+
+{- | Retrieves the name of the union member.
+
+The result must be freed with @duckdb_free@.
+
+Parameters:
+* @type@: The logical type object
+* @index@: The child index
+
+Returns The name of the union member. Must be freed with @duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_union_type_member_name"
+    c_duckdb_union_type_member_name :: DuckDBLogicalType -> DuckDBIdx -> IO CString
+
+{- | Retrieves the child type of the given union member at the specified index.
+
+The result must be freed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @type@: The logical type object
+* @index@: The child index
+
+Returns The child type of the union member. Must be destroyed with
+@duckdb_destroy_logical_type@.
+-}
+foreign import ccall unsafe "duckdb_union_type_member_type"
+    c_duckdb_union_type_member_type :: DuckDBLogicalType -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Destroys the logical type and de-allocates all memory allocated for that type.
+
+Parameters:
+* @type@: The logical type to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_logical_type"
+    c_duckdb_destroy_logical_type :: Ptr DuckDBLogicalType -> IO ()
+
+{- | Registers a custom type within the given connection. The type must have an
+alias
+
+Parameters:
+* @con@: The connection to use
+* @type@: The custom type to register
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_logical_type"
+    c_duckdb_register_logical_type :: DuckDBConnection -> DuckDBLogicalType -> DuckDBCreateTypeInfo -> IO DuckDBState
diff --git a/src/Database/DuckDB/FFI/OpenConnect.hs b/src/Database/DuckDB/FFI/OpenConnect.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/OpenConnect.hs
@@ -0,0 +1,215 @@
+module Database.DuckDB.FFI.OpenConnect (
+    c_duckdb_create_instance_cache,
+    c_duckdb_get_or_create_from_cache,
+    c_duckdb_destroy_instance_cache,
+    c_duckdb_open,
+    c_duckdb_open_ext,
+    c_duckdb_close,
+    c_duckdb_connect,
+    c_duckdb_interrupt,
+    c_duckdb_query_progress,
+    c_duckdb_disconnect,
+    c_duckdb_connection_get_client_context,
+    c_duckdb_connection_get_arrow_options,
+    c_duckdb_client_context_get_connection_id,
+    c_duckdb_destroy_client_context,
+    c_duckdb_destroy_arrow_options,
+    c_duckdb_library_version,
+    c_duckdb_get_table_names,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new database instance cache. The instance cache is necessary if a
+client/program (re)opens multiple databases to the same file within the same
+process. Must be destroyed with @duckdb_destroy_instance_cache@.
+
+Returns The database instance cache.
+-}
+foreign import ccall unsafe "duckdb_create_instance_cache"
+    c_duckdb_create_instance_cache :: IO DuckDBInstanceCache
+
+{- | Creates a new database instance in the instance cache, or retrieves an
+existing database instance. Must be closed with @duckdb_close@.
+
+Parameters:
+* @instance_cache@: The instance cache in which to create the database, or
+  from which to take the database.
+* @path@: Path to the database file on disk. Both @nullptr@ and @:memory:@
+  open or retrieve an in-memory database.
+* @out_database@: The resulting cached database.
+* @config@: (Optional) configuration used to create the database.
+* @out_error@: If set and the function returns @DuckDBError@, this contains
+  the error message. Note that the error message must be freed using
+  @duckdb_free@.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_get_or_create_from_cache"
+    c_duckdb_get_or_create_from_cache :: DuckDBInstanceCache -> CString -> Ptr DuckDBDatabase -> DuckDBConfig -> Ptr CString -> IO DuckDBState
+
+{- | Destroys an existing database instance cache and de-allocates its memory.
+
+Parameters:
+* @instance_cache@: The instance cache to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_instance_cache"
+    c_duckdb_destroy_instance_cache :: Ptr DuckDBInstanceCache -> IO ()
+
+{- | Creates a new database or opens an existing database file stored at the given
+path. If no path is given a new in-memory database is created instead. The
+database must be closed with @duckdb_close@.
+
+Parameters:
+* @path@: Path to the database file on disk. Both @nullptr@ and @:memory:@
+  open an in-memory database.
+* @out_database@: The result database object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_open"
+    c_duckdb_open :: CString -> Ptr DuckDBDatabase -> IO DuckDBState
+
+{- | Extended version of duckdb_open. Creates a new database or opens an existing
+database file stored at the given path. The database must be closed with
+@duckdb_close@.
+
+Parameters:
+* @path@: Path to the database file on disk. Both @nullptr@ and @:memory:@
+  open an in-memory database.
+* @out_database@: The result database object.
+* @config@: (Optional) configuration used to start up the database.
+* @out_error@: If set and the function returns @DuckDBError@, this contains
+  the error message. Note that the error message must be freed using
+  @duckdb_free@.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_open_ext"
+    c_duckdb_open_ext :: CString -> Ptr DuckDBDatabase -> DuckDBConfig -> Ptr CString -> IO DuckDBState
+
+{- | Closes the specified database and de-allocates all memory allocated for that
+database. This should be called after you are done with any database allocated
+through @duckdb_open@ or @duckdb_open_ext@. Note that failing to call
+@duckdb_close@ (in case of e.g., a program crash) will not cause data
+corruption. Still, it is recommended to always correctly close a database
+object after you are done with it.
+
+Parameters:
+* @database@: The database object to shut down.
+-}
+foreign import ccall safe "duckdb_close"
+    c_duckdb_close :: Ptr DuckDBDatabase -> IO ()
+
+{- | Opens a connection to a database. Connections are required to query the
+database, and store transactional state associated with the connection. The
+instantiated connection should be closed using @duckdb_disconnect@.
+
+Parameters:
+* @database@: The database file to connect to.
+* @out_connection@: The result connection object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_connect"
+    c_duckdb_connect :: DuckDBDatabase -> Ptr DuckDBConnection -> IO DuckDBState
+
+{- | Interrupt running query
+
+Parameters:
+* @connection@: The connection to interrupt
+-}
+foreign import ccall unsafe "duckdb_interrupt"
+    c_duckdb_interrupt :: DuckDBConnection -> IO ()
+
+{- | Get progress of the running query
+
+Parameters:
+* @connection@: The working connection
+
+Returns -1 if no progress or a percentage of the progress
+
+These bindings call the wrapper symbol @wrapped_duckdb_query_progress@
+but mirror the DuckDB C API semantics of @duckdb_query_progress@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_query_progress"
+    c_duckdb_query_progress :: DuckDBConnection -> Ptr DuckDBQueryProgress -> IO ()
+
+{- | Closes the specified connection and de-allocates all memory allocated for that
+connection.
+
+Parameters:
+* @connection@: The connection to close.
+-}
+foreign import ccall safe "duckdb_disconnect"
+    c_duckdb_disconnect :: Ptr DuckDBConnection -> IO ()
+
+{- | Retrieves the client context of the connection.
+
+Parameters:
+* @connection@: The connection.
+* @out_context@: The client context of the connection. Must be destroyed with
+  @duckdb_destroy_client_context@.
+-}
+foreign import ccall unsafe "duckdb_connection_get_client_context"
+    c_duckdb_connection_get_client_context :: DuckDBConnection -> Ptr DuckDBClientContext -> IO ()
+
+{- | Retrieves the arrow options of the connection.
+
+Parameters:
+* @connection@: The connection.
+-}
+foreign import ccall unsafe "duckdb_connection_get_arrow_options"
+    c_duckdb_connection_get_arrow_options :: DuckDBConnection -> Ptr DuckDBArrowOptions -> IO ()
+
+{- | Returns the connection id of the client context.
+
+Parameters:
+* @context@: The client context.
+
+Returns The connection id of the client context.
+-}
+foreign import ccall unsafe "duckdb_client_context_get_connection_id"
+    c_duckdb_client_context_get_connection_id :: DuckDBClientContext -> IO DuckDBIdx
+
+{- | Destroys the client context and deallocates its memory.
+
+Parameters:
+* @context@: The client context to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_client_context"
+    c_duckdb_destroy_client_context :: Ptr DuckDBClientContext -> IO ()
+
+{- | Destroys the arrow options and deallocates its memory.
+
+Parameters:
+* @arrow_options@: The arrow options to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_arrow_options"
+    c_duckdb_destroy_arrow_options :: Ptr DuckDBArrowOptions -> IO ()
+
+{- | Returns the version of the linked DuckDB, with a version postfix for dev
+versions
+
+Usually used for developing C extensions that must return this for a
+compatibility check.
+-}
+foreign import ccall unsafe "duckdb_library_version"
+    c_duckdb_library_version :: IO CString
+
+{- | Get the list of (fully qualified) table names of the query.
+
+Parameters:
+* @connection@: The connection for which to get the table names.
+* @query@: The query for which to get the table names.
+* @qualified@: Returns fully qualified table names (catalog.schema.table), if
+  set to true, else only the (not escaped) table names.
+
+Returns A duckdb_value of type VARCHAR[] containing the (fully qualified)
+table names of the query. Must be destroyed with duckdb_destroy_value.
+-}
+foreign import ccall safe "duckdb_get_table_names"
+    c_duckdb_get_table_names :: DuckDBConnection -> CString -> CBool -> IO DuckDBValue
diff --git a/src/Database/DuckDB/FFI/PendingResult.hs b/src/Database/DuckDB/FFI/PendingResult.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/PendingResult.hs
@@ -0,0 +1,143 @@
+module Database.DuckDB.FFI.PendingResult (
+    c_duckdb_pending_prepared,
+    c_duckdb_pending_prepared_streaming,
+    c_duckdb_destroy_pending,
+    c_duckdb_pending_error,
+    c_duckdb_pending_execute_task,
+    c_duckdb_pending_execute_check_state,
+    c_duckdb_execute_pending,
+    c_duckdb_pending_execution_is_finished,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Executes the prepared statement with the given bound parameters, and returns a
+pending result. The pending result represents an intermediate structure for a
+query that is not yet fully executed. The pending result can be used to
+incrementally execute a query, returning control to the client between tasks.
+
+Note that after calling @duckdb_pending_prepared@, the pending result should
+always be destroyed using @duckdb_destroy_pending@, even if this function
+returns DuckDBError.
+
+Parameters:
+* @prepared_statement@: The prepared statement to execute.
+* @out_result@: The pending query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_pending_prepared"
+    c_duckdb_pending_prepared :: DuckDBPreparedStatement -> Ptr DuckDBPendingResult -> IO DuckDBState
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Executes the prepared statement with the given bound parameters, and returns a
+pending result. This pending result will create a streaming duckdb_result when
+executed. The pending result represents an intermediate structure for a query
+that is not yet fully executed.
+
+Note that after calling @duckdb_pending_prepared_streaming@, the pending
+result should always be destroyed using @duckdb_destroy_pending@, even if this
+function returns DuckDBError.
+
+Parameters:
+* @prepared_statement@: The prepared statement to execute.
+* @out_result@: The pending query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_pending_prepared_streaming"
+    c_duckdb_pending_prepared_streaming :: DuckDBPreparedStatement -> Ptr DuckDBPendingResult -> IO DuckDBState
+
+{- | Closes the pending result and de-allocates all memory allocated for the
+result.
+
+Parameters:
+* @pending_result@: The pending result to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_pending"
+    c_duckdb_destroy_pending :: Ptr DuckDBPendingResult -> IO ()
+
+{- | Returns the error message contained within the pending result.
+
+The result of this function must not be freed. It will be cleaned up when
+@duckdb_destroy_pending@ is called.
+
+Parameters:
+* @pending_result@: The pending result to fetch the error from.
+
+Returns The error of the pending result.
+-}
+foreign import ccall unsafe "duckdb_pending_error"
+    c_duckdb_pending_error :: DuckDBPendingResult -> IO CString
+
+{- | Executes a single task within the query, returning whether or not the query is
+ready.
+
+If this returns DUCKDB_PENDING_RESULT_READY, the duckdb_execute_pending
+function can be called to obtain the result. If this returns
+DUCKDB_PENDING_RESULT_NOT_READY, the duckdb_pending_execute_task function
+should be called again. If this returns DUCKDB_PENDING_ERROR, an error
+occurred during execution.
+
+The error message can be obtained by calling duckdb_pending_error on the
+pending_result.
+
+Parameters:
+* @pending_result@: The pending result to execute a task within.
+
+Returns The state of the pending result after the execution.
+-}
+foreign import ccall safe "duckdb_pending_execute_task"
+    c_duckdb_pending_execute_task :: DuckDBPendingResult -> IO DuckDBPendingState
+
+{- | If this returns DUCKDB_PENDING_RESULT_READY, the duckdb_execute_pending
+function can be called to obtain the result. If this returns
+DUCKDB_PENDING_RESULT_NOT_READY, the duckdb_pending_execute_check_state
+function should be called again. If this returns DUCKDB_PENDING_ERROR, an
+error occurred during execution.
+
+The error message can be obtained by calling duckdb_pending_error on the
+pending_result.
+
+Parameters:
+* @pending_result@: The pending result.
+
+Returns The state of the pending result.
+-}
+foreign import ccall unsafe "duckdb_pending_execute_check_state"
+    c_duckdb_pending_execute_check_state :: DuckDBPendingResult -> IO DuckDBPendingState
+
+{- | Fully execute a pending query result, returning the final query result.
+
+If duckdb_pending_execute_task has been called until
+DUCKDB_PENDING_RESULT_READY was returned, this will return fast. Otherwise,
+all remaining tasks must be executed first.
+
+Note that the result must be freed with @duckdb_destroy_result@.
+
+Parameters:
+* @pending_result@: The pending result to execute.
+* @out_result@: The result object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_execute_pending"
+    c_duckdb_execute_pending :: DuckDBPendingResult -> Ptr DuckDBResult -> IO DuckDBState
+
+{- | Returns whether a duckdb_pending_state is finished executing. For example if
+@pending_state@ is DUCKDB_PENDING_RESULT_READY, this function will return
+true.
+
+Parameters:
+* @pending_state@: The pending state on which to decide whether to finish
+  execution.
+
+Returns Boolean indicating pending execution should be considered finished.
+-}
+foreign import ccall unsafe "duckdb_pending_execution_is_finished"
+    c_duckdb_pending_execution_is_finished :: DuckDBPendingState -> IO CBool
diff --git a/src/Database/DuckDB/FFI/PreparedStatements.hs b/src/Database/DuckDB/FFI/PreparedStatements.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/PreparedStatements.hs
@@ -0,0 +1,184 @@
+module Database.DuckDB.FFI.PreparedStatements (
+    c_duckdb_prepare,
+    c_duckdb_destroy_prepare,
+    c_duckdb_prepare_error,
+    c_duckdb_nparams,
+    c_duckdb_parameter_name,
+    c_duckdb_param_type,
+    c_duckdb_param_logical_type,
+    c_duckdb_clear_bindings,
+    c_duckdb_prepared_statement_type,
+    c_duckdb_prepared_statement_column_count,
+    c_duckdb_prepared_statement_column_name,
+    c_duckdb_prepared_statement_column_logical_type,
+    c_duckdb_prepared_statement_column_type,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Create a prepared statement object from a query.
+
+Note that after calling @duckdb_prepare@, the prepared statement should always
+be destroyed using @duckdb_destroy_prepare@, even if the prepare fails.
+
+If the prepare fails, @duckdb_prepare_error@ can be called to obtain the
+reason why the prepare failed.
+
+Parameters:
+* @connection@: The connection object
+* @query@: The SQL query to prepare
+* @out_prepared_statement@: The resulting prepared statement object
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_prepare"
+    c_duckdb_prepare :: DuckDBConnection -> CString -> Ptr DuckDBPreparedStatement -> IO DuckDBState
+
+{- | Closes the prepared statement and de-allocates all memory allocated for the
+statement.
+
+Parameters:
+* @prepared_statement@: The prepared statement to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_prepare"
+    c_duckdb_destroy_prepare :: Ptr DuckDBPreparedStatement -> IO ()
+
+{- | Returns the error message associated with the given prepared statement. If the
+prepared statement has no error message, this returns @nullptr@ instead.
+
+The error message should not be freed. It will be de-allocated when
+@duckdb_destroy_prepare@ is called.
+
+Parameters:
+* @prepared_statement@: The prepared statement to obtain the error from.
+
+Returns The error message, or @nullptr@ if there is none.
+-}
+foreign import ccall unsafe "duckdb_prepare_error"
+    c_duckdb_prepare_error :: DuckDBPreparedStatement -> IO CString
+
+{- | Returns the number of parameters that can be provided to the given prepared
+statement.
+
+Returns 0 if the query was not successfully prepared.
+
+Parameters:
+* @prepared_statement@: The prepared statement to obtain the number of
+  parameters for.
+-}
+foreign import ccall unsafe "duckdb_nparams"
+    c_duckdb_nparams :: DuckDBPreparedStatement -> IO DuckDBIdx
+
+{- | Returns the name used to identify the parameter The returned string should be
+freed using @duckdb_free@.
+
+Returns NULL if the index is out of range for the provided prepared statement.
+
+Parameters:
+* @prepared_statement@: The prepared statement for which to get the parameter
+  name from.
+-}
+foreign import ccall unsafe "duckdb_parameter_name"
+    c_duckdb_parameter_name :: DuckDBPreparedStatement -> DuckDBIdx -> IO CString
+
+{- | Returns the parameter type for the parameter at the given index.
+
+Returns @DUCKDB_TYPE_INVALID@ if the parameter index is out of range or the
+statement was not successfully prepared.
+
+Parameters:
+* @prepared_statement@: The prepared statement.
+* @param_idx@: The parameter index.
+
+Returns The parameter type
+-}
+foreign import ccall unsafe "duckdb_param_type"
+    c_duckdb_param_type :: DuckDBPreparedStatement -> DuckDBIdx -> IO DuckDBType
+
+{- | Returns the logical type for the parameter at the given index.
+
+Returns @nullptr@ if the parameter index is out of range or the statement was
+not successfully prepared.
+
+The return type of this call should be destroyed with
+@duckdb_destroy_logical_type@.
+
+Parameters:
+* @prepared_statement@: The prepared statement.
+* @param_idx@: The parameter index.
+
+Returns The logical type of the parameter
+-}
+foreign import ccall unsafe "duckdb_param_logical_type"
+    c_duckdb_param_logical_type :: DuckDBPreparedStatement -> DuckDBIdx -> IO DuckDBLogicalType
+
+-- | Clear the params bind to the prepared statement.
+foreign import ccall unsafe "duckdb_clear_bindings"
+    c_duckdb_clear_bindings :: DuckDBPreparedStatement -> IO DuckDBState
+
+{- | Returns the statement type of the statement to be executed
+
+Parameters:
+* @statement@: The prepared statement.
+
+Returns duckdb_statement_type value or DUCKDB_STATEMENT_TYPE_INVALID
+-}
+foreign import ccall unsafe "duckdb_prepared_statement_type"
+    c_duckdb_prepared_statement_type :: DuckDBPreparedStatement -> IO DuckDBStatementType
+
+{- | Returns the number of columns present in a the result of the prepared
+statement. If any of the column types are invalid, the result will be 1.
+
+Parameters:
+* @prepared_statement@: The prepared statement.
+
+Returns The number of columns present in the result of the prepared statement.
+-}
+foreign import ccall unsafe "duckdb_prepared_statement_column_count"
+    c_duckdb_prepared_statement_column_count :: DuckDBPreparedStatement -> IO DuckDBIdx
+
+{- | Returns the name of the specified column of the result of the
+prepared_statement. The returned string should be freed using @duckdb_free@.
+
+Returns @nullptr@ if the column is out of range.
+
+Parameters:
+* @prepared_statement@: The prepared statement.
+* @col_idx@: The column index.
+
+Returns The column name of the specified column.
+-}
+foreign import ccall unsafe "duckdb_prepared_statement_column_name"
+    c_duckdb_prepared_statement_column_name :: DuckDBPreparedStatement -> DuckDBIdx -> IO CString
+
+{- | Returns the column type of the specified column of the result of the
+prepared_statement.
+
+Returns @DUCKDB_TYPE_INVALID@ if the column is out of range. The return type
+of this call should be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @prepared_statement@: The prepared statement to fetch the column type from.
+* @col_idx@: The column index.
+
+Returns The logical type of the specified column.
+-}
+foreign import ccall unsafe "duckdb_prepared_statement_column_logical_type"
+    c_duckdb_prepared_statement_column_logical_type :: DuckDBPreparedStatement -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Returns the column type of the specified column of the result of the
+prepared_statement.
+
+Returns @DUCKDB_TYPE_INVALID@ if the column is out of range.
+
+Parameters:
+* @prepared_statement@: The prepared statement to fetch the column type from.
+* @col_idx@: The column index.
+
+Returns The type of the specified column.
+-}
+foreign import ccall unsafe "duckdb_prepared_statement_column_type"
+    c_duckdb_prepared_statement_column_type :: DuckDBPreparedStatement -> DuckDBIdx -> IO DuckDBType
diff --git a/src/Database/DuckDB/FFI/ProfilingInfo.hs b/src/Database/DuckDB/FFI/ProfilingInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ProfilingInfo.hs
@@ -0,0 +1,67 @@
+module Database.DuckDB.FFI.ProfilingInfo (
+    c_duckdb_get_profiling_info,
+    c_duckdb_profiling_info_get_value,
+    c_duckdb_profiling_info_get_metrics,
+    c_duckdb_profiling_info_get_child_count,
+    c_duckdb_profiling_info_get_child,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+
+{- | Returns the root node of the profiling information. Returns nullptr, if
+profiling is not enabled.
+
+Parameters:
+* @connection@: A connection object.
+
+Returns A profiling information object.
+-}
+foreign import ccall unsafe "duckdb_get_profiling_info"
+    c_duckdb_get_profiling_info :: DuckDBConnection -> IO DuckDBProfilingInfo
+
+{- | Returns the value of the metric of the current profiling info node. Returns
+nullptr, if the metric does not exist or is not enabled. Currently, the value
+holds a string, and you can retrieve the string by calling the corresponding
+function: char *duckdb_get_varchar(duckdb_value value).
+
+Parameters:
+* @info@: A profiling information object.
+* @key@: The name of the requested metric.
+
+Returns The value of the metric. Must be freed with @duckdb_destroy_value@
+-}
+foreign import ccall unsafe "duckdb_profiling_info_get_value"
+    c_duckdb_profiling_info_get_value :: DuckDBProfilingInfo -> CString -> IO DuckDBValue
+
+{- | Returns the key-value metric map of this profiling node as a MAP duckdb_value.
+The individual elements are accessible via the duckdb_value MAP functions.
+
+Parameters:
+* @info@: A profiling information object.
+
+Returns The key-value metric map as a MAP duckdb_value.
+-}
+foreign import ccall unsafe "duckdb_profiling_info_get_metrics"
+    c_duckdb_profiling_info_get_metrics :: DuckDBProfilingInfo -> IO DuckDBValue
+
+{- | Returns the number of children in the current profiling info node.
+
+Parameters:
+* @info@: A profiling information object.
+
+Returns The number of children in the current node.
+-}
+foreign import ccall unsafe "duckdb_profiling_info_get_child_count"
+    c_duckdb_profiling_info_get_child_count :: DuckDBProfilingInfo -> IO DuckDBIdx
+
+{- | Returns the child node at the specified index.
+
+Parameters:
+* @info@: A profiling information object.
+* @index@: The index of the child node.
+
+Returns The child node at the specified index.
+-}
+foreign import ccall unsafe "duckdb_profiling_info_get_child"
+    c_duckdb_profiling_info_get_child :: DuckDBProfilingInfo -> DuckDBIdx -> IO DuckDBProfilingInfo
diff --git a/src/Database/DuckDB/FFI/QueryExecution.hs b/src/Database/DuckDB/FFI/QueryExecution.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/QueryExecution.hs
@@ -0,0 +1,229 @@
+module Database.DuckDB.FFI.QueryExecution (
+    c_duckdb_query,
+    c_duckdb_destroy_result,
+    c_duckdb_column_name,
+    c_duckdb_column_type,
+    c_duckdb_result_statement_type,
+    c_duckdb_column_logical_type,
+    c_duckdb_result_get_arrow_options,
+    c_duckdb_column_count,
+    c_duckdb_row_count,
+    c_duckdb_rows_changed,
+    c_duckdb_column_data,
+    c_duckdb_nullmask_data,
+    c_duckdb_result_error,
+    c_duckdb_result_error_type,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Executes a SQL query within a connection and stores the full (materialized)
+result in the out_result pointer. If the query fails to execute, DuckDBError
+is returned and the error message can be retrieved by calling
+@duckdb_result_error@.
+
+Note that after running @duckdb_query@, @duckdb_destroy_result@ must be called
+on the result object even if the query fails, otherwise the error stored
+within the result will not be freed correctly.
+
+Parameters:
+* @connection@: The connection to perform the query in.
+* @query@: The SQL query to run.
+* @out_result@: The query result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_query"
+    c_duckdb_query :: DuckDBConnection -> CString -> Ptr DuckDBResult -> IO DuckDBState
+
+{- | Closes the result and de-allocates all memory allocated for that result.
+
+Parameters:
+* @result@: The result to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_result"
+    c_duckdb_destroy_result :: Ptr DuckDBResult -> IO ()
+
+{- | Returns the column name of the specified column. The result should not need to
+be freed; the column names will automatically be destroyed when the result is
+destroyed.
+
+Returns @NULL@ if the column is out of range.
+
+Parameters:
+* @result@: The result object to fetch the column name from.
+* @col@: The column index.
+
+Returns The column name of the specified column.
+-}
+foreign import ccall unsafe "duckdb_column_name"
+    c_duckdb_column_name :: Ptr DuckDBResult -> DuckDBIdx -> IO CString
+
+{- | Returns the column type of the specified column.
+
+Returns @DUCKDB_TYPE_INVALID@ if the column is out of range.
+
+Parameters:
+* @result@: The result object to fetch the column type from.
+* @col@: The column index.
+
+Returns The column type of the specified column.
+-}
+foreign import ccall unsafe "duckdb_column_type"
+    c_duckdb_column_type :: Ptr DuckDBResult -> DuckDBIdx -> IO DuckDBType
+
+{- | Returns the statement type of the statement that was executed
+
+Parameters:
+* @result@: The result object to fetch the statement type from.
+
+Returns duckdb_statement_type value or DUCKDB_STATEMENT_TYPE_INVALID
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_statement_type@ but mirror the DuckDB C API semantics of
+@duckdb_result_statement_type@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_statement_type"
+    c_duckdb_result_statement_type :: Ptr DuckDBResult -> IO DuckDBStatementType
+
+{- | Returns the logical column type of the specified column.
+
+The return type of this call should be destroyed with
+@duckdb_destroy_logical_type@.
+
+Returns @NULL@ if the column is out of range.
+
+Parameters:
+* @result@: The result object to fetch the column type from.
+* @col@: The column index.
+
+Returns The logical column type of the specified column.
+-}
+foreign import ccall unsafe "duckdb_column_logical_type"
+    c_duckdb_column_logical_type :: Ptr DuckDBResult -> DuckDBIdx -> IO DuckDBLogicalType
+
+{- | Returns the arrow options associated with the given result. These options are
+definitions of how the arrow arrays/schema should be produced.
+
+Parameters:
+* @result@: The result object to fetch arrow options from.
+
+Returns The arrow options associated with the given result. This must be
+destroyed with @duckdb_destroy_arrow_options@.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_get_arrow_options@ but mirror the DuckDB C API semantics
+of @duckdb_result_get_arrow_options@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_get_arrow_options"
+    c_duckdb_result_get_arrow_options :: Ptr DuckDBResult -> IO DuckDBArrowOptions
+
+{- | Returns the number of columns present in a the result object.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of columns present in the result object.
+-}
+foreign import ccall unsafe "duckdb_column_count"
+    c_duckdb_column_count :: Ptr DuckDBResult -> IO DuckDBIdx
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the number of rows present in the result object.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of rows present in the result object.
+-}
+foreign import ccall unsafe "duckdb_row_count"
+    c_duckdb_row_count :: Ptr DuckDBResult -> IO DuckDBIdx
+
+{- | Returns the number of rows changed by the query stored in the result. This is
+relevant only for INSERT/UPDATE/DELETE queries. For other queries the
+rows_changed will be 0.
+
+Parameters:
+* @result@: The result object.
+
+Returns The number of rows changed.
+-}
+foreign import ccall unsafe "duckdb_rows_changed"
+    c_duckdb_rows_changed :: Ptr DuckDBResult -> IO DuckDBIdx
+
+{- | > Deprecated This method has been deprecated. Prefer using
+@duckdb_result_get_chunk@ instead.
+
+Returns the data of a specific column of a result in columnar format.
+
+The function returns a dense array which contains the result data. The exact
+type stored in the array depends on the corresponding duckdb_type (as provided
+by @duckdb_column_type@). For the exact type by which the data should be
+accessed, see the comments in [the types section](types) or the @DUCKDB_TYPE@
+enum.
+
+For example, for a column of type @DUCKDB_TYPE_INTEGER@, rows can be accessed
+in the following manner: ``@c int32_t *data = (int32_t *)
+duckdb_column_data(&result, 0); printf("Data for row %d: %d\n", row,
+data[row]); @``
+
+Parameters:
+* @result@: The result object to fetch the column data from.
+* @col@: The column index.
+
+Returns The column data of the specified column.
+-}
+foreign import ccall unsafe "duckdb_column_data"
+    c_duckdb_column_data :: Ptr DuckDBResult -> DuckDBIdx -> IO (Ptr ())
+
+{- | > Deprecated This method has been deprecated. Prefer using
+@duckdb_result_get_chunk@ instead.
+
+Returns the nullmask of a specific column of a result in columnar format. The
+nullmask indicates for every row whether or not the corresponding row is
+@NULL@. If a row is @NULL@, the values present in the array provided by
+@duckdb_column_data@ are undefined.
+
+``@c int32_t *data = (int32_t *) duckdb_column_data(&result, 0); bool
+*nullmask = duckdb_nullmask_data(&result, 0); if (nullmask[row]) {
+printf("Data for row %d: NULL\n", row); } else { printf("Data for row %d:
+%d\n", row, data[row]); } @``
+
+Parameters:
+* @result@: The result object to fetch the nullmask from.
+* @col@: The column index.
+
+Returns The nullmask of the specified column.
+-}
+foreign import ccall unsafe "duckdb_nullmask_data"
+    c_duckdb_nullmask_data :: Ptr DuckDBResult -> DuckDBIdx -> IO (Ptr CBool)
+
+{- | Returns the error message contained within the result. The error is only set
+if @duckdb_query@ returns @DuckDBError@.
+
+The result of this function must not be freed. It will be cleaned up when
+@duckdb_destroy_result@ is called.
+
+Parameters:
+* @result@: The result object to fetch the error from.
+
+Returns The error of the result.
+-}
+foreign import ccall unsafe "duckdb_result_error"
+    c_duckdb_result_error :: Ptr DuckDBResult -> IO CString
+
+{- | Returns the result error type contained within the result. The error is only
+set if @duckdb_query@ returns @DuckDBError@.
+
+Parameters:
+* @result@: The result object to fetch the error from.
+
+Returns The error type of the result.
+-}
+foreign import ccall unsafe "duckdb_result_error_type"
+    c_duckdb_result_error_type :: Ptr DuckDBResult -> IO DuckDBErrorType
diff --git a/src/Database/DuckDB/FFI/ReplacementScans.hs b/src/Database/DuckDB/FFI/ReplacementScans.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ReplacementScans.hs
@@ -0,0 +1,50 @@
+module Database.DuckDB.FFI.ReplacementScans (
+    c_duckdb_add_replacement_scan,
+    c_duckdb_replacement_scan_set_function_name,
+    c_duckdb_replacement_scan_add_parameter,
+    c_duckdb_replacement_scan_set_error,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.Ptr (Ptr)
+
+{- | Add a replacement scan definition to the specified database.
+
+Parameters:
+* @db@: The database object to add the replacement scan to
+* @replacement@: The replacement scan callback
+* @extra_data@: Extra data that is passed back into the specified callback
+* @delete_callback@: The delete callback to call on the extra data, if any
+-}
+foreign import ccall unsafe "duckdb_add_replacement_scan"
+    c_duckdb_add_replacement_scan :: DuckDBDatabase -> DuckDBReplacementCallback -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Sets the replacement function name. If this function is called in the
+replacement callback, the replacement scan is performed. If it is not called,
+the replacement callback is not performed.
+
+Parameters:
+* @info@: The info object
+* @function_name@: The function name to substitute.
+-}
+foreign import ccall unsafe "duckdb_replacement_scan_set_function_name"
+    c_duckdb_replacement_scan_set_function_name :: DuckDBReplacementScanInfo -> CString -> IO ()
+
+{- | Adds a parameter to the replacement scan function.
+
+Parameters:
+* @info@: The info object
+* @parameter@: The parameter to add.
+-}
+foreign import ccall unsafe "duckdb_replacement_scan_add_parameter"
+    c_duckdb_replacement_scan_add_parameter :: DuckDBReplacementScanInfo -> DuckDBValue -> IO ()
+
+{- | Report that an error has occurred while executing the replacement scan.
+
+Parameters:
+* @info@: The info object
+* @error@: The error message
+-}
+foreign import ccall unsafe "duckdb_replacement_scan_set_error"
+    c_duckdb_replacement_scan_set_error :: DuckDBReplacementScanInfo -> CString -> IO ()
diff --git a/src/Database/DuckDB/FFI/ResultFunctions.hs b/src/Database/DuckDB/FFI/ResultFunctions.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ResultFunctions.hs
@@ -0,0 +1,92 @@
+module Database.DuckDB.FFI.ResultFunctions (
+    c_duckdb_result_get_chunk,
+    c_duckdb_result_is_streaming,
+    c_duckdb_result_chunk_count,
+    c_duckdb_result_return_type,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Fetches a data chunk from the duckdb_result. This function should be called
+repeatedly until the result is exhausted.
+
+The result must be destroyed with @duckdb_destroy_data_chunk@.
+
+This function supersedes all @duckdb_value@ functions, as well as the
+@duckdb_column_data@ and @duckdb_nullmask_data@ functions. It results in
+significantly better performance, and should be preferred in newer code-bases.
+
+If this function is used, none of the other result functions can be used and
+vice versa (i.e., this function cannot be mixed with the legacy result
+functions).
+
+Use @duckdb_result_chunk_count@ to figure out how many chunks there are in the
+result.
+
+Parameters:
+* @result@: The result object to fetch the data chunk from.
+* @chunk_index@: The chunk index to fetch from.
+
+Returns The resulting data chunk. Returns @NULL@ if the chunk index is out of
+bounds.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_get_chunk@ but mirror the DuckDB C API semantics of
+@duckdb_result_get_chunk@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_get_chunk"
+    c_duckdb_result_get_chunk :: Ptr DuckDBResult -> DuckDBIdx -> IO DuckDBDataChunk
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Checks if the type of the internal result is StreamQueryResult.
+
+Parameters:
+* @result@: The result object to check.
+
+Returns Whether or not the result object is of the type StreamQueryResult
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_is_streaming@ but mirror the DuckDB C API semantics of
+@duckdb_result_is_streaming@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_is_streaming"
+    c_duckdb_result_is_streaming :: Ptr DuckDBResult -> IO CBool
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns the number of data chunks present in the result.
+
+Parameters:
+* @result@: The result object
+
+Returns Number of data chunks present in the result.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_chunk_count@ but mirror the DuckDB C API semantics of
+@duckdb_result_chunk_count@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_chunk_count"
+    c_duckdb_result_chunk_count :: Ptr DuckDBResult -> IO DuckDBIdx
+
+{- | Returns the return_type of the given result, or DUCKDB_RETURN_TYPE_INVALID on
+error
+
+Parameters:
+* @result@: The result object
+
+Returns The return_type
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_result_return_type@ but mirror the DuckDB C API semantics of
+@duckdb_result_return_type@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_result_return_type"
+    c_duckdb_result_return_type :: Ptr DuckDBResult -> IO DuckDBResultType
diff --git a/src/Database/DuckDB/FFI/SafeFetch.hs b/src/Database/DuckDB/FFI/SafeFetch.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/SafeFetch.hs
@@ -0,0 +1,280 @@
+module Database.DuckDB.FFI.SafeFetch (
+    c_duckdb_value_boolean,
+    c_duckdb_value_int8,
+    c_duckdb_value_int16,
+    c_duckdb_value_int32,
+    c_duckdb_value_int64,
+    c_duckdb_value_hugeint,
+    c_duckdb_value_uhugeint,
+    c_duckdb_value_decimal,
+    c_duckdb_value_uint8,
+    c_duckdb_value_uint16,
+    c_duckdb_value_uint32,
+    c_duckdb_value_uint64,
+    c_duckdb_value_float,
+    c_duckdb_value_double,
+    c_duckdb_value_date,
+    c_duckdb_value_time,
+    c_duckdb_value_timestamp,
+    c_duckdb_value_interval,
+    c_duckdb_value_varchar,
+    c_duckdb_value_string,
+    c_duckdb_value_varchar_internal,
+    c_duckdb_value_string_internal,
+    c_duckdb_value_blob,
+    c_duckdb_value_is_null,
+) where
+
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..))
+import Foreign.Ptr (Ptr)
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The boolean value at the specified location, or false if the value
+cannot be converted.
+-}
+foreign import ccall unsafe "duckdb_value_boolean"
+    c_duckdb_value_boolean :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CBool
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The int8_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_int8"
+    c_duckdb_value_int8 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Int8
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The int16_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_int16"
+    c_duckdb_value_int16 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Int16
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The int32_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_int32"
+    c_duckdb_value_int32 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Int32
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The int64_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_int64"
+    c_duckdb_value_int64 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Int64
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_hugeint value at the specified location, or 0 if the value
+cannot be converted.
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_hugeint@
+but mirror the DuckDB C API semantics of @duckdb_value_hugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_hugeint"
+    c_duckdb_value_hugeint :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBHugeInt -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_uhugeint value at the specified location, or 0 if the value
+cannot be converted.
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_uhugeint@
+but mirror the DuckDB C API semantics of @duckdb_value_uhugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_uhugeint"
+    c_duckdb_value_uhugeint :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBUHugeInt -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_decimal value at the specified location, or 0 if the value
+cannot be converted.
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_decimal@
+but mirror the DuckDB C API semantics of @duckdb_value_decimal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_decimal"
+    c_duckdb_value_decimal :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBDecimal -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The uint8_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_uint8"
+    c_duckdb_value_uint8 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Word8
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The uint16_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_uint16"
+    c_duckdb_value_uint16 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Word16
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The uint32_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_uint32"
+    c_duckdb_value_uint32 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Word32
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The uint64_t value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_uint64"
+    c_duckdb_value_uint64 :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Word64
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The float value at the specified location, or 0 if the value cannot be
+converted.
+-}
+foreign import ccall unsafe "duckdb_value_float"
+    c_duckdb_value_float :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CFloat
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The double value at the specified location, or 0 if the value cannot
+be converted.
+-}
+foreign import ccall unsafe "duckdb_value_double"
+    c_duckdb_value_double :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CDouble
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_date value at the specified location, or 0 if the value
+cannot be converted.
+-}
+foreign import ccall unsafe "duckdb_value_date"
+    c_duckdb_value_date :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO DuckDBDate
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_time value at the specified location, or 0 if the value
+cannot be converted.
+-}
+foreign import ccall unsafe "duckdb_value_time"
+    c_duckdb_value_time :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO DuckDBTime
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_timestamp value at the specified location, or 0 if the
+value cannot be converted.
+-}
+foreign import ccall unsafe "duckdb_value_timestamp"
+    c_duckdb_value_timestamp :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO DuckDBTimestamp
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_interval value at the specified location, or 0 if the value
+cannot be converted.
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_interval@
+but mirror the DuckDB C API semantics of @duckdb_value_interval@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_interval"
+    c_duckdb_value_interval :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBInterval -> IO ()
+
+{- | > Deprecated This method has been deprecated. Use duckdb_value_string instead.
+This function does not work correctly if the string contains null bytes.
+
+Returns The text value at the specified location as a null-terminated string,
+or nullptr if the value cannot be converted. The result must be freed with
+@duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_value_varchar"
+    c_duckdb_value_varchar :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CString
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+No support for nested types, and for other complex types. The resulting field
+"string.data" must be freed with @duckdb_free.@
+
+Returns The string value at the specified location. Attempts to cast the
+result value to string.
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_string@
+but mirror the DuckDB C API semantics of @duckdb_value_string@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_string"
+    c_duckdb_value_string :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBString -> IO ()
+
+{- | > Deprecated This method has been deprecated. Use duckdb_value_string_internal
+instead. This function does not work correctly if the string contains null
+bytes.
+
+Returns The char* value at the specified location. ONLY works on VARCHAR
+columns and does not auto-cast. If the column is NOT a VARCHAR column this
+function will return NULL. The result must NOT be freed.
+-}
+foreign import ccall unsafe "duckdb_value_varchar_internal"
+    c_duckdb_value_varchar_internal :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CString
+
+{- | > Deprecated This method has been deprecated. Use duckdb_value_string_internal
+instead. This function does not work correctly if the string contains null
+bytes.
+
+Returns The char* value at the specified location. ONLY works on VARCHAR
+columns and does not auto-cast. If the column is NOT a VARCHAR column this
+function will return NULL. The result must NOT be freed.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_value_string_internal@ but mirror the DuckDB C API semantics of
+@duckdb_value_string_internal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_string_internal"
+    c_duckdb_value_string_internal :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBString -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns The duckdb_blob value at the specified location. Returns a blob with
+blob.data set to nullptr if the value cannot be converted. The resulting field
+"blob.data" must be freed with @duckdb_free.@
+
+These bindings call the wrapper symbol @wrapped_duckdb_value_blob@ but
+mirror the DuckDB C API semantics of @duckdb_value_blob@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_value_blob"
+    c_duckdb_value_blob :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> Ptr DuckDBBlob -> IO ()
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Returns Returns true if the value at the specified index is NULL, and false
+otherwise.
+-}
+foreign import ccall unsafe "duckdb_value_is_null"
+    c_duckdb_value_is_null :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO CBool
diff --git a/src/Database/DuckDB/FFI/ScalarFunctions.hs b/src/Database/DuckDB/FFI/ScalarFunctions.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ScalarFunctions.hs
@@ -0,0 +1,299 @@
+module Database.DuckDB.FFI.ScalarFunctions (
+    c_duckdb_create_scalar_function,
+    c_duckdb_destroy_scalar_function,
+    c_duckdb_scalar_function_set_name,
+    c_duckdb_scalar_function_set_varargs,
+    c_duckdb_scalar_function_set_special_handling,
+    c_duckdb_scalar_function_set_volatile,
+    c_duckdb_scalar_function_add_parameter,
+    c_duckdb_scalar_function_set_return_type,
+    c_duckdb_scalar_function_set_extra_info,
+    c_duckdb_scalar_function_set_bind,
+    c_duckdb_scalar_function_set_bind_data,
+    c_duckdb_scalar_function_set_bind_data_copy,
+    c_duckdb_scalar_function_bind_set_error,
+    c_duckdb_scalar_function_set_function,
+    c_duckdb_register_scalar_function,
+    c_duckdb_scalar_function_get_extra_info,
+    c_duckdb_scalar_function_bind_get_extra_info,
+    c_duckdb_scalar_function_get_bind_data,
+    c_duckdb_scalar_function_get_client_context,
+    c_duckdb_scalar_function_set_error,
+    c_duckdb_create_scalar_function_set,
+    c_duckdb_destroy_scalar_function_set,
+    c_duckdb_add_scalar_function_to_set,
+    c_duckdb_register_scalar_function_set,
+    c_duckdb_scalar_function_bind_get_argument_count,
+    c_duckdb_scalar_function_bind_get_argument,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new empty scalar function.
+
+The return value must be destroyed with @duckdb_destroy_scalar_function@.
+
+Returns The scalar function object.
+-}
+foreign import ccall unsafe "duckdb_create_scalar_function"
+    c_duckdb_create_scalar_function :: IO DuckDBScalarFunction
+
+{- | Destroys the given scalar function object.
+
+Parameters:
+* @scalar_function@: The scalar function to destroy
+-}
+foreign import ccall unsafe "duckdb_destroy_scalar_function"
+    c_duckdb_destroy_scalar_function :: Ptr DuckDBScalarFunction -> IO ()
+
+{- | Sets the name of the given scalar function.
+
+Parameters:
+* @scalar_function@: The scalar function
+* @name@: The name of the scalar function
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_name"
+    c_duckdb_scalar_function_set_name :: DuckDBScalarFunction -> CString -> IO ()
+
+{- | Sets the parameters of the given scalar function to varargs. Does not require
+adding parameters with duckdb_scalar_function_add_parameter.
+
+Parameters:
+* @scalar_function@: The scalar function.
+* @type@: The type of the arguments.
+
+Returns The parameter type. Cannot contain INVALID.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_varargs"
+    c_duckdb_scalar_function_set_varargs :: DuckDBScalarFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the scalar function's null-handling behavior to special.
+
+Parameters:
+* @scalar_function@: The scalar function.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_special_handling"
+    c_duckdb_scalar_function_set_special_handling :: DuckDBScalarFunction -> IO ()
+
+{- | Sets the Function Stability of the scalar function to VOLATILE, indicating the
+function should be re-run for every row. This limits optimization that can be
+performed for the function.
+
+Parameters:
+* @scalar_function@: The scalar function.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_volatile"
+    c_duckdb_scalar_function_set_volatile :: DuckDBScalarFunction -> IO ()
+
+{- | Adds a parameter to the scalar function.
+
+Parameters:
+* @scalar_function@: The scalar function.
+* @type@: The parameter type. Cannot contain INVALID.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_add_parameter"
+    c_duckdb_scalar_function_add_parameter :: DuckDBScalarFunction -> DuckDBLogicalType -> IO ()
+
+{- | Sets the return type of the scalar function.
+
+Parameters:
+* @scalar_function@: The scalar function
+* @type@: Cannot contain INVALID or ANY.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_return_type"
+    c_duckdb_scalar_function_set_return_type :: DuckDBScalarFunction -> DuckDBLogicalType -> IO ()
+
+{- | Assigns extra information to the scalar function that can be fetched during
+binding, etc.
+
+Parameters:
+* @scalar_function@: The scalar function
+* @extra_info@: The extra information
+* @destroy@: The callback that will be called to destroy the extra information
+  (if any)
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_extra_info"
+    c_duckdb_scalar_function_set_extra_info :: DuckDBScalarFunction -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Sets the (optional) bind function of the scalar function.
+
+Parameters:
+* @scalar_function@: The scalar function.
+* @bind@: The bind function.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_bind"
+    c_duckdb_scalar_function_set_bind :: DuckDBScalarFunction -> DuckDBScalarFunctionBindFun -> IO ()
+
+{- | Sets the user-provided bind data in the bind object of the scalar function.
+The bind data object can be retrieved again during execution. In most case,
+you also need to set the copy-callback of your bind data via
+duckdb_scalar_function_set_bind_data_copy.
+
+Parameters:
+* @info@: The bind info of the scalar function.
+* @bind_data@: The bind data object.
+* @destroy@: The callback to destroy the bind data (if any).
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_bind_data"
+    c_duckdb_scalar_function_set_bind_data :: DuckDBBindInfo -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Sets the copy-callback for the user-provided bind data in the bind object of
+the scalar function.
+
+Parameters:
+* @info@: The bind info of the scalar function.
+* @copy@: The callback to copy the bind data (if any).
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_bind_data_copy"
+    c_duckdb_scalar_function_set_bind_data_copy :: DuckDBBindInfo -> DuckDBCopyCallback -> IO ()
+
+{- | Report that an error has occurred while calling bind on a scalar function.
+
+Parameters:
+* @info@: The bind info object.
+* @error@: The error message.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_bind_set_error"
+    c_duckdb_scalar_function_bind_set_error :: DuckDBBindInfo -> CString -> IO ()
+
+{- | Sets the main function of the scalar function.
+
+Parameters:
+* @scalar_function@: The scalar function
+* @function@: The function
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_function"
+    c_duckdb_scalar_function_set_function :: DuckDBScalarFunction -> DuckDBScalarFunctionFun -> IO ()
+
+{- | Register the scalar function object within the given connection.
+
+The function requires at least a name, a function and a return type.
+
+If the function is incomplete or a function with this name already exists
+DuckDBError is returned.
+
+Parameters:
+* @con@: The connection to register it in.
+* @scalar_function@: The function pointer
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_scalar_function"
+    c_duckdb_register_scalar_function :: DuckDBConnection -> DuckDBScalarFunction -> IO DuckDBState
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_scalar_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object.
+
+Returns The extra info.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_get_extra_info"
+    c_duckdb_scalar_function_get_extra_info :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Retrieves the extra info of the function as set in the bind info.
+
+Parameters:
+* @info@: The info object.
+
+Returns The extra info.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_bind_get_extra_info"
+    c_duckdb_scalar_function_bind_get_extra_info :: DuckDBBindInfo -> IO (Ptr ())
+
+{- | Gets the scalar function's bind data set by
+@duckdb_scalar_function_set_bind_data@. Note that the bind data is read-only.
+
+Parameters:
+* @info@: The function info.
+
+Returns The bind data object.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_get_bind_data"
+    c_duckdb_scalar_function_get_bind_data :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Retrieves the client context of the bind info of a scalar function.
+
+Parameters:
+* @info@: The bind info object of the scalar function.
+* @out_context@: The client context of the bind info. Must be destroyed with
+  @duckdb_destroy_client_context@.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_get_client_context"
+    c_duckdb_scalar_function_get_client_context :: DuckDBBindInfo -> Ptr DuckDBClientContext -> IO ()
+
+{- | Report that an error has occurred while executing the scalar function.
+
+Parameters:
+* @info@: The info object.
+* @error@: The error message
+-}
+foreign import ccall unsafe "duckdb_scalar_function_set_error"
+    c_duckdb_scalar_function_set_error :: DuckDBFunctionInfo -> CString -> IO ()
+
+{- | Creates a new empty scalar function set.
+
+The return value must be destroyed with @duckdb_destroy_scalar_function_set@.
+
+Returns The scalar function set object.
+-}
+foreign import ccall unsafe "duckdb_create_scalar_function_set"
+    c_duckdb_create_scalar_function_set :: CString -> IO DuckDBScalarFunctionSet
+
+-- | Destroys the given scalar function set object.
+foreign import ccall unsafe "duckdb_destroy_scalar_function_set"
+    c_duckdb_destroy_scalar_function_set :: Ptr DuckDBScalarFunctionSet -> IO ()
+
+{- | Adds the scalar function as a new overload to the scalar function set.
+
+Returns DuckDBError if the function could not be added, for example if the
+overload already exists.
+
+Parameters:
+* @set@: The scalar function set
+* @function@: The function to add
+-}
+foreign import ccall unsafe "duckdb_add_scalar_function_to_set"
+    c_duckdb_add_scalar_function_to_set :: DuckDBScalarFunctionSet -> DuckDBScalarFunction -> IO DuckDBState
+
+{- | Register the scalar function set within the given connection.
+
+The set requires at least a single valid overload.
+
+If the set is incomplete or a function with this name already exists
+DuckDBError is returned.
+
+Parameters:
+* @con@: The connection to register it in.
+* @set@: The function set to register
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_scalar_function_set"
+    c_duckdb_register_scalar_function_set :: DuckDBConnection -> DuckDBScalarFunctionSet -> IO DuckDBState
+
+{- | Returns the number of input arguments of the scalar function.
+
+Parameters:
+* @info@: The bind info.
+
+Returns The number of input arguments.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_bind_get_argument_count"
+    c_duckdb_scalar_function_bind_get_argument_count :: DuckDBBindInfo -> IO DuckDBIdx
+
+{- | Returns the input argument at index of the scalar function.
+
+Parameters:
+* @info@: The bind info.
+* @index@: The argument index.
+
+Returns The input argument at index. Must be destroyed with
+@duckdb_destroy_expression@.
+-}
+foreign import ccall unsafe "duckdb_scalar_function_bind_get_argument"
+    c_duckdb_scalar_function_bind_get_argument :: DuckDBBindInfo -> DuckDBIdx -> IO DuckDBExpression
diff --git a/src/Database/DuckDB/FFI/SelectionVector.hs b/src/Database/DuckDB/FFI/SelectionVector.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/SelectionVector.hs
@@ -0,0 +1,37 @@
+module Database.DuckDB.FFI.SelectionVector (
+    c_duckdb_create_selection_vector,
+    c_duckdb_destroy_selection_vector,
+    c_duckdb_selection_vector_get_data_ptr,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new selection vector of size @size@. Must be destroyed with
+@duckdb_destroy_selection_vector@.
+
+Parameters:
+* @size@: The size of the selection vector.
+
+Returns The selection vector.
+-}
+foreign import ccall unsafe "duckdb_create_selection_vector"
+    c_duckdb_create_selection_vector :: DuckDBIdx -> IO DuckDBSelectionVector
+
+{- | Destroys the selection vector and de-allocates its memory.
+
+Parameters:
+* @sel@: The selection vector.
+-}
+foreign import ccall unsafe "duckdb_destroy_selection_vector"
+    c_duckdb_destroy_selection_vector :: DuckDBSelectionVector -> IO ()
+
+{- | Access the data pointer of a selection vector.
+
+Parameters:
+* @sel@: The selection vector.
+
+Returns The data pointer.
+-}
+foreign import ccall unsafe "duckdb_selection_vector_get_data_ptr"
+    c_duckdb_selection_vector_get_data_ptr :: DuckDBSelectionVector -> IO (Ptr DuckDBSel)
diff --git a/src/Database/DuckDB/FFI/StreamingResult.hs b/src/Database/DuckDB/FFI/StreamingResult.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/StreamingResult.hs
@@ -0,0 +1,54 @@
+module Database.DuckDB.FFI.StreamingResult (
+    c_duckdb_stream_fetch_chunk,
+    c_duckdb_fetch_chunk,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.Ptr (Ptr)
+
+{- | > Warning Deprecation notice. This method is scheduled for removal in a future
+release.
+
+Fetches a data chunk from the (streaming) duckdb_result. This function should
+be called repeatedly until the result is exhausted.
+
+The result must be destroyed with @duckdb_destroy_data_chunk@.
+
+This function can only be used on duckdb_results created with
+@duckdb_pending_prepared_streaming@
+
+If this function is used, none of the other result functions can be used and
+vice versa (i.e., this function cannot be mixed with the legacy result
+functions or the materialized result functions).
+
+It is not known beforehand how many chunks will be returned by this result.
+
+Parameters:
+* @result@: The result object to fetch the data chunk from.
+
+Returns The resulting data chunk. Returns @NULL@ if the result has an error.
+
+These bindings call the wrapper symbol
+@wrapped_duckdb_stream_fetch_chunk@ but mirror the DuckDB C API semantics of
+@duckdb_stream_fetch_chunk@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_stream_fetch_chunk"
+    c_duckdb_stream_fetch_chunk :: Ptr DuckDBResult -> IO DuckDBDataChunk
+
+{- | Fetches a data chunk from a duckdb_result. This function should be called
+repeatedly until the result is exhausted.
+
+The result must be destroyed with @duckdb_destroy_data_chunk@.
+
+It is not known beforehand how many chunks will be returned by this result.
+
+Parameters:
+* @result@: The result object to fetch the data chunk from.
+
+Returns The resulting data chunk. Returns @NULL@ if the result has an error.
+
+These bindings call the wrapper symbol @wrapped_duckdb_fetch_chunk@ but
+mirror the DuckDB C API semantics of @duckdb_fetch_chunk@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_fetch_chunk"
+    c_duckdb_fetch_chunk :: Ptr DuckDBResult -> IO DuckDBDataChunk
diff --git a/src/Database/DuckDB/FFI/TableDescription.hs b/src/Database/DuckDB/FFI/TableDescription.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/TableDescription.hs
@@ -0,0 +1,90 @@
+module Database.DuckDB.FFI.TableDescription (
+    c_duckdb_table_description_create,
+    c_duckdb_table_description_create_ext,
+    c_duckdb_table_description_destroy,
+    c_duckdb_table_description_error,
+    c_duckdb_column_has_default,
+    c_duckdb_table_description_get_column_name,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a table description object. Note that
+@duckdb_table_description_destroy@ should always be called on the resulting
+table_description, even if the function returns @DuckDBError@.
+
+Parameters:
+* @connection@: The connection context.
+* @schema@: The schema of the table, or @nullptr@ for the default schema.
+* @table@: The table name.
+* @out@: The resulting table description object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_table_description_create"
+    c_duckdb_table_description_create :: DuckDBConnection -> CString -> CString -> Ptr DuckDBTableDescription -> IO DuckDBState
+
+{- | Creates a table description object. Note that
+@duckdb_table_description_destroy@ must be called on the resulting
+table_description, even if the function returns @DuckDBError@.
+
+Parameters:
+* @connection@: The connection context.
+* @catalog@: The catalog (database) name of the table, or @nullptr@ for the
+  default catalog.
+* @schema@: The schema of the table, or @nullptr@ for the default schema.
+* @table@: The table name.
+* @out@: The resulting table description object.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall safe "duckdb_table_description_create_ext"
+    c_duckdb_table_description_create_ext :: DuckDBConnection -> CString -> CString -> CString -> Ptr DuckDBTableDescription -> IO DuckDBState
+
+{- | Destroy the TableDescription object.
+
+Parameters:
+* @table_description@: The table_description to destroy.
+-}
+foreign import ccall unsafe "duckdb_table_description_destroy"
+    c_duckdb_table_description_destroy :: Ptr DuckDBTableDescription -> IO ()
+
+{- | Returns the error message associated with the given table_description. If the
+table_description has no error message, this returns @nullptr@ instead. The
+error message should not be freed. It will be de-allocated when
+@duckdb_table_description_destroy@ is called.
+
+Parameters:
+* @table_description@: The table_description to get the error from.
+
+Returns The error message, or @nullptr@ if there is none.
+-}
+foreign import ccall unsafe "duckdb_table_description_error"
+    c_duckdb_table_description_error :: DuckDBTableDescription -> IO CString
+
+{- | Check if the column at @index@ index of the table has a DEFAULT expression.
+
+Parameters:
+* @table_description@: The table_description to query.
+* @index@: The index of the column to query.
+* @out@: The out-parameter used to store the result.
+
+Returns @DuckDBSuccess@ on success or @DuckDBError@ on failure.
+-}
+foreign import ccall unsafe "duckdb_column_has_default"
+    c_duckdb_column_has_default :: DuckDBTableDescription -> DuckDBIdx -> Ptr CBool -> IO DuckDBState
+
+{- | Obtain the column name at @index@. The out result must be destroyed with
+@duckdb_free@.
+
+Parameters:
+* @table_description@: The table_description to query.
+* @index@: The index of the column to query.
+
+Returns The column name.
+-}
+foreign import ccall unsafe "duckdb_table_description_get_column_name"
+    c_duckdb_table_description_get_column_name :: DuckDBTableDescription -> DuckDBIdx -> IO CString
diff --git a/src/Database/DuckDB/FFI/TableFunctions.hs b/src/Database/DuckDB/FFI/TableFunctions.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/TableFunctions.hs
@@ -0,0 +1,405 @@
+module Database.DuckDB.FFI.TableFunctions (
+    -- Table function definition
+    c_duckdb_create_table_function,
+    c_duckdb_destroy_table_function,
+    c_duckdb_table_function_set_name,
+    c_duckdb_table_function_add_parameter,
+    c_duckdb_table_function_add_named_parameter,
+    c_duckdb_table_function_set_extra_info,
+    c_duckdb_table_function_set_bind,
+    c_duckdb_table_function_set_init,
+    c_duckdb_table_function_set_local_init,
+    c_duckdb_table_function_set_function,
+    c_duckdb_table_function_supports_projection_pushdown,
+    c_duckdb_register_table_function,
+    -- Bind helpers
+    c_duckdb_bind_get_extra_info,
+    c_duckdb_table_function_get_client_context,
+    c_duckdb_bind_add_result_column,
+    c_duckdb_bind_get_parameter_count,
+    c_duckdb_bind_get_parameter,
+    c_duckdb_bind_get_named_parameter,
+    c_duckdb_bind_set_bind_data,
+    c_duckdb_bind_set_cardinality,
+    c_duckdb_bind_set_error,
+    -- Init helpers
+    c_duckdb_init_get_extra_info,
+    c_duckdb_init_get_bind_data,
+    c_duckdb_init_set_init_data,
+    c_duckdb_init_get_column_count,
+    c_duckdb_init_get_column_index,
+    c_duckdb_init_set_max_threads,
+    c_duckdb_init_set_error,
+    -- Execution helpers
+    c_duckdb_function_get_extra_info,
+    c_duckdb_function_get_bind_data,
+    c_duckdb_function_get_init_data,
+    c_duckdb_function_get_local_init_data,
+    c_duckdb_function_set_error,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a new empty table function.
+
+The return value should be destroyed with @duckdb_destroy_table_function@.
+
+Returns The table function object.
+-}
+foreign import ccall unsafe "duckdb_create_table_function"
+    c_duckdb_create_table_function :: IO DuckDBTableFunction
+
+{- | Destroys the given table function object.
+
+Parameters:
+* @table_function@: The table function to destroy
+-}
+foreign import ccall unsafe "duckdb_destroy_table_function"
+    c_duckdb_destroy_table_function :: Ptr DuckDBTableFunction -> IO ()
+
+{- | Sets the name of the given table function.
+
+Parameters:
+* @table_function@: The table function
+* @name@: The name of the table function
+-}
+foreign import ccall unsafe "duckdb_table_function_set_name"
+    c_duckdb_table_function_set_name :: DuckDBTableFunction -> CString -> IO ()
+
+{- | Adds a parameter to the table function.
+
+Parameters:
+* @table_function@: The table function.
+* @type@: The parameter type. Cannot contain INVALID.
+-}
+foreign import ccall unsafe "duckdb_table_function_add_parameter"
+    c_duckdb_table_function_add_parameter :: DuckDBTableFunction -> DuckDBLogicalType -> IO ()
+
+{- | Adds a named parameter to the table function.
+
+Parameters:
+* @table_function@: The table function.
+* @name@: The parameter name.
+* @type@: The parameter type. Cannot contain INVALID.
+-}
+foreign import ccall unsafe "duckdb_table_function_add_named_parameter"
+    c_duckdb_table_function_add_named_parameter :: DuckDBTableFunction -> CString -> DuckDBLogicalType -> IO ()
+
+{- | Assigns extra information to the table function that can be fetched during
+binding, etc.
+
+Parameters:
+* @table_function@: The table function
+* @extra_info@: The extra information
+* @destroy@: The callback that will be called to destroy the extra information
+  (if any)
+-}
+foreign import ccall unsafe "duckdb_table_function_set_extra_info"
+    c_duckdb_table_function_set_extra_info :: DuckDBTableFunction -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Sets the bind function of the table function.
+
+Parameters:
+* @table_function@: The table function
+* @bind@: The bind function
+-}
+foreign import ccall unsafe "duckdb_table_function_set_bind"
+    c_duckdb_table_function_set_bind :: DuckDBTableFunction -> DuckDBTableFunctionBindFun -> IO ()
+
+{- | Sets the init function of the table function.
+
+Parameters:
+* @table_function@: The table function
+* @init@: The init function
+-}
+foreign import ccall unsafe "duckdb_table_function_set_init"
+    c_duckdb_table_function_set_init :: DuckDBTableFunction -> DuckDBTableFunctionInitFun -> IO ()
+
+{- | Sets the thread-local init function of the table function.
+
+Parameters:
+* @table_function@: The table function
+* @init@: The init function
+-}
+foreign import ccall unsafe "duckdb_table_function_set_local_init"
+    c_duckdb_table_function_set_local_init :: DuckDBTableFunction -> DuckDBTableFunctionInitFun -> IO ()
+
+{- | Sets the main function of the table function.
+
+Parameters:
+* @table_function@: The table function
+* @function@: The function
+-}
+foreign import ccall unsafe "duckdb_table_function_set_function"
+    c_duckdb_table_function_set_function :: DuckDBTableFunction -> DuckDBTableFunctionFun -> IO ()
+
+{- | Sets whether or not the given table function supports projection pushdown.
+
+If this is set to true, the system will provide a list of all required columns
+in the @init@ stage through the @duckdb_init_get_column_count@ and
+@duckdb_init_get_column_index@ functions. If this is set to false (the
+default), the system will expect all columns to be projected.
+
+Parameters:
+* @table_function@: The table function
+* @pushdown@: True if the table function supports projection pushdown, false
+  otherwise.
+-}
+foreign import ccall unsafe "duckdb_table_function_supports_projection_pushdown"
+    c_duckdb_table_function_supports_projection_pushdown :: DuckDBTableFunction -> CBool -> IO ()
+
+{- | Register the table function object within the given connection.
+
+The function requires at least a name, a bind function, an init function and a
+main function.
+
+If the function is incomplete or a function with this name already exists
+DuckDBError is returned.
+
+Parameters:
+* @con@: The connection to register it in.
+* @function@: The function pointer
+
+Returns Whether or not the registration was successful.
+-}
+foreign import ccall unsafe "duckdb_register_table_function"
+    c_duckdb_register_table_function :: DuckDBConnection -> DuckDBTableFunction -> IO DuckDBState
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_table_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object
+
+Returns The extra info
+-}
+foreign import ccall unsafe "duckdb_bind_get_extra_info"
+    c_duckdb_bind_get_extra_info :: DuckDBBindInfo -> IO (Ptr ())
+
+{- | Retrieves the client context of the bind info of a table function.
+
+Parameters:
+* @info@: The bind info object of the table function.
+* @out_context@: The client context of the bind info. Must be destroyed with
+  @duckdb_destroy_client_context@.
+-}
+foreign import ccall unsafe "duckdb_table_function_get_client_context"
+    c_duckdb_table_function_get_client_context :: DuckDBBindInfo -> Ptr DuckDBClientContext -> IO ()
+
+{- | Adds a result column to the output of the table function.
+
+Parameters:
+* @info@: The table function's bind info.
+* @name@: The column name.
+* @type@: The logical column type.
+-}
+foreign import ccall unsafe "duckdb_bind_add_result_column"
+    c_duckdb_bind_add_result_column :: DuckDBBindInfo -> CString -> DuckDBLogicalType -> IO ()
+
+{- | Retrieves the number of regular (non-named) parameters to the function.
+
+Parameters:
+* @info@: The info object
+
+Returns The number of parameters
+-}
+foreign import ccall unsafe "duckdb_bind_get_parameter_count"
+    c_duckdb_bind_get_parameter_count :: DuckDBBindInfo -> IO DuckDBIdx
+
+{- | Retrieves the parameter at the given index.
+
+The result must be destroyed with @duckdb_destroy_value@.
+
+Parameters:
+* @info@: The info object
+* @index@: The index of the parameter to get
+
+Returns The value of the parameter. Must be destroyed with
+@duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_bind_get_parameter"
+    c_duckdb_bind_get_parameter :: DuckDBBindInfo -> DuckDBIdx -> IO DuckDBValue
+
+{- | Retrieves a named parameter with the given name.
+
+The result must be destroyed with @duckdb_destroy_value@.
+
+Parameters:
+* @info@: The info object
+* @name@: The name of the parameter
+
+Returns The value of the parameter. Must be destroyed with
+@duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_bind_get_named_parameter"
+    c_duckdb_bind_get_named_parameter :: DuckDBBindInfo -> CString -> IO DuckDBValue
+
+{- | Sets the user-provided bind data in the bind object of the table function.
+This object can be retrieved again during execution.
+
+Parameters:
+* @info@: The bind info of the table function.
+* @bind_data@: The bind data object.
+* @destroy@: The callback to destroy the bind data (if any).
+-}
+foreign import ccall unsafe "duckdb_bind_set_bind_data"
+    c_duckdb_bind_set_bind_data :: DuckDBBindInfo -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Sets the cardinality estimate for the table function, used for optimization.
+
+Parameters:
+* @info@: The bind data object.
+* @is_exact@: Whether or not the cardinality estimate is exact, or an
+  approximation
+-}
+foreign import ccall unsafe "duckdb_bind_set_cardinality"
+    c_duckdb_bind_set_cardinality :: DuckDBBindInfo -> DuckDBIdx -> CBool -> IO ()
+
+{- | Report that an error has occurred while calling bind on a table function.
+
+Parameters:
+* @info@: The info object
+* @error@: The error message
+-}
+foreign import ccall unsafe "duckdb_bind_set_error"
+    c_duckdb_bind_set_error :: DuckDBBindInfo -> CString -> IO ()
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_table_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object
+
+Returns The extra info
+-}
+foreign import ccall unsafe "duckdb_init_get_extra_info"
+    c_duckdb_init_get_extra_info :: DuckDBInitInfo -> IO (Ptr ())
+
+{- | Gets the bind data set by @duckdb_bind_set_bind_data@ during the bind.
+
+Note that the bind data should be considered as read-only. For tracking state,
+use the init data instead.
+
+Parameters:
+* @info@: The info object
+
+Returns The bind data object
+-}
+foreign import ccall unsafe "duckdb_init_get_bind_data"
+    c_duckdb_init_get_bind_data :: DuckDBInitInfo -> IO (Ptr ())
+
+{- | Sets the user-provided init data in the init object. This object can be
+retrieved again during execution.
+
+Parameters:
+* @info@: The info object
+* @init_data@: The init data object.
+* @destroy@: The callback that will be called to destroy the init data (if
+  any)
+-}
+foreign import ccall unsafe "duckdb_init_set_init_data"
+    c_duckdb_init_set_init_data :: DuckDBInitInfo -> Ptr () -> DuckDBDeleteCallback -> IO ()
+
+{- | Returns the number of projected columns.
+
+This function must be used if projection pushdown is enabled to figure out
+which columns to emit.
+
+Parameters:
+* @info@: The info object
+
+Returns The number of projected columns.
+-}
+foreign import ccall unsafe "duckdb_init_get_column_count"
+    c_duckdb_init_get_column_count :: DuckDBInitInfo -> IO DuckDBIdx
+
+{- | Returns the column index of the projected column at the specified position.
+
+This function must be used if projection pushdown is enabled to figure out
+which columns to emit.
+
+Parameters:
+* @info@: The info object
+* @column_index@: The index at which to get the projected column index, from
+  0..duckdb_init_get_column_count(info)
+
+Returns The column index of the projected column.
+-}
+foreign import ccall unsafe "duckdb_init_get_column_index"
+    c_duckdb_init_get_column_index :: DuckDBInitInfo -> DuckDBIdx -> IO DuckDBIdx
+
+{- | Sets how many threads can process this table function in parallel (default: 1)
+
+Parameters:
+* @info@: The info object
+* @max_threads@: The maximum amount of threads that can process this table
+  function
+-}
+foreign import ccall unsafe "duckdb_init_set_max_threads"
+    c_duckdb_init_set_max_threads :: DuckDBInitInfo -> DuckDBIdx -> IO ()
+
+{- | Report that an error has occurred while calling init.
+
+Parameters:
+* @info@: The info object
+* @error@: The error message
+-}
+foreign import ccall unsafe "duckdb_init_set_error"
+    c_duckdb_init_set_error :: DuckDBInitInfo -> CString -> IO ()
+
+{- | Retrieves the extra info of the function as set in
+@duckdb_table_function_set_extra_info@.
+
+Parameters:
+* @info@: The info object
+
+Returns The extra info
+-}
+foreign import ccall unsafe "duckdb_function_get_extra_info"
+    c_duckdb_function_get_extra_info :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Gets the table function's bind data set by @duckdb_bind_set_bind_data@.
+
+Note that the bind data is read-only. For tracking state, use the init data
+instead.
+
+Parameters:
+* @info@: The function info object.
+
+Returns The bind data object.
+-}
+foreign import ccall unsafe "duckdb_function_get_bind_data"
+    c_duckdb_function_get_bind_data :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Gets the init data set by @duckdb_init_set_init_data@ during the init.
+
+Parameters:
+* @info@: The info object
+
+Returns The init data object
+-}
+foreign import ccall unsafe "duckdb_function_get_init_data"
+    c_duckdb_function_get_init_data :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Gets the thread-local init data set by @duckdb_init_set_init_data@ during the
+local_init.
+
+Parameters:
+* @info@: The info object
+
+Returns The init data object
+-}
+foreign import ccall unsafe "duckdb_function_get_local_init_data"
+    c_duckdb_function_get_local_init_data :: DuckDBFunctionInfo -> IO (Ptr ())
+
+{- | Report that an error has occurred while executing the function.
+
+Parameters:
+* @info@: The info object
+* @error@: The error message
+-}
+foreign import ccall unsafe "duckdb_function_set_error"
+    c_duckdb_function_set_error :: DuckDBFunctionInfo -> CString -> IO ()
diff --git a/src/Database/DuckDB/FFI/Threading.hs b/src/Database/DuckDB/FFI/Threading.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Threading.hs
@@ -0,0 +1,104 @@
+module Database.DuckDB.FFI.Threading (
+    c_duckdb_execute_tasks,
+    c_duckdb_create_task_state,
+    c_duckdb_execute_tasks_state,
+    c_duckdb_execute_n_tasks_state,
+    c_duckdb_finish_execution,
+    c_duckdb_task_state_is_finished,
+    c_duckdb_destroy_task_state,
+    c_duckdb_execution_is_finished,
+) where
+
+import Database.DuckDB.FFI.Types
+import Foreign.C.Types (CBool (..))
+
+{- | Execute DuckDB tasks on this thread.
+
+Will return after @max_tasks@ have been executed, or if there are no more
+tasks present.
+
+Parameters:
+* @database@: The database object to execute tasks for
+* @max_tasks@: The maximum amount of tasks to execute
+-}
+foreign import ccall safe "duckdb_execute_tasks"
+    c_duckdb_execute_tasks :: DuckDBDatabase -> DuckDBIdx -> IO ()
+
+{- | Creates a task state that can be used with duckdb_execute_tasks_state to
+execute tasks until @duckdb_finish_execution@ is called on the state.
+
+@duckdb_destroy_state@ must be called on the result.
+
+Parameters:
+* @database@: The database object to create the task state for
+
+Returns The task state that can be used with duckdb_execute_tasks_state.
+-}
+foreign import ccall unsafe "duckdb_create_task_state"
+    c_duckdb_create_task_state :: DuckDBDatabase -> IO DuckDBTaskState
+
+{- | Execute DuckDB tasks on this thread.
+
+The thread will keep on executing tasks forever, until duckdb_finish_execution
+is called on the state. Multiple threads can share the same duckdb_task_state.
+
+Parameters:
+* @state@: The task state of the executor
+-}
+foreign import ccall safe "duckdb_execute_tasks_state"
+    c_duckdb_execute_tasks_state :: DuckDBTaskState -> IO ()
+
+{- | Execute DuckDB tasks on this thread.
+
+The thread will keep on executing tasks until either duckdb_finish_execution
+is called on the state, max_tasks tasks have been executed or there are no
+more tasks to be executed.
+
+Multiple threads can share the same duckdb_task_state.
+
+Parameters:
+* @state@: The task state of the executor
+* @max_tasks@: The maximum amount of tasks to execute
+
+Returns The amount of tasks that have actually been executed
+-}
+foreign import ccall safe "duckdb_execute_n_tasks_state"
+    c_duckdb_execute_n_tasks_state :: DuckDBTaskState -> DuckDBIdx -> IO DuckDBIdx
+
+{- | Finish execution on a specific task.
+
+Parameters:
+* @state@: The task state to finish execution
+-}
+foreign import ccall unsafe "duckdb_finish_execution"
+    c_duckdb_finish_execution :: DuckDBTaskState -> IO ()
+
+{- | Check if the provided duckdb_task_state has finished execution
+
+Parameters:
+* @state@: The task state to inspect
+
+Returns Whether or not duckdb_finish_execution has been called on the task
+state
+-}
+foreign import ccall unsafe "duckdb_task_state_is_finished"
+    c_duckdb_task_state_is_finished :: DuckDBTaskState -> IO CBool
+
+{- | Destroys the task state returned from duckdb_create_task_state.
+
+Note that this should not be called while there is an active
+duckdb_execute_tasks_state running on the task state.
+
+Parameters:
+* @state@: The task state to clean up
+-}
+foreign import ccall unsafe "duckdb_destroy_task_state"
+    c_duckdb_destroy_task_state :: DuckDBTaskState -> IO ()
+
+{- | Returns true if the execution of the current query is finished.
+
+Parameters:
+* @con@: The connection on which to check
+-}
+foreign import ccall unsafe "duckdb_execution_is_finished"
+    c_duckdb_execution_is_finished :: DuckDBConnection -> IO CBool
diff --git a/src/Database/DuckDB/FFI/Types.hs b/src/Database/DuckDB/FFI/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Types.hs
@@ -0,0 +1,1515 @@
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE StrictData #-}
+
+module Database.DuckDB.FFI.Types (
+    -- * Enumerations
+    DuckDBState (..),
+    pattern DuckDBSuccess,
+    pattern DuckDBError,
+    DuckDBType (..),
+    pattern DuckDBTypeInvalid,
+    pattern DuckDBTypeBoolean,
+    pattern DuckDBTypeTinyInt,
+    pattern DuckDBTypeSmallInt,
+    pattern DuckDBTypeInteger,
+    pattern DuckDBTypeBigInt,
+    pattern DuckDBTypeUTinyInt,
+    pattern DuckDBTypeUSmallInt,
+    pattern DuckDBTypeUInteger,
+    pattern DuckDBTypeUBigInt,
+    pattern DuckDBTypeFloat,
+    pattern DuckDBTypeDouble,
+    pattern DuckDBTypeTimestamp,
+    pattern DuckDBTypeDate,
+    pattern DuckDBTypeTime,
+    pattern DuckDBTypeInterval,
+    pattern DuckDBTypeHugeInt,
+    pattern DuckDBTypeUHugeInt,
+    pattern DuckDBTypeVarchar,
+    pattern DuckDBTypeBlob,
+    pattern DuckDBTypeDecimal,
+    pattern DuckDBTypeTimestampS,
+    pattern DuckDBTypeTimestampMs,
+    pattern DuckDBTypeTimestampNs,
+    pattern DuckDBTypeEnum,
+    pattern DuckDBTypeList,
+    pattern DuckDBTypeStruct,
+    pattern DuckDBTypeMap,
+    pattern DuckDBTypeArray,
+    pattern DuckDBTypeUUID,
+    pattern DuckDBTypeUnion,
+    pattern DuckDBTypeBit,
+    pattern DuckDBTypeTimeTz,
+    pattern DuckDBTypeTimestampTz,
+    pattern DuckDBTypeAny,
+    pattern DuckDBTypeBigNum,
+    pattern DuckDBTypeSQLNull,
+    pattern DuckDBTypeStringLiteral,
+    pattern DuckDBTypeIntegerLiteral,
+    pattern DuckDBTypeTimeNs,
+    DuckDBPendingState (..),
+    pattern DuckDBPendingResultReady,
+    pattern DuckDBPendingResultNotReady,
+    pattern DuckDBPendingError,
+    pattern DuckDBPendingNoTasksAvailable,
+    DuckDBResultType (..),
+    pattern DuckDBResultTypeInvalid,
+    pattern DuckDBResultTypeChangedRows,
+    pattern DuckDBResultTypeNothing,
+    pattern DuckDBResultTypeQueryResult,
+    DuckDBStatementType (..),
+    pattern DuckDBStatementTypeInvalid,
+    pattern DuckDBStatementTypeSelect,
+    pattern DuckDBStatementTypeInsert,
+    pattern DuckDBStatementTypeUpdate,
+    pattern DuckDBStatementTypeExplain,
+    pattern DuckDBStatementTypeDelete,
+    pattern DuckDBStatementTypePrepare,
+    pattern DuckDBStatementTypeCreate,
+    pattern DuckDBStatementTypeExecute,
+    pattern DuckDBStatementTypeAlter,
+    pattern DuckDBStatementTypeTransaction,
+    pattern DuckDBStatementTypeCopy,
+    pattern DuckDBStatementTypeAnalyze,
+    pattern DuckDBStatementTypeVariableSet,
+    pattern DuckDBStatementTypeCreateFunc,
+    pattern DuckDBStatementTypeDrop,
+    pattern DuckDBStatementTypeExport,
+    pattern DuckDBStatementTypePragma,
+    pattern DuckDBStatementTypeVacuum,
+    pattern DuckDBStatementTypeCall,
+    pattern DuckDBStatementTypeSet,
+    pattern DuckDBStatementTypeLoad,
+    pattern DuckDBStatementTypeRelation,
+    pattern DuckDBStatementTypeExtension,
+    pattern DuckDBStatementTypeLogicalPlan,
+    pattern DuckDBStatementTypeAttach,
+    pattern DuckDBStatementTypeDetach,
+    pattern DuckDBStatementTypeMulti,
+    DuckDBErrorType (..),
+    pattern DuckDBErrorInvalid,
+    pattern DuckDBErrorOutOfRange,
+    pattern DuckDBErrorConversion,
+    pattern DuckDBErrorUnknownType,
+    pattern DuckDBErrorDecimal,
+    pattern DuckDBErrorMismatchType,
+    pattern DuckDBErrorDivideByZero,
+    pattern DuckDBErrorObjectSize,
+    pattern DuckDBErrorInvalidType,
+    pattern DuckDBErrorSerialization,
+    pattern DuckDBErrorTransaction,
+    pattern DuckDBErrorNotImplemented,
+    pattern DuckDBErrorExpression,
+    pattern DuckDBErrorCatalog,
+    pattern DuckDBErrorParser,
+    pattern DuckDBErrorPlanner,
+    pattern DuckDBErrorScheduler,
+    pattern DuckDBErrorExecutor,
+    pattern DuckDBErrorConstraint,
+    pattern DuckDBErrorIndex,
+    pattern DuckDBErrorStat,
+    pattern DuckDBErrorConnection,
+    pattern DuckDBErrorSyntax,
+    pattern DuckDBErrorSettings,
+    pattern DuckDBErrorBinder,
+    pattern DuckDBErrorNetwork,
+    pattern DuckDBErrorOptimizer,
+    pattern DuckDBErrorNullPointer,
+    pattern DuckDBErrorIO,
+    pattern DuckDBErrorInterrupt,
+    pattern DuckDBErrorFatal,
+    pattern DuckDBErrorInternal,
+    pattern DuckDBErrorInvalidInput,
+    pattern DuckDBErrorOutOfMemory,
+    pattern DuckDBErrorPermission,
+    pattern DuckDBErrorParameterNotResolved,
+    pattern DuckDBErrorParameterNotAllowed,
+    pattern DuckDBErrorDependency,
+    pattern DuckDBErrorHTTP,
+    pattern DuckDBErrorMissingExtension,
+    pattern DuckDBErrorAutoload,
+    pattern DuckDBErrorSequence,
+    pattern DuckDBInvalidConfiguration,
+    pattern DuckDBErrorInvalidConfiguration,
+    DuckDBCastMode (..),
+    pattern DuckDBCastNormal,
+    pattern DuckDBCastTry,
+
+    -- * Scalar Types
+    DuckDBIdx,
+    DuckDBSel,
+    DuckDBDate (..),
+    DuckDBDateStruct (..),
+    DuckDBTime (..),
+    DuckDBTimeStruct (..),
+    DuckDBTimeNs (..),
+    DuckDBTimeTz (..),
+    DuckDBTimeTzStruct (..),
+    DuckDBTimestamp (..),
+    DuckDBTimestampStruct (..),
+    DuckDBTimestampS (..),
+    DuckDBTimestampMs (..),
+    DuckDBTimestampNs (..),
+    DuckDBInterval (..),
+    DuckDBHugeInt (..),
+    DuckDBUHugeInt (..),
+    DuckDBDecimal (..),
+    DuckDBBlob (..),
+    DuckDBString (..),
+    DuckDBStringT,
+    DuckDBBit (..),
+    DuckDBBignum (..),
+    DuckDBQueryProgress (..),
+
+    -- * Result Structures
+    DuckDBResult (..),
+    DuckDBColumn,
+
+    -- * Opaque Pointer Types
+    DuckDBDatabase,
+    DuckDBConnection,
+    DuckDBConfig,
+    DuckDBInstanceCache,
+    DuckDBArrowOptions,
+    DuckDBArrow,
+    DuckDBArrowSchema,
+    DuckDBArrowArray,
+    ArrowSchemaPtr (..),
+    ArrowArrayPtr (..),
+    ArrowStreamPtr (..),
+    DuckDBArrowConvertedSchema,
+    DuckDBArrowStream,
+    DuckDBPreparedStatement,
+    DuckDBPendingResult,
+    DuckDBExtractedStatements,
+    DuckDBLogicalType,
+    DuckDBCreateTypeInfo,
+    DuckDBVector,
+    DuckDBDataChunk,
+    DuckDBSelectionVector,
+    DuckDBFunctionInfo,
+    DuckDBBindInfo,
+    DuckDBInitInfo,
+    DuckDBScalarFunction,
+    DuckDBScalarFunctionSet,
+    DuckDBAggregateFunction,
+    DuckDBAggregateFunctionSet,
+    DuckDBAggregateState,
+    DuckDBCastFunction,
+    DuckDBExpression,
+    DuckDBClientContext,
+    DuckDBTableFunction,
+    DuckDBValue,
+    DuckDBErrorData,
+    DuckDBAppender,
+    DuckDBTableDescription,
+    DuckDBProfilingInfo,
+    DuckDBReplacementScanInfo,
+    DuckDBTaskState,
+    ArrowArray (..),
+    ArrowSchema (..),
+
+    -- * Opaque Struct Tags
+    DuckDBDatabaseStruct,
+    DuckDBConnectionStruct,
+    DuckDBConfigStruct,
+    DuckDBInstanceCacheStruct,
+    DuckDBExtractedStatementsStruct,
+    DuckDBFunctionInfoStruct,
+    DuckDBBindInfoStruct,
+    DuckDBScalarFunctionStruct,
+    DuckDBScalarFunctionSetStruct,
+    DuckDBAggregateFunctionStruct,
+    DuckDBAggregateFunctionSetStruct,
+    DuckDBVectorStruct,
+    DuckDBDataChunkStruct,
+    DuckDBSelectionVectorStruct,
+    DuckDBArrowOptionsStruct,
+    DuckDBArrowStruct,
+    DuckDBArrowConvertedSchemaStruct,
+    DuckDBArrowStreamStruct,
+    DuckDBExpressionStruct,
+    DuckDBClientContextStruct,
+    DuckDBPreparedStatementStruct,
+    DuckDBValueStruct,
+    DuckDBPendingResultStruct,
+    DuckDBLogicalTypeStruct,
+    DuckDBCreateTypeInfoStruct,
+    DuckDBErrorDataStruct,
+    DuckDBInitInfoStruct,
+    DuckDBCastFunctionStruct,
+    DuckDBTableFunctionStruct,
+    DuckDBAppenderStruct,
+    DuckDBTableDescriptionStruct,
+    DuckDBProfilingInfoStruct,
+    DuckDBReplacementScanInfoStruct,
+    DuckDBAggregateStateStruct,
+
+    -- * Function Pointer Types
+    DuckDBScalarFunctionFun,
+    DuckDBScalarFunctionBindFun,
+    DuckDBDeleteCallback,
+    DuckDBCopyCallback,
+    DuckDBCastFunctionFun,
+    DuckDBAggregateStateSizeFun,
+    DuckDBAggregateInitFun,
+    DuckDBAggregateDestroyFun,
+    DuckDBAggregateUpdateFun,
+    DuckDBAggregateCombineFun,
+    DuckDBAggregateFinalizeFun,
+    DuckDBTableFunctionBindFun,
+    DuckDBTableFunctionInitFun,
+    DuckDBTableFunctionFun,
+    DuckDBReplacementCallback,
+) where
+
+import Data.Int (Int32, Int64, Int8)
+import Data.Word (Word32, Word64, Word8)
+import Foreign.C.String (CString)
+import Foreign.C.Types
+import Foreign.Ptr (FunPtr, Ptr, nullPtr)
+import Foreign.Storable (Storable (..), peekByteOff, pokeByteOff)
+
+-- | Unsigned index type used by DuckDB (mirrors @idx_t@).
+type DuckDBIdx = Word64
+
+-- | Selection vector entry type (mirrors @sel_t@).
+type DuckDBSel = Word32
+
+-- | Result state returned by most DuckDB C API calls.
+newtype DuckDBState = DuckDBState {unDuckDBState :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for @duckdb_state@ constants.
+pattern DuckDBSuccess, DuckDBError :: DuckDBState
+pattern DuckDBSuccess = DuckDBState 0
+pattern DuckDBError = DuckDBState 1
+
+{-# COMPLETE DuckDBSuccess, DuckDBError #-}
+
+-- | DuckDB primitive physical type identifiers (mirrors @duckdb_type@).
+newtype DuckDBType = DuckDBType {unDuckDBType :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for DuckDB's physical value type tags.
+pattern
+    DuckDBTypeInvalid
+    , DuckDBTypeBoolean
+    , DuckDBTypeTinyInt
+    , DuckDBTypeSmallInt
+    , DuckDBTypeInteger
+    , DuckDBTypeBigInt
+    , DuckDBTypeUTinyInt
+    , DuckDBTypeUSmallInt
+    , DuckDBTypeUInteger
+    , DuckDBTypeUBigInt
+    , DuckDBTypeFloat
+    , DuckDBTypeDouble
+    , DuckDBTypeTimestamp
+    , DuckDBTypeDate
+    , DuckDBTypeTime
+    , DuckDBTypeInterval
+    , DuckDBTypeHugeInt
+    , DuckDBTypeUHugeInt
+    , DuckDBTypeVarchar
+    , DuckDBTypeBlob
+    , DuckDBTypeDecimal
+    , DuckDBTypeTimestampS
+    , DuckDBTypeTimestampMs
+    , DuckDBTypeTimestampNs
+    , DuckDBTypeEnum
+    , DuckDBTypeList
+    , DuckDBTypeStruct
+    , DuckDBTypeMap
+    , DuckDBTypeArray
+    , DuckDBTypeUUID
+    , DuckDBTypeUnion
+    , DuckDBTypeBit
+    , DuckDBTypeTimeTz
+    , DuckDBTypeTimestampTz
+    , DuckDBTypeAny
+    , DuckDBTypeBigNum
+    , DuckDBTypeSQLNull
+    , DuckDBTypeStringLiteral
+    , DuckDBTypeIntegerLiteral
+    , DuckDBTypeTimeNs ::
+        DuckDBType
+pattern DuckDBTypeInvalid = DuckDBType 0
+pattern DuckDBTypeBoolean = DuckDBType 1
+pattern DuckDBTypeTinyInt = DuckDBType 2
+pattern DuckDBTypeSmallInt = DuckDBType 3
+pattern DuckDBTypeInteger = DuckDBType 4
+pattern DuckDBTypeBigInt = DuckDBType 5
+pattern DuckDBTypeUTinyInt = DuckDBType 6
+pattern DuckDBTypeUSmallInt = DuckDBType 7
+pattern DuckDBTypeUInteger = DuckDBType 8
+pattern DuckDBTypeUBigInt = DuckDBType 9
+pattern DuckDBTypeFloat = DuckDBType 10
+pattern DuckDBTypeDouble = DuckDBType 11
+pattern DuckDBTypeTimestamp = DuckDBType 12
+pattern DuckDBTypeDate = DuckDBType 13
+pattern DuckDBTypeTime = DuckDBType 14
+pattern DuckDBTypeInterval = DuckDBType 15
+pattern DuckDBTypeHugeInt = DuckDBType 16
+pattern DuckDBTypeUHugeInt = DuckDBType 32
+pattern DuckDBTypeVarchar = DuckDBType 17
+pattern DuckDBTypeBlob = DuckDBType 18
+pattern DuckDBTypeDecimal = DuckDBType 19
+pattern DuckDBTypeTimestampS = DuckDBType 20
+pattern DuckDBTypeTimestampMs = DuckDBType 21
+pattern DuckDBTypeTimestampNs = DuckDBType 22
+pattern DuckDBTypeEnum = DuckDBType 23
+pattern DuckDBTypeList = DuckDBType 24
+pattern DuckDBTypeStruct = DuckDBType 25
+pattern DuckDBTypeMap = DuckDBType 26
+pattern DuckDBTypeArray = DuckDBType 33
+pattern DuckDBTypeUUID = DuckDBType 27
+pattern DuckDBTypeUnion = DuckDBType 28
+pattern DuckDBTypeBit = DuckDBType 29
+pattern DuckDBTypeTimeTz = DuckDBType 30
+pattern DuckDBTypeTimestampTz = DuckDBType 31
+pattern DuckDBTypeAny = DuckDBType 34
+pattern DuckDBTypeBigNum = DuckDBType 35
+pattern DuckDBTypeSQLNull = DuckDBType 36
+pattern DuckDBTypeStringLiteral = DuckDBType 37
+pattern DuckDBTypeIntegerLiteral = DuckDBType 38
+pattern DuckDBTypeTimeNs = DuckDBType 39
+
+-- | Pending result state returned from @duckdb_pending_*@ APIs.
+newtype DuckDBPendingState = DuckDBPendingState {unDuckDBPendingState :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for @duckdb_pending_state@ constants.
+pattern
+    DuckDBPendingResultReady
+    , DuckDBPendingResultNotReady
+    , DuckDBPendingError
+    , DuckDBPendingNoTasksAvailable ::
+        DuckDBPendingState
+pattern DuckDBPendingResultReady = DuckDBPendingState 0
+pattern DuckDBPendingResultNotReady = DuckDBPendingState 1
+pattern DuckDBPendingError = DuckDBPendingState 2
+pattern DuckDBPendingNoTasksAvailable = DuckDBPendingState 3
+
+{-# COMPLETE
+    DuckDBPendingResultReady
+    , DuckDBPendingResultNotReady
+    , DuckDBPendingError
+    , DuckDBPendingNoTasksAvailable
+    #-}
+
+-- | Result payload type returned by DuckDB queries (@duckdb_result_type@).
+newtype DuckDBResultType = DuckDBResultType {unDuckDBResultType :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for @duckdb_result_type@ constants.
+pattern
+    DuckDBResultTypeInvalid
+    , DuckDBResultTypeChangedRows
+    , DuckDBResultTypeNothing
+    , DuckDBResultTypeQueryResult ::
+        DuckDBResultType
+pattern DuckDBResultTypeInvalid = DuckDBResultType 0
+pattern DuckDBResultTypeChangedRows = DuckDBResultType 1
+pattern DuckDBResultTypeNothing = DuckDBResultType 2
+pattern DuckDBResultTypeQueryResult = DuckDBResultType 3
+
+{-# COMPLETE
+    DuckDBResultTypeInvalid
+    , DuckDBResultTypeChangedRows
+    , DuckDBResultTypeNothing
+    , DuckDBResultTypeQueryResult
+    #-}
+
+-- | Classifies the SQL statement executed (@duckdb_statement_type@).
+newtype DuckDBStatementType = DuckDBStatementType {unDuckDBStatementType :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for @duckdb_statement_type@ constants.
+pattern
+    DuckDBStatementTypeInvalid
+    , DuckDBStatementTypeSelect
+    , DuckDBStatementTypeInsert
+    , DuckDBStatementTypeUpdate
+    , DuckDBStatementTypeExplain
+    , DuckDBStatementTypeDelete
+    , DuckDBStatementTypePrepare
+    , DuckDBStatementTypeCreate
+    , DuckDBStatementTypeExecute
+    , DuckDBStatementTypeAlter
+    , DuckDBStatementTypeTransaction
+    , DuckDBStatementTypeCopy
+    , DuckDBStatementTypeAnalyze
+    , DuckDBStatementTypeVariableSet
+    , DuckDBStatementTypeCreateFunc
+    , DuckDBStatementTypeDrop
+    , DuckDBStatementTypeExport
+    , DuckDBStatementTypePragma
+    , DuckDBStatementTypeVacuum
+    , DuckDBStatementTypeCall
+    , DuckDBStatementTypeSet
+    , DuckDBStatementTypeLoad
+    , DuckDBStatementTypeRelation
+    , DuckDBStatementTypeExtension
+    , DuckDBStatementTypeLogicalPlan
+    , DuckDBStatementTypeAttach
+    , DuckDBStatementTypeDetach
+    , DuckDBStatementTypeMulti ::
+        DuckDBStatementType
+pattern DuckDBStatementTypeInvalid = DuckDBStatementType 0
+pattern DuckDBStatementTypeSelect = DuckDBStatementType 1
+pattern DuckDBStatementTypeInsert = DuckDBStatementType 2
+pattern DuckDBStatementTypeUpdate = DuckDBStatementType 3
+pattern DuckDBStatementTypeExplain = DuckDBStatementType 4
+pattern DuckDBStatementTypeDelete = DuckDBStatementType 5
+pattern DuckDBStatementTypePrepare = DuckDBStatementType 6
+pattern DuckDBStatementTypeCreate = DuckDBStatementType 7
+pattern DuckDBStatementTypeExecute = DuckDBStatementType 8
+pattern DuckDBStatementTypeAlter = DuckDBStatementType 9
+pattern DuckDBStatementTypeTransaction = DuckDBStatementType 10
+pattern DuckDBStatementTypeCopy = DuckDBStatementType 11
+pattern DuckDBStatementTypeAnalyze = DuckDBStatementType 12
+pattern DuckDBStatementTypeVariableSet = DuckDBStatementType 13
+pattern DuckDBStatementTypeCreateFunc = DuckDBStatementType 14
+pattern DuckDBStatementTypeDrop = DuckDBStatementType 15
+pattern DuckDBStatementTypeExport = DuckDBStatementType 16
+pattern DuckDBStatementTypePragma = DuckDBStatementType 17
+pattern DuckDBStatementTypeVacuum = DuckDBStatementType 18
+pattern DuckDBStatementTypeCall = DuckDBStatementType 19
+pattern DuckDBStatementTypeSet = DuckDBStatementType 20
+pattern DuckDBStatementTypeLoad = DuckDBStatementType 21
+pattern DuckDBStatementTypeRelation = DuckDBStatementType 22
+pattern DuckDBStatementTypeExtension = DuckDBStatementType 23
+pattern DuckDBStatementTypeLogicalPlan = DuckDBStatementType 24
+pattern DuckDBStatementTypeAttach = DuckDBStatementType 25
+pattern DuckDBStatementTypeDetach = DuckDBStatementType 26
+pattern DuckDBStatementTypeMulti = DuckDBStatementType 27
+
+{-# COMPLETE
+    DuckDBStatementTypeInvalid
+    , DuckDBStatementTypeSelect
+    , DuckDBStatementTypeInsert
+    , DuckDBStatementTypeUpdate
+    , DuckDBStatementTypeExplain
+    , DuckDBStatementTypeDelete
+    , DuckDBStatementTypePrepare
+    , DuckDBStatementTypeCreate
+    , DuckDBStatementTypeExecute
+    , DuckDBStatementTypeAlter
+    , DuckDBStatementTypeTransaction
+    , DuckDBStatementTypeCopy
+    , DuckDBStatementTypeAnalyze
+    , DuckDBStatementTypeVariableSet
+    , DuckDBStatementTypeCreateFunc
+    , DuckDBStatementTypeDrop
+    , DuckDBStatementTypeExport
+    , DuckDBStatementTypePragma
+    , DuckDBStatementTypeVacuum
+    , DuckDBStatementTypeCall
+    , DuckDBStatementTypeSet
+    , DuckDBStatementTypeLoad
+    , DuckDBStatementTypeRelation
+    , DuckDBStatementTypeExtension
+    , DuckDBStatementTypeLogicalPlan
+    , DuckDBStatementTypeAttach
+    , DuckDBStatementTypeDetach
+    , DuckDBStatementTypeMulti
+    #-}
+
+-- | DuckDB error classification codes.
+newtype DuckDBErrorType = DuckDBErrorType {unDuckDBErrorType :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms mirroring @duckdb_error_type@ values.
+pattern
+    DuckDBErrorInvalid
+    , DuckDBErrorOutOfRange
+    , DuckDBErrorConversion
+    , DuckDBErrorUnknownType
+    , DuckDBErrorDecimal
+    , DuckDBErrorMismatchType
+    , DuckDBErrorDivideByZero
+    , DuckDBErrorObjectSize
+    , DuckDBErrorInvalidType
+    , DuckDBErrorSerialization
+    , DuckDBErrorTransaction
+    , DuckDBErrorNotImplemented
+    , DuckDBErrorExpression
+    , DuckDBErrorCatalog
+    , DuckDBErrorParser
+    , DuckDBErrorPlanner
+    , DuckDBErrorScheduler
+    , DuckDBErrorExecutor
+    , DuckDBErrorConstraint
+    , DuckDBErrorIndex
+    , DuckDBErrorStat
+    , DuckDBErrorConnection
+    , DuckDBErrorSyntax
+    , DuckDBErrorSettings
+    , DuckDBErrorBinder
+    , DuckDBErrorNetwork
+    , DuckDBErrorOptimizer
+    , DuckDBErrorNullPointer
+    , DuckDBErrorIO
+    , DuckDBErrorInterrupt
+    , DuckDBErrorFatal
+    , DuckDBErrorInternal
+    , DuckDBErrorInvalidInput
+    , DuckDBErrorOutOfMemory
+    , DuckDBErrorPermission
+    , DuckDBErrorParameterNotResolved
+    , DuckDBErrorParameterNotAllowed
+    , DuckDBErrorDependency
+    , DuckDBErrorHTTP
+    , DuckDBErrorMissingExtension
+    , DuckDBErrorAutoload
+    , DuckDBErrorSequence
+    , DuckDBInvalidConfiguration ::
+        DuckDBErrorType
+pattern DuckDBErrorInvalid = DuckDBErrorType 0
+pattern DuckDBErrorOutOfRange = DuckDBErrorType 1
+pattern DuckDBErrorConversion = DuckDBErrorType 2
+pattern DuckDBErrorUnknownType = DuckDBErrorType 3
+pattern DuckDBErrorDecimal = DuckDBErrorType 4
+pattern DuckDBErrorMismatchType = DuckDBErrorType 5
+pattern DuckDBErrorDivideByZero = DuckDBErrorType 6
+pattern DuckDBErrorObjectSize = DuckDBErrorType 7
+pattern DuckDBErrorInvalidType = DuckDBErrorType 8
+pattern DuckDBErrorSerialization = DuckDBErrorType 9
+pattern DuckDBErrorTransaction = DuckDBErrorType 10
+pattern DuckDBErrorNotImplemented = DuckDBErrorType 11
+pattern DuckDBErrorExpression = DuckDBErrorType 12
+pattern DuckDBErrorCatalog = DuckDBErrorType 13
+pattern DuckDBErrorParser = DuckDBErrorType 14
+pattern DuckDBErrorPlanner = DuckDBErrorType 15
+pattern DuckDBErrorScheduler = DuckDBErrorType 16
+pattern DuckDBErrorExecutor = DuckDBErrorType 17
+pattern DuckDBErrorConstraint = DuckDBErrorType 18
+pattern DuckDBErrorIndex = DuckDBErrorType 19
+pattern DuckDBErrorStat = DuckDBErrorType 20
+pattern DuckDBErrorConnection = DuckDBErrorType 21
+pattern DuckDBErrorSyntax = DuckDBErrorType 22
+pattern DuckDBErrorSettings = DuckDBErrorType 23
+pattern DuckDBErrorBinder = DuckDBErrorType 24
+pattern DuckDBErrorNetwork = DuckDBErrorType 25
+pattern DuckDBErrorOptimizer = DuckDBErrorType 26
+pattern DuckDBErrorNullPointer = DuckDBErrorType 27
+pattern DuckDBErrorIO = DuckDBErrorType 28
+pattern DuckDBErrorInterrupt = DuckDBErrorType 29
+pattern DuckDBErrorFatal = DuckDBErrorType 30
+pattern DuckDBErrorInternal = DuckDBErrorType 31
+pattern DuckDBErrorInvalidInput = DuckDBErrorType 32
+pattern DuckDBErrorOutOfMemory = DuckDBErrorType 33
+pattern DuckDBErrorPermission = DuckDBErrorType 34
+pattern DuckDBErrorParameterNotResolved = DuckDBErrorType 35
+pattern DuckDBErrorParameterNotAllowed = DuckDBErrorType 36
+pattern DuckDBErrorDependency = DuckDBErrorType 37
+pattern DuckDBErrorHTTP = DuckDBErrorType 38
+pattern DuckDBErrorMissingExtension = DuckDBErrorType 39
+pattern DuckDBErrorAutoload = DuckDBErrorType 40
+pattern DuckDBErrorSequence = DuckDBErrorType 41
+pattern DuckDBInvalidConfiguration = DuckDBErrorType 42
+
+-- | Backwards-compatible alias for 'DuckDBInvalidConfiguration'.
+{-# DEPRECATED DuckDBErrorInvalidConfiguration "Use DuckDBInvalidConfiguration (matches upstream duckdb.h)" #-}
+pattern DuckDBErrorInvalidConfiguration :: DuckDBErrorType
+pattern DuckDBErrorInvalidConfiguration = DuckDBInvalidConfiguration
+
+{-# COMPLETE
+    DuckDBErrorInvalid
+    , DuckDBErrorOutOfRange
+    , DuckDBErrorConversion
+    , DuckDBErrorUnknownType
+    , DuckDBErrorDecimal
+    , DuckDBErrorMismatchType
+    , DuckDBErrorDivideByZero
+    , DuckDBErrorObjectSize
+    , DuckDBErrorInvalidType
+    , DuckDBErrorSerialization
+    , DuckDBErrorTransaction
+    , DuckDBErrorNotImplemented
+    , DuckDBErrorExpression
+    , DuckDBErrorCatalog
+    , DuckDBErrorParser
+    , DuckDBErrorPlanner
+    , DuckDBErrorScheduler
+    , DuckDBErrorExecutor
+    , DuckDBErrorConstraint
+    , DuckDBErrorIndex
+    , DuckDBErrorStat
+    , DuckDBErrorConnection
+    , DuckDBErrorSyntax
+    , DuckDBErrorSettings
+    , DuckDBErrorBinder
+    , DuckDBErrorNetwork
+    , DuckDBErrorOptimizer
+    , DuckDBErrorNullPointer
+    , DuckDBErrorIO
+    , DuckDBErrorInterrupt
+    , DuckDBErrorFatal
+    , DuckDBErrorInternal
+    , DuckDBErrorInvalidInput
+    , DuckDBErrorOutOfMemory
+    , DuckDBErrorPermission
+    , DuckDBErrorParameterNotResolved
+    , DuckDBErrorParameterNotAllowed
+    , DuckDBErrorDependency
+    , DuckDBErrorHTTP
+    , DuckDBErrorMissingExtension
+    , DuckDBErrorAutoload
+    , DuckDBErrorSequence
+    , DuckDBInvalidConfiguration
+    #-}
+
+-- | Behaviour of DuckDB's casting functions (@duckdb_cast_mode@).
+newtype DuckDBCastMode = DuckDBCastMode {unDuckDBCastMode :: CInt}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Pattern synonyms for @duckdb_cast_mode@ values.
+pattern DuckDBCastNormal, DuckDBCastTry :: DuckDBCastMode
+pattern DuckDBCastNormal = DuckDBCastMode 0
+pattern DuckDBCastTry = DuckDBCastMode 1
+
+{-# COMPLETE DuckDBCastNormal, DuckDBCastTry #-}
+
+-- | Represents DuckDB's @duckdb_date@.
+newtype DuckDBDate = DuckDBDate {unDuckDBDate :: Int32}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Decomposed representation of a @duckdb_date@.
+data DuckDBDateStruct = DuckDBDateStruct
+    { duckDBDateStructYear :: !Int32
+    , duckDBDateStructMonth :: !Int8
+    , duckDBDateStructDay :: !Int8
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBDateStruct where
+    sizeOf _ = alignedSize
+      where
+        rawSize = sizeOf (undefined :: Int32) + 2 * sizeOf (undefined :: Int8)
+        align = alignment (undefined :: Int32)
+        alignedSize = ((rawSize + align - 1) `div` align) * align
+    alignment _ = alignment (undefined :: Int32)
+    peek ptr = do
+        year <- peekByteOff ptr 0
+        month <- peekByteOff ptr (sizeOf (undefined :: Int32))
+        day <- peekByteOff ptr (sizeOf (undefined :: Int32) + sizeOf (undefined :: Int8))
+        pure (DuckDBDateStruct year month day)
+    poke ptr DuckDBDateStruct{duckDBDateStructYear = year, duckDBDateStructMonth = month, duckDBDateStructDay = day} = do
+        pokeByteOff ptr 0 year
+        pokeByteOff ptr (sizeOf (undefined :: Int32)) month
+        pokeByteOff ptr (sizeOf (undefined :: Int32) + sizeOf (undefined :: Int8)) day
+
+-- | Represents DuckDB's @duckdb_time@.
+newtype DuckDBTime = DuckDBTime {unDuckDBTime :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Decomposed representation of a @duckdb_time@.
+data DuckDBTimeStruct = DuckDBTimeStruct
+    { duckDBTimeStructHour :: !Int8
+    , duckDBTimeStructMinute :: !Int8
+    , duckDBTimeStructSecond :: !Int8
+    , duckDBTimeStructMicros :: !Int32
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBTimeStruct where
+    sizeOf _ = 2 * alignment (undefined :: Int32)
+    alignment _ = alignment (undefined :: Int32)
+    peek ptr = do
+        hour <- peekByteOff ptr 0
+        minute <- peekByteOff ptr 1
+        second <- peekByteOff ptr 2
+        micros <- peekByteOff ptr 4
+        pure (DuckDBTimeStruct hour minute second micros)
+    poke ptr DuckDBTimeStruct{duckDBTimeStructHour = hour, duckDBTimeStructMinute = minute, duckDBTimeStructSecond = second, duckDBTimeStructMicros = micros} = do
+        pokeByteOff ptr 0 hour
+        pokeByteOff ptr 1 minute
+        pokeByteOff ptr 2 second
+        pokeByteOff ptr 4 micros
+
+-- | Represents DuckDB's @duckdb_time_ns@.
+newtype DuckDBTimeNs = DuckDBTimeNs {unDuckDBTimeNs :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Represents DuckDB's @duckdb_time_tz@.
+newtype DuckDBTimeTz = DuckDBTimeTz {unDuckDBTimeTz :: Word64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Decomposed representation of a @duckdb_time_tz@.
+data DuckDBTimeTzStruct = DuckDBTimeTzStruct
+    { duckDBTimeTzStructTime :: !DuckDBTimeStruct
+    , duckDBTimeTzStructOffset :: !Int32
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBTimeTzStruct where
+    sizeOf _ = sizeOf (undefined :: DuckDBTimeStruct) + sizeOf (undefined :: Int32)
+    alignment _ = alignment (undefined :: Int32)
+    peek ptr = do
+        time <- peekByteOff ptr 0
+        offset <- peekByteOff ptr (sizeOf (undefined :: DuckDBTimeStruct))
+        pure (DuckDBTimeTzStruct time offset)
+    poke ptr DuckDBTimeTzStruct{duckDBTimeTzStructTime = time, duckDBTimeTzStructOffset = offset} = do
+        pokeByteOff ptr 0 time
+        pokeByteOff ptr (sizeOf (undefined :: DuckDBTimeStruct)) offset
+
+-- | Represents DuckDB's @duckdb_timestamp@.
+newtype DuckDBTimestamp = DuckDBTimestamp {unDuckDBTimestamp :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Decomposed representation of a @duckdb_timestamp@.
+data DuckDBTimestampStruct = DuckDBTimestampStruct
+    { duckDBTimestampStructDate :: !DuckDBDateStruct
+    , duckDBTimestampStructTime :: !DuckDBTimeStruct
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBTimestampStruct where
+    sizeOf _ = sizeOf (undefined :: DuckDBDateStruct) + sizeOf (undefined :: DuckDBTimeStruct)
+    alignment _ = alignment (undefined :: DuckDBTimeStruct)
+    peek ptr = do
+        date <- peekByteOff ptr 0
+        time <- peekByteOff ptr (sizeOf (undefined :: DuckDBDateStruct))
+        pure (DuckDBTimestampStruct date time)
+    poke ptr DuckDBTimestampStruct{duckDBTimestampStructDate = date, duckDBTimestampStructTime = time} = do
+        pokeByteOff ptr 0 date
+        pokeByteOff ptr (sizeOf (undefined :: DuckDBDateStruct)) time
+
+-- | Represents DuckDB's @duckdb_timestamp_s@.
+newtype DuckDBTimestampS = DuckDBTimestampS {unDuckDBTimestampS :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Represents DuckDB's @duckdb_timestamp_ms@.
+newtype DuckDBTimestampMs = DuckDBTimestampMs {unDuckDBTimestampMs :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Represents DuckDB's @duckdb_timestamp_ns@.
+newtype DuckDBTimestampNs = DuckDBTimestampNs {unDuckDBTimestampNs :: Int64}
+    deriving (Eq, Ord, Show, Storable)
+
+-- | Represents DuckDB's @duckdb_interval@.
+data DuckDBInterval = DuckDBInterval
+    { duckDBIntervalMonths :: !Int32
+    , duckDBIntervalDays :: !Int32
+    , duckDBIntervalMicros :: !Int64
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBInterval where
+    sizeOf _ = 2 * sizeOf (undefined :: Int32) + sizeOf (undefined :: Int64)
+    alignment _ = alignment (undefined :: Int64)
+    peek ptr = do
+        months <- peekByteOff ptr 0
+        days <- peekByteOff ptr (sizeOf (undefined :: Int32))
+        micros <- peekByteOff ptr (2 * sizeOf (undefined :: Int32))
+        pure (DuckDBInterval months days micros)
+    poke ptr (DuckDBInterval months days micros) = do
+        pokeByteOff ptr 0 months
+        pokeByteOff ptr (sizeOf (undefined :: Int32)) days
+        pokeByteOff ptr (2 * sizeOf (undefined :: Int32)) micros
+
+-- | Represents DuckDB's @duckdb_hugeint@.
+data DuckDBHugeInt = DuckDBHugeInt
+    { duckDBHugeIntLower :: !Word64
+    , duckDBHugeIntUpper :: !Int64
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBHugeInt where
+    sizeOf _ = sizeOf (undefined :: Word64) + sizeOf (undefined :: Int64)
+    alignment _ = alignment (undefined :: Word64)
+    peek ptr = do
+        lower <- peekByteOff ptr 0
+        upper <- peekByteOff ptr (sizeOf (undefined :: Word64))
+        pure (DuckDBHugeInt lower upper)
+    poke ptr (DuckDBHugeInt lower upper) = do
+        pokeByteOff ptr 0 lower
+        pokeByteOff ptr (sizeOf (undefined :: Word64)) upper
+
+-- | Represents DuckDB's @duckdb_uhugeint@.
+data DuckDBUHugeInt = DuckDBUHugeInt
+    { duckDBUHugeIntLower :: !Word64
+    , duckDBUHugeIntUpper :: !Word64
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBUHugeInt where
+    sizeOf _ = 2 * sizeOf (undefined :: Word64)
+    alignment _ = alignment (undefined :: Word64)
+    peek ptr = do
+        lower <- peekByteOff ptr 0
+        upper <- peekByteOff ptr (sizeOf (undefined :: Word64))
+        pure (DuckDBUHugeInt lower upper)
+    poke ptr (DuckDBUHugeInt lower upper) = do
+        pokeByteOff ptr 0 lower
+        pokeByteOff ptr (sizeOf (undefined :: Word64)) upper
+
+-- | Represents DuckDB's @duckdb_decimal@.
+data DuckDBDecimal = DuckDBDecimal
+    { duckDBDecimalWidth :: !Word8
+    , duckDBDecimalScale :: !Word8
+    , duckDBDecimalValue :: !DuckDBHugeInt
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBDecimal where
+    sizeOf _ = valueOffset + sizeOf (undefined :: DuckDBHugeInt)
+      where
+        alignHuge = alignment (undefined :: DuckDBHugeInt)
+        valueOffset = ((2 + alignHuge - 1) `div` alignHuge) * alignHuge
+    alignment _ = alignment (undefined :: DuckDBHugeInt)
+    peek ptr = do
+        width <- peekByteOff ptr 0
+        scale <- peekByteOff ptr 1
+        let alignHuge = alignment (undefined :: DuckDBHugeInt)
+            valueOffset = ((2 + alignHuge - 1) `div` alignHuge) * alignHuge
+        value <- peekByteOff ptr valueOffset
+        pure (DuckDBDecimal width scale value)
+    poke ptr (DuckDBDecimal width scale value) = do
+        pokeByteOff ptr 0 width
+        pokeByteOff ptr 1 scale
+        let alignHuge = alignment (undefined :: DuckDBHugeInt)
+            valueOffset = ((2 + alignHuge - 1) `div` alignHuge) * alignHuge
+        pokeByteOff ptr valueOffset value
+
+-- | Represents DuckDB's @duckdb_blob@.
+data DuckDBBlob = DuckDBBlob
+    { duckDBBlobData :: !(Ptr ())
+    , duckDBBlobSize :: !DuckDBIdx
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBBlob where
+    sizeOf _ = sizeOf (undefined :: Ptr ()) + sizeOf (undefined :: DuckDBIdx)
+    alignment _ = alignment (undefined :: Ptr ())
+    peek ptr = do
+        dat <- peekByteOff ptr 0
+        len <- peekByteOff ptr (sizeOf (undefined :: Ptr ()))
+        pure (DuckDBBlob dat len)
+    poke ptr (DuckDBBlob dat len) = do
+        pokeByteOff ptr 0 dat
+        pokeByteOff ptr (sizeOf (undefined :: Ptr ())) len
+
+-- | Represents DuckDB's @duckdb_string@.
+data DuckDBString = DuckDBString
+    { duckDBStringData :: !(Ptr CChar)
+    , duckDBStringSize :: !DuckDBIdx
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBString where
+    sizeOf _ = sizeOf (undefined :: Ptr CChar) + sizeOf (undefined :: DuckDBIdx)
+    alignment _ = alignment (undefined :: Ptr CChar)
+    peek ptr = do
+        dat <- peekByteOff ptr 0
+        len <- peekByteOff ptr (sizeOf (undefined :: Ptr CChar))
+        pure (DuckDBString dat len)
+    poke ptr (DuckDBString dat len) = do
+        pokeByteOff ptr 0 dat
+        pokeByteOff ptr (sizeOf (undefined :: Ptr CChar)) len
+
+-- | Represents DuckDB's @duckdb_string_t@.
+data DuckDBStringT
+
+-- | Represents DuckDB's @duckdb_bit@.
+data DuckDBBit = DuckDBBit
+    { duckDBBitData :: !(Ptr Word8)
+    , duckDBBitSize :: !DuckDBIdx
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBBit where
+    sizeOf _ = sizeOf (undefined :: Ptr Word8) + sizeOf (undefined :: DuckDBIdx)
+    alignment _ = alignment (undefined :: Ptr Word8)
+    peek ptr = do
+        dat <- peekByteOff ptr 0
+        len <- peekByteOff ptr (sizeOf (undefined :: Ptr Word8))
+        pure (DuckDBBit dat len)
+    poke ptr (DuckDBBit dat len) = do
+        pokeByteOff ptr 0 dat
+        pokeByteOff ptr (sizeOf (undefined :: Ptr Word8)) len
+
+-- | Represents DuckDB's @duckdb_bignum@.
+data DuckDBBignum = DuckDBBignum
+    { duckDBBignumData :: !(Ptr Word8)
+    , duckDBBignumSize :: !DuckDBIdx
+    , duckDBBignumIsNegative :: !CBool
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBBignum where
+    sizeOf _ = alignedSize
+      where
+        baseSize = sizeOf (undefined :: Ptr Word8) + sizeOf (undefined :: DuckDBIdx) + sizeOf (undefined :: CBool)
+        align = alignment (undefined :: Ptr Word8)
+        alignedSize = ((baseSize + align - 1) `div` align) * align
+    alignment _ = alignment (undefined :: Ptr Word8)
+    peek ptr = do
+        dat <- peekByteOff ptr 0
+        len <- peekByteOff ptr (sizeOf (undefined :: Ptr Word8))
+        isNeg <- peekByteOff ptr (sizeOf (undefined :: Ptr Word8) + sizeOf (undefined :: DuckDBIdx))
+        pure (DuckDBBignum dat len isNeg)
+    poke ptr (DuckDBBignum dat len isNeg) = do
+        pokeByteOff ptr 0 dat
+        pokeByteOff ptr (sizeOf (undefined :: Ptr Word8)) len
+        pokeByteOff ptr (sizeOf (undefined :: Ptr Word8) + sizeOf (undefined :: DuckDBIdx)) isNeg
+
+-- | Represents DuckDB's @duckdb_query_progress_type@.
+data DuckDBQueryProgress = DuckDBQueryProgress
+    { duckDBQueryProgressPercentage :: !Double
+    , duckDBQueryProgressRowsProcessed :: !Word64
+    , duckDBQueryProgressTotalRows :: !Word64
+    }
+    deriving (Eq, Show)
+
+instance Storable DuckDBQueryProgress where
+    sizeOf _ = sizeOf (undefined :: CDouble) + 2 * sizeOf (undefined :: Word64)
+    alignment _ = alignment (undefined :: CDouble)
+    peek ptr = do
+        percentage <- realToFrac <$> (peekByteOff ptr 0 :: IO CDouble)
+        let offset1 = sizeOf (undefined :: CDouble)
+            offset2 = offset1 + sizeOf (undefined :: Word64)
+        processed <- peekByteOff ptr offset1
+        total <- peekByteOff ptr offset2
+        pure (DuckDBQueryProgress percentage processed total)
+    poke ptr (DuckDBQueryProgress percentage processed total) = do
+        pokeByteOff ptr 0 (realToFrac percentage :: CDouble)
+        let offset1 = sizeOf (undefined :: CDouble)
+            offset2 = offset1 + sizeOf (undefined :: Word64)
+        pokeByteOff ptr offset1 processed
+        pokeByteOff ptr offset2 total
+
+-- | Opaque DuckDB column handle.
+data DuckDBColumn
+
+-- | DuckDB result structure (opaque to callers, but required for FFI marshalling).
+data DuckDBResult = DuckDBResult
+    { duckDBResultDeprecatedColumnCount :: !DuckDBIdx
+    , duckDBResultDeprecatedRowCount :: !DuckDBIdx
+    , duckDBResultDeprecatedRowsChanged :: !DuckDBIdx
+    , duckDBResultDeprecatedColumns :: !(Ptr DuckDBColumn)
+    , duckDBResultDeprecatedErrorMessage :: !CString
+    , duckDBResultInternalData :: !(Ptr ())
+    }
+
+instance Storable DuckDBResult where
+    sizeOf _ = 3 * sizeOf (undefined :: DuckDBIdx) + 3 * sizeOf (undefined :: Ptr ())
+    alignment _ = alignment (undefined :: DuckDBIdx)
+    peek ptr = do
+        colCount <- peekByteOff ptr 0
+        rowCount <- peekByteOff ptr (sizeOf (undefined :: DuckDBIdx))
+        rowsChanged <- peekByteOff ptr (2 * sizeOf (undefined :: DuckDBIdx))
+        let basePtr = 3 * sizeOf (undefined :: DuckDBIdx)
+        columns <- peekByteOff ptr basePtr
+        errMsg <- peekByteOff ptr (basePtr + sizeOf (undefined :: Ptr ()))
+        internal <- peekByteOff ptr (basePtr + 2 * sizeOf (undefined :: Ptr ()))
+        pure
+            DuckDBResult
+                { duckDBResultDeprecatedColumnCount = colCount
+                , duckDBResultDeprecatedRowCount = rowCount
+                , duckDBResultDeprecatedRowsChanged = rowsChanged
+                , duckDBResultDeprecatedColumns = columns
+                , duckDBResultDeprecatedErrorMessage = errMsg
+                , duckDBResultInternalData = internal
+                }
+    poke ptr result = do
+        let columnCount = duckDBResultDeprecatedColumnCount result
+            rowCount = duckDBResultDeprecatedRowCount result
+            rowsChanged = duckDBResultDeprecatedRowsChanged result
+            columns = duckDBResultDeprecatedColumns result
+            errorMessage = duckDBResultDeprecatedErrorMessage result
+            internalData = duckDBResultInternalData result
+            basePtr = 3 * sizeOf (undefined :: DuckDBIdx)
+        pokeByteOff ptr 0 columnCount
+        pokeByteOff ptr (sizeOf (undefined :: DuckDBIdx)) rowCount
+        pokeByteOff ptr (2 * sizeOf (undefined :: DuckDBIdx)) rowsChanged
+        pokeByteOff ptr basePtr columns
+        pokeByteOff ptr (basePtr + sizeOf (undefined :: Ptr ())) errorMessage
+        pokeByteOff ptr (basePtr + 2 * sizeOf (undefined :: Ptr ())) internalData
+
+-- | Tag type backing @duckdb_database@ pointers.
+data DuckDBDatabaseStruct
+
+-- | Handle to a DuckDB database instance.
+type DuckDBDatabase = Ptr DuckDBDatabaseStruct
+
+-- | Tag type backing @duckdb_connection@ pointers.
+data DuckDBConnectionStruct
+
+-- | Handle to a DuckDB connection.
+type DuckDBConnection = Ptr DuckDBConnectionStruct
+
+-- | Tag type backing @duckdb_config@ pointers.
+data DuckDBConfigStruct
+
+-- | Handle to a DuckDB configuration object.
+type DuckDBConfig = Ptr DuckDBConfigStruct
+
+-- | Tag type backing @duckdb_instance_cache@ pointers.
+data DuckDBInstanceCacheStruct
+
+-- | Handle to a DuckDB instance cache.
+type DuckDBInstanceCache = Ptr DuckDBInstanceCacheStruct
+
+-- | Tag type backing @duckdb_extracted_statements@ pointers.
+data DuckDBExtractedStatementsStruct
+
+-- | Handle to extracted SQL statements.
+type DuckDBExtractedStatements = Ptr DuckDBExtractedStatementsStruct
+
+-- | Tag type backing @duckdb_function_info@ pointers.
+data DuckDBFunctionInfoStruct
+
+-- | Handle to function execution context.
+type DuckDBFunctionInfo = Ptr DuckDBFunctionInfoStruct
+
+-- | Tag type backing @duckdb_bind_info@ pointers.
+data DuckDBBindInfoStruct
+
+-- | Handle to scalar function bind context.
+type DuckDBBindInfo = Ptr DuckDBBindInfoStruct
+
+-- | Tag type backing @duckdb_scalar_function@ pointers.
+data DuckDBScalarFunctionStruct
+
+-- | Handle to a scalar function definition.
+type DuckDBScalarFunction = Ptr DuckDBScalarFunctionStruct
+
+-- | Tag type backing @duckdb_scalar_function_set@ pointers.
+data DuckDBScalarFunctionSetStruct
+
+-- | Handle to a set of scalar function overloads.
+type DuckDBScalarFunctionSet = Ptr DuckDBScalarFunctionSetStruct
+
+-- | Tag type backing @duckdb_aggregate_function@ pointers.
+data DuckDBAggregateFunctionStruct
+
+-- | Handle to an aggregate function definition.
+type DuckDBAggregateFunction = Ptr DuckDBAggregateFunctionStruct
+
+-- | Tag type backing @duckdb_aggregate_function_set@ pointers.
+data DuckDBAggregateFunctionSetStruct
+
+-- | Handle to an aggregate function set.
+type DuckDBAggregateFunctionSet = Ptr DuckDBAggregateFunctionSetStruct
+
+-- | Tag type backing @duckdb_vector@ pointers.
+data DuckDBVectorStruct
+
+-- | Handle to a DuckDB vector.
+type DuckDBVector = Ptr DuckDBVectorStruct
+
+-- | Tag type backing @duckdb_data_chunk@ pointers.
+data DuckDBDataChunkStruct
+
+-- | Handle to a DuckDB data chunk.
+type DuckDBDataChunk = Ptr DuckDBDataChunkStruct
+
+-- | Tag type backing @duckdb_selection_vector@ pointers.
+data DuckDBSelectionVectorStruct
+
+-- | Handle to a DuckDB selection vector.
+type DuckDBSelectionVector = Ptr DuckDBSelectionVectorStruct
+
+-- | Tag type backing @duckdb_arrow_options@ pointers.
+data DuckDBArrowOptionsStruct
+
+-- | Handle to DuckDB Arrow options.
+type DuckDBArrowOptions = Ptr DuckDBArrowOptionsStruct
+
+-- | Tag type backing @duckdb_arrow@ pointers.
+newtype DuckDBArrowStruct = DuckDBArrowStruct
+    { duckdbArrowInternalPtr :: Ptr ()
+    }
+
+-- | Handle to an Arrow query result.
+type DuckDBArrow = Ptr DuckDBArrowStruct
+
+-- | Tag type backing @duckdb_arrow_schema@ pointers.
+newtype DuckDBArrowSchemaStruct = DuckDBArrowSchemaStruct
+    { duckdbArrowSchemaInternalPtr :: Ptr ()
+    }
+
+-- | Handle to an Arrow schema.
+type DuckDBArrowSchema = Ptr DuckDBArrowSchemaStruct
+
+-- | Tag type backing @duckdb_arrow_array@ pointers.
+newtype DuckDBArrowArrayStruct = DuckDBArrowArrayStruct
+    { duckdbArrowArrayInternalPtr :: Ptr ()
+    }
+
+-- | Handle to an Arrow array.
+type DuckDBArrowArray = Ptr DuckDBArrowArrayStruct
+
+instance Storable DuckDBArrowStruct where
+    sizeOf _ = pointerSize
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        DuckDBArrowStruct
+            <$> peekByteOff ptr 0
+    poke ptr DuckDBArrowStruct{..} =
+        pokeByteOff ptr 0 duckdbArrowInternalPtr
+
+instance Storable DuckDBArrowSchemaStruct where
+    sizeOf _ = pointerSize
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        DuckDBArrowSchemaStruct
+            <$> peekByteOff ptr 0
+    poke ptr DuckDBArrowSchemaStruct{..} =
+        pokeByteOff ptr 0 duckdbArrowSchemaInternalPtr
+
+instance Storable DuckDBArrowArrayStruct where
+    sizeOf _ = pointerSize
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        DuckDBArrowArrayStruct
+            <$> peekByteOff ptr 0
+    poke ptr DuckDBArrowArrayStruct{..} =
+        pokeByteOff ptr 0 duckdbArrowArrayInternalPtr
+
+-- | Tag type backing @duckdb_arrow_converted_schema@ pointers.
+data DuckDBArrowConvertedSchemaStruct
+
+-- | Handle to a converted Arrow schema.
+type DuckDBArrowConvertedSchema = Ptr DuckDBArrowConvertedSchemaStruct
+
+-- | Tag type backing @duckdb_arrow_stream@ pointers.
+newtype DuckDBArrowStreamStruct = DuckDBArrowStreamStruct
+    { duckdbArrowStreamInternalPtr :: Ptr ()
+    }
+
+-- | Handle to an Arrow stream.
+type DuckDBArrowStream = Ptr DuckDBArrowStreamStruct
+
+instance Storable DuckDBArrowStreamStruct where
+    sizeOf _ = pointerSize
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        DuckDBArrowStreamStruct
+            <$> peekByteOff ptr 0
+    poke ptr DuckDBArrowStreamStruct{..} =
+        pokeByteOff ptr 0 duckdbArrowStreamInternalPtr
+
+-- | Tag type backing @duckdb_expression@ pointers.
+data DuckDBExpressionStruct
+
+-- | Handle to a DuckDB expression.
+type DuckDBExpression = Ptr DuckDBExpressionStruct
+
+-- | Tag type backing @duckdb_client_context@ pointers.
+data DuckDBClientContextStruct
+
+-- | Handle to a DuckDB client context.
+type DuckDBClientContext = Ptr DuckDBClientContextStruct
+
+-- | Tag type backing @duckdb_prepared_statement@ pointers.
+data DuckDBPreparedStatementStruct
+
+-- | Handle to a prepared statement.
+type DuckDBPreparedStatement = Ptr DuckDBPreparedStatementStruct
+
+-- | Tag type backing @duckdb_value@ pointers.
+data DuckDBValueStruct
+
+-- | Handle to a scalar DuckDB value.
+type DuckDBValue = Ptr DuckDBValueStruct
+
+-- | Tag type backing @duckdb_pending_result@ pointers.
+data DuckDBPendingResultStruct
+
+-- | Handle to a pending (incremental) query result.
+type DuckDBPendingResult = Ptr DuckDBPendingResultStruct
+
+-- | Tag type backing @duckdb_logical_type@ pointers.
+data DuckDBLogicalTypeStruct
+
+-- | Handle to a DuckDB logical type value.
+type DuckDBLogicalType = Ptr DuckDBLogicalTypeStruct
+
+-- | Tag type backing @duckdb_create_type_info@ pointers.
+data DuckDBCreateTypeInfoStruct
+
+-- | Handle to logical type registration details.
+type DuckDBCreateTypeInfo = Ptr DuckDBCreateTypeInfoStruct
+
+-- | Tag type backing @duckdb_error_data@ pointers.
+data DuckDBErrorDataStruct
+
+-- | Handle to DuckDB error data.
+type DuckDBErrorData = Ptr DuckDBErrorDataStruct
+
+-- | Tag type backing @duckdb_init_info@ pointers.
+data DuckDBInitInfoStruct
+
+-- | Handle to table function initialization state.
+type DuckDBInitInfo = Ptr DuckDBInitInfoStruct
+
+-- | Tag type backing @duckdb_cast_function@ pointers.
+data DuckDBCastFunctionStruct
+
+-- | Handle to a cast function definition.
+type DuckDBCastFunction = Ptr DuckDBCastFunctionStruct
+
+-- | Tag type backing @duckdb_table_function@ pointers.
+data DuckDBTableFunctionStruct
+
+-- | Handle to a table function definition.
+type DuckDBTableFunction = Ptr DuckDBTableFunctionStruct
+
+-- | Tag type backing @duckdb_appender@ pointers.
+data DuckDBAppenderStruct
+
+-- | Handle to an appender.
+type DuckDBAppender = Ptr DuckDBAppenderStruct
+
+-- | Tag type backing @duckdb_table_description@ pointers.
+data DuckDBTableDescriptionStruct
+
+-- | Handle to a table description.
+type DuckDBTableDescription = Ptr DuckDBTableDescriptionStruct
+
+-- | Tag type backing @duckdb_profiling_info@ pointers.
+data DuckDBProfilingInfoStruct
+
+-- | Handle to profiling information.
+type DuckDBProfilingInfo = Ptr DuckDBProfilingInfoStruct
+
+-- | Tag type backing @duckdb_replacement_scan_info@ pointers.
+data DuckDBReplacementScanInfoStruct
+
+-- | Handle to replacement scan context.
+type DuckDBReplacementScanInfo = Ptr DuckDBReplacementScanInfoStruct
+
+-- | Handle to a DuckDB aggregate state.
+data DuckDBAggregateStateStruct
+
+-- | Opaque pointer to the aggregate-function state handed to user callbacks.
+type DuckDBAggregateState = Ptr DuckDBAggregateStateStruct
+
+-- | Handle to a DuckDB task state.
+type DuckDBTaskState = Ptr ()
+
+-- | Function pointer used to represent scalar function execution callbacks.
+type DuckDBScalarFunctionFun = FunPtr (DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ())
+
+-- | Function pointer used to represent scalar function bind callbacks.
+type DuckDBScalarFunctionBindFun = FunPtr (DuckDBBindInfo -> IO ())
+
+-- | Function pointer used to destroy user-provided data blobs.
+type DuckDBDeleteCallback = FunPtr (Ptr () -> IO ())
+
+-- | Function pointer used to copy user-provided data blobs.
+type DuckDBCopyCallback = FunPtr (Ptr () -> IO (Ptr ()))
+
+-- | Function pointer implementing cast functions.
+type DuckDBCastFunctionFun =
+    FunPtr (DuckDBFunctionInfo -> DuckDBIdx -> DuckDBVector -> DuckDBVector -> IO CBool)
+
+-- | Function pointer returning aggregate state size.
+type DuckDBAggregateStateSizeFun = FunPtr (DuckDBFunctionInfo -> IO DuckDBIdx)
+
+-- | Function pointer initializing aggregate state.
+type DuckDBAggregateInitFun = FunPtr (DuckDBFunctionInfo -> DuckDBAggregateState -> IO ())
+
+-- | Function pointer destroying aggregate state batches.
+type DuckDBAggregateDestroyFun = FunPtr (Ptr DuckDBAggregateState -> DuckDBIdx -> IO ())
+
+-- | Function pointer updating aggregate states.
+type DuckDBAggregateUpdateFun =
+    FunPtr (DuckDBFunctionInfo -> DuckDBDataChunk -> Ptr DuckDBAggregateState -> IO ())
+
+-- | Function pointer combining aggregate states.
+type DuckDBAggregateCombineFun =
+    FunPtr
+        ( DuckDBFunctionInfo ->
+          Ptr DuckDBAggregateState ->
+          Ptr DuckDBAggregateState ->
+          DuckDBIdx ->
+          IO ()
+        )
+
+-- | Function pointer finalising aggregate states.
+type DuckDBAggregateFinalizeFun =
+    FunPtr
+        ( DuckDBFunctionInfo ->
+          Ptr DuckDBAggregateState ->
+          DuckDBVector ->
+          DuckDBIdx ->
+          DuckDBIdx ->
+          IO ()
+        )
+
+-- | Function pointer for table function bind callbacks.
+type DuckDBTableFunctionBindFun = FunPtr (DuckDBBindInfo -> IO ())
+
+-- | Function pointer for table function init callbacks.
+type DuckDBTableFunctionInitFun = FunPtr (DuckDBInitInfo -> IO ())
+
+-- | Function pointer for table function execution callbacks.
+type DuckDBTableFunctionFun = FunPtr (DuckDBFunctionInfo -> DuckDBDataChunk -> IO ())
+
+-- | Function pointer for replacement scan callbacks.
+type DuckDBReplacementCallback =
+    FunPtr (DuckDBReplacementScanInfo -> CString -> Ptr () -> IO ())
+
+-- The full Arrow C Data Interface definitions are not included here to avoid
+-- introducing a dependency on the Arrow C headers. Instead, we define only the
+-- parts we need for testing DuckDB's Arrow integration.
+-- See https://arrow.apache.org/docs/format/CDataInterface.html for the full
+-- specification.
+-- #ifndef ARROW_C_DATA_INTERFACE
+-- #define ARROW_C_DATA_INTERFACE
+
+-- #define ARROW_FLAG_DICTIONARY_ORDERED 1
+-- #define ARROW_FLAG_NULLABLE 2
+-- #define ARROW_FLAG_MAP_KEYS_SORTED 4
+
+-- struct ArrowSchema {
+--   // Array type description
+--   const char* format;
+--   const char* name;
+--   const char* metadata;
+--   int64_t flags;
+--   int64_t n_children;
+--   struct ArrowSchema** children;
+--   struct ArrowSchema* dictionary;
+
+--   // Release callback
+--   void (*release)(struct ArrowSchema*);
+--   // Opaque producer-specific data
+--   void* private_data;
+-- };
+
+-- struct ArrowArray {
+--   // Array data description
+--   int64_t length;
+--   int64_t null_count;
+--   int64_t offset;
+--   int64_t n_buffers;
+--   int64_t n_children;
+--   const void** buffers;
+--   struct ArrowArray** children;
+--   struct ArrowArray* dictionary;
+
+--   // Release callback
+--   void (*release)(struct ArrowArray*);
+--   // Opaque producer-specific data
+--   void* private_data;
+-- };
+
+-- #endif  // ARROW_C_DATA_INTERFACE
+
+{- | Partial Arrow schema view used for tests that require inspecting DuckDB's
+Arrow wrappers without depending on the full Arrow C Data Interface
+definitions.
+-}
+data ArrowSchema = ArrowSchema
+    { arrowSchemaFormat :: CString
+    , arrowSchemaName :: CString
+    , arrowSchemaMetadata :: CString
+    , arrowSchemaFlags :: Int64
+    , arrowSchemaChildCount :: Int64
+    , arrowSchemaChildren :: Ptr (Ptr ArrowSchema)
+    , arrowSchemaDictionary :: Ptr ArrowSchema
+    , arrowSchemaRelease :: FunPtr (Ptr ArrowSchema -> IO ())
+    , arrowSchemaPrivateData :: Ptr ()
+    }
+
+instance Storable ArrowSchema where
+    sizeOf _ = pointerSize * 9
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        ArrowSchema
+            <$> peekByteOff ptr 0
+            <*> peekByteOff ptr pointerSize
+            <*> peekByteOff ptr (pointerSize * 2)
+            <*> peekByteOff ptr (pointerSize * 3)
+            <*> peekByteOff ptr (pointerSize * 4)
+            <*> peekByteOff ptr (pointerSize * 5)
+            <*> peekByteOff ptr (pointerSize * 6)
+            <*> peekByteOff ptr (pointerSize * 7)
+            <*> peekByteOff ptr (pointerSize * 8)
+    poke ptr ArrowSchema{..} = do
+        pokeByteOff ptr 0 arrowSchemaFormat
+        pokeByteOff ptr pointerSize arrowSchemaName
+        pokeByteOff ptr (pointerSize * 2) arrowSchemaMetadata
+        pokeByteOff ptr (pointerSize * 3) arrowSchemaFlags
+        pokeByteOff ptr (pointerSize * 4) arrowSchemaChildCount
+        pokeByteOff ptr (pointerSize * 5) arrowSchemaChildren
+        pokeByteOff ptr (pointerSize * 6) arrowSchemaDictionary
+        pokeByteOff ptr (pointerSize * 7) arrowSchemaRelease
+        pokeByteOff ptr (pointerSize * 8) arrowSchemaPrivateData
+
+-- | Partial Arrow array view mirroring the DuckDB C API layout.
+data ArrowArray = ArrowArray
+    { arrowArrayLength :: Int64
+    , arrowArrayNullCount :: Int64
+    , arrowArrayOffset :: Int64
+    , arrowArrayBufferCount :: Int64
+    , arrowArrayChildCount :: Int64
+    , arrowArrayBuffers :: Ptr (Ptr ())
+    , arrowArrayChildren :: Ptr (Ptr ArrowArray)
+    , arrowArrayDictionary :: Ptr ArrowArray
+    , arrowArrayRelease :: FunPtr (Ptr ArrowArray -> IO ())
+    , arrowArrayPrivateData :: Ptr ()
+    }
+
+instance Storable ArrowArray where
+    sizeOf _ = pointerSize * 5 + intFieldSize * 5
+    alignment _ = alignment (nullPtr :: Ptr ())
+    peek ptr =
+        ArrowArray
+            <$> peekByteOff ptr 0
+            <*> peekByteOff ptr intFieldSize
+            <*> peekByteOff ptr (intFieldSize * 2)
+            <*> peekByteOff ptr (intFieldSize * 3)
+            <*> peekByteOff ptr (intFieldSize * 4)
+            <*> peekByteOff ptr (intFieldSize * 5)
+            <*> peekByteOff ptr (intFieldSize * 5 + pointerSize)
+            <*> peekByteOff ptr (intFieldSize * 5 + pointerSize * 2)
+            <*> peekByteOff ptr (intFieldSize * 5 + pointerSize * 3)
+            <*> peekByteOff ptr (intFieldSize * 5 + pointerSize * 4)
+    poke ptr ArrowArray{..} = do
+        pokeByteOff ptr 0 arrowArrayLength
+        pokeByteOff ptr intFieldSize arrowArrayNullCount
+        pokeByteOff ptr (intFieldSize * 2) arrowArrayOffset
+        pokeByteOff ptr (intFieldSize * 3) arrowArrayBufferCount
+        pokeByteOff ptr (intFieldSize * 4) arrowArrayChildCount
+        pokeByteOff ptr (intFieldSize * 5) arrowArrayBuffers
+        pokeByteOff ptr (intFieldSize * 5 + pointerSize) arrowArrayChildren
+        pokeByteOff ptr (intFieldSize * 5 + pointerSize * 2) arrowArrayDictionary
+        pokeByteOff ptr (intFieldSize * 5 + pointerSize * 3) arrowArrayRelease
+        pokeByteOff ptr (intFieldSize * 5 + pointerSize * 4) arrowArrayPrivateData
+
+{- | Pointer wrapper for the deprecated Arrow schema handle exposed by DuckDB.
+The underlying memory is managed by DuckDB and must only be accessed through
+the deprecated Arrow helper functions.
+-}
+newtype ArrowSchemaPtr = ArrowSchemaPtr {unArrowSchemaPtr :: Ptr ArrowSchema}
+    deriving (Eq)
+
+{- | Pointer wrapper for the deprecated Arrow array handle exposed by DuckDB.
+DuckDB assumes exclusive ownership and coordinates buffer lifetimes via the
+release callback stored in the referenced @ArrowArray@ struct.
+-}
+newtype ArrowArrayPtr = ArrowArrayPtr {unArrowArrayPtr :: Ptr ArrowArray}
+    deriving (Eq)
+
+{- | Pointer wrapper for the deprecated Arrow stream handle returned by DuckDB.
+DuckDB owns the referenced stream; call @c_duckdb_destroy_arrow_stream@ after
+invoking @duckdb_arrow_scan@.
+-}
+newtype ArrowStreamPtr = ArrowStreamPtr {unArrowStreamPtr :: Ptr ()}
+    deriving (Eq)
+
+pointerSize :: Int
+pointerSize = sizeOf (nullPtr :: Ptr ())
+
+intFieldSize :: Int
+intFieldSize = sizeOf (0 :: Int64)
diff --git a/src/Database/DuckDB/FFI/Validity.hs b/src/Database/DuckDB/FFI/Validity.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Validity.hs
@@ -0,0 +1,61 @@
+module Database.DuckDB.FFI.Validity (
+    c_duckdb_validity_row_is_valid,
+    c_duckdb_validity_set_row_validity,
+    c_duckdb_validity_set_row_invalid,
+    c_duckdb_validity_set_row_valid,
+) where
+
+import Data.Word (Word64)
+import Database.DuckDB.FFI.Types
+import Foreign.C.Types (CBool (..))
+import Foreign.Ptr (Ptr)
+
+{- | Returns whether or not a row is valid (i.e., not NULL) in the given validity
+mask.
+
+Parameters:
+* @validity@: The validity mask, as obtained through
+  @duckdb_vector_get_validity@
+* @row@: The row index
+
+Returns true if the row is valid, false otherwise
+-}
+foreign import ccall unsafe "duckdb_validity_row_is_valid"
+    c_duckdb_validity_row_is_valid :: Ptr Word64 -> DuckDBIdx -> IO CBool
+
+{- | In a validity mask, sets a specific row to either valid or invalid.
+
+Note that @duckdb_vector_ensure_validity_writable@ should be called before
+calling @duckdb_vector_get_validity@, to ensure that there is a validity mask
+to write to.
+
+Parameters:
+* @validity@: The validity mask, as obtained through
+  @duckdb_vector_get_validity@.
+* @row@: The row index
+* @valid@: Whether or not to set the row to valid, or invalid
+-}
+foreign import ccall unsafe "duckdb_validity_set_row_validity"
+    c_duckdb_validity_set_row_validity :: Ptr Word64 -> DuckDBIdx -> CBool -> IO ()
+
+{- | In a validity mask, sets a specific row to invalid.
+
+Equivalent to @duckdb_validity_set_row_validity@ with valid set to false.
+
+Parameters:
+* @validity@: The validity mask
+* @row@: The row index
+-}
+foreign import ccall unsafe "duckdb_validity_set_row_invalid"
+    c_duckdb_validity_set_row_invalid :: Ptr Word64 -> DuckDBIdx -> IO ()
+
+{- | In a validity mask, sets a specific row to valid.
+
+Equivalent to @duckdb_validity_set_row_validity@ with valid set to true.
+
+Parameters:
+* @validity@: The validity mask
+* @row@: The row index
+-}
+foreign import ccall unsafe "duckdb_validity_set_row_valid"
+    c_duckdb_validity_set_row_valid :: Ptr Word64 -> DuckDBIdx -> IO ()
diff --git a/src/Database/DuckDB/FFI/ValueInterface.hs b/src/Database/DuckDB/FFI/ValueInterface.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/ValueInterface.hs
@@ -0,0 +1,928 @@
+module Database.DuckDB.FFI.ValueInterface (
+    c_duckdb_destroy_value,
+    c_duckdb_create_varchar,
+    c_duckdb_create_varchar_length,
+    c_duckdb_create_bool,
+    c_duckdb_create_int8,
+    c_duckdb_create_uint8,
+    c_duckdb_create_int16,
+    c_duckdb_create_uint16,
+    c_duckdb_create_int32,
+    c_duckdb_create_uint32,
+    c_duckdb_create_uint64,
+    c_duckdb_create_int64,
+    c_duckdb_create_hugeint,
+    c_duckdb_create_uhugeint,
+    c_duckdb_create_bignum,
+    c_duckdb_create_decimal,
+    c_duckdb_create_float,
+    c_duckdb_create_double,
+    c_duckdb_create_date,
+    c_duckdb_create_time,
+    c_duckdb_create_time_ns,
+    c_duckdb_create_time_tz_value,
+    c_duckdb_create_timestamp,
+    c_duckdb_create_timestamp_tz,
+    c_duckdb_create_timestamp_s,
+    c_duckdb_create_timestamp_ms,
+    c_duckdb_create_timestamp_ns,
+    c_duckdb_create_interval,
+    c_duckdb_create_blob,
+    c_duckdb_create_bit,
+    c_duckdb_create_uuid,
+    c_duckdb_get_bool,
+    c_duckdb_get_int8,
+    c_duckdb_get_uint8,
+    c_duckdb_get_int16,
+    c_duckdb_get_uint16,
+    c_duckdb_get_int32,
+    c_duckdb_get_uint32,
+    c_duckdb_get_int64,
+    c_duckdb_get_uint64,
+    c_duckdb_get_hugeint,
+    c_duckdb_get_uhugeint,
+    c_duckdb_get_bignum,
+    c_duckdb_get_decimal,
+    c_duckdb_get_float,
+    c_duckdb_get_double,
+    c_duckdb_get_date,
+    c_duckdb_get_time,
+    c_duckdb_get_time_ns,
+    c_duckdb_get_time_tz,
+    c_duckdb_get_timestamp,
+    c_duckdb_get_timestamp_tz,
+    c_duckdb_get_timestamp_s,
+    c_duckdb_get_timestamp_ms,
+    c_duckdb_get_timestamp_ns,
+    c_duckdb_get_interval,
+    c_duckdb_get_value_type,
+    c_duckdb_get_blob,
+    c_duckdb_get_bit,
+    c_duckdb_get_uuid,
+    c_duckdb_get_varchar,
+    c_duckdb_create_struct_value,
+    c_duckdb_create_list_value,
+    c_duckdb_create_array_value,
+    c_duckdb_create_map_value,
+    c_duckdb_create_union_value,
+    c_duckdb_get_map_size,
+    c_duckdb_get_map_key,
+    c_duckdb_get_map_value,
+    c_duckdb_is_null_value,
+    c_duckdb_create_null_value,
+    c_duckdb_get_list_size,
+    c_duckdb_get_list_child,
+    c_duckdb_create_enum_value,
+    c_duckdb_get_enum_value,
+    c_duckdb_get_struct_child,
+    c_duckdb_value_to_string,
+) where
+
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..))
+import Foreign.Ptr (Ptr)
+
+{- | Destroys the value and de-allocates all memory allocated for that type.
+
+Parameters:
+* @value@: The value to destroy.
+-}
+foreign import ccall unsafe "duckdb_destroy_value"
+    c_duckdb_destroy_value :: Ptr DuckDBValue -> IO ()
+
+{- | Creates a value from a null-terminated string
+
+Parameters:
+* @text@: The null-terminated string
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_varchar"
+    c_duckdb_create_varchar :: CString -> IO DuckDBValue
+
+{- | Creates a value from a string
+
+Parameters:
+* @text@: The text
+* @length@: The length of the text
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_varchar_length"
+    c_duckdb_create_varchar_length :: CString -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates a value from a boolean
+
+Parameters:
+* @input@: The boolean value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_bool"
+    c_duckdb_create_bool :: CBool -> IO DuckDBValue
+
+{- | Creates a value from an int8_t (a tinyint)
+
+Parameters:
+* @input@: The tinyint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_int8"
+    c_duckdb_create_int8 :: Int8 -> IO DuckDBValue
+
+{- | Creates a value from a uint8_t (a utinyint)
+
+Parameters:
+* @input@: The utinyint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_uint8"
+    c_duckdb_create_uint8 :: Word8 -> IO DuckDBValue
+
+{- | Creates a value from an int16_t (a smallint)
+
+Parameters:
+* @input@: The smallint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_int16"
+    c_duckdb_create_int16 :: Int16 -> IO DuckDBValue
+
+{- | Creates a value from a uint16_t (a usmallint)
+
+Parameters:
+* @input@: The usmallint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_uint16"
+    c_duckdb_create_uint16 :: Word16 -> IO DuckDBValue
+
+{- | Creates a value from an int32_t (an integer)
+
+Parameters:
+* @input@: The integer value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_int32"
+    c_duckdb_create_int32 :: Int32 -> IO DuckDBValue
+
+{- | Creates a value from a uint32_t (a uinteger)
+
+Parameters:
+* @input@: The uinteger value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_uint32"
+    c_duckdb_create_uint32 :: Word32 -> IO DuckDBValue
+
+{- | Creates a value from a uint64_t (a ubigint)
+
+Parameters:
+* @input@: The ubigint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_uint64"
+    c_duckdb_create_uint64 :: Word64 -> IO DuckDBValue
+
+{- | Creates a value from an int64
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_int64"
+    c_duckdb_create_int64 :: Int64 -> IO DuckDBValue
+
+{- | Creates a value from a hugeint
+
+Parameters:
+* @input@: The hugeint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_hugeint@
+but mirror the DuckDB C API semantics of @duckdb_create_hugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_hugeint"
+    c_duckdb_create_hugeint :: Ptr DuckDBHugeInt -> IO DuckDBValue
+
+{- | Creates a value from a uhugeint
+
+Parameters:
+* @input@: The uhugeint value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_uhugeint@
+but mirror the DuckDB C API semantics of @duckdb_create_uhugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_uhugeint"
+    c_duckdb_create_uhugeint :: Ptr DuckDBUHugeInt -> IO DuckDBValue
+
+{- | Creates a BIGNUM value from a duckdb_bignum
+
+Parameters:
+* @input@: The duckdb_bignum value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_bignum@
+but mirror the DuckDB C API semantics of @duckdb_create_bignum@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_bignum"
+    c_duckdb_create_bignum :: Ptr DuckDBBignum -> IO DuckDBValue
+
+{- | Creates a DECIMAL value from a duckdb_decimal
+
+Parameters:
+* @input@: The duckdb_decimal value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_decimal@
+but mirror the DuckDB C API semantics of @duckdb_create_decimal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_decimal"
+    c_duckdb_create_decimal :: Ptr DuckDBDecimal -> IO DuckDBValue
+
+{- | Creates a value from a float
+
+Parameters:
+* @input@: The float value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_float"
+    c_duckdb_create_float :: CFloat -> IO DuckDBValue
+
+{- | Creates a value from a double
+
+Parameters:
+* @input@: The double value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_double"
+    c_duckdb_create_double :: CDouble -> IO DuckDBValue
+
+{- | Creates a value from a date
+
+Parameters:
+* @input@: The date value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_date"
+    c_duckdb_create_date :: DuckDBDate -> IO DuckDBValue
+
+{- | Creates a value from a time
+
+Parameters:
+* @input@: The time value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_time"
+    c_duckdb_create_time :: DuckDBTime -> IO DuckDBValue
+
+{- | Creates a value from a time_ns
+
+Parameters:
+* @input@: The time value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_time_ns"
+    c_duckdb_create_time_ns :: DuckDBTimeNs -> IO DuckDBValue
+
+{- | Creates a value from a time_tz. Not to be confused with
+@duckdb_create_time_tz@, which creates a duckdb_time_tz_t.
+
+Parameters:
+* @value@: The time_tz value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_time_tz_value"
+    c_duckdb_create_time_tz_value :: DuckDBTimeTz -> IO DuckDBValue
+
+{- | Creates a TIMESTAMP value from a duckdb_timestamp
+
+Parameters:
+* @input@: The duckdb_timestamp value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_timestamp"
+    c_duckdb_create_timestamp :: DuckDBTimestamp -> IO DuckDBValue
+
+{- | Creates a TIMESTAMP_TZ value from a duckdb_timestamp
+
+Parameters:
+* @input@: The duckdb_timestamp value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_timestamp_tz"
+    c_duckdb_create_timestamp_tz :: DuckDBTimestamp -> IO DuckDBValue
+
+{- | Creates a TIMESTAMP_S value from a duckdb_timestamp_s
+
+Parameters:
+* @input@: The duckdb_timestamp_s value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_timestamp_s"
+    c_duckdb_create_timestamp_s :: DuckDBTimestampS -> IO DuckDBValue
+
+{- | Creates a TIMESTAMP_MS value from a duckdb_timestamp_ms
+
+Parameters:
+* @input@: The duckdb_timestamp_ms value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_timestamp_ms"
+    c_duckdb_create_timestamp_ms :: DuckDBTimestampMs -> IO DuckDBValue
+
+{- | Creates a TIMESTAMP_NS value from a duckdb_timestamp_ns
+
+Parameters:
+* @input@: The duckdb_timestamp_ns value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_timestamp_ns"
+    c_duckdb_create_timestamp_ns :: DuckDBTimestampNs -> IO DuckDBValue
+
+{- | Creates a value from an interval
+
+Parameters:
+* @input@: The interval value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_interval@
+but mirror the DuckDB C API semantics of @duckdb_create_interval@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_interval"
+    c_duckdb_create_interval :: Ptr DuckDBInterval -> IO DuckDBValue
+
+{- | Creates a value from a blob
+
+Parameters:
+* @data@: The blob data
+* @length@: The length of the blob data
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_blob"
+    c_duckdb_create_blob :: Ptr Word8 -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates a BIT value from a duckdb_bit
+
+Parameters:
+* @input@: The duckdb_bit value
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_bit@ but
+mirror the DuckDB C API semantics of @duckdb_create_bit@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_bit"
+    c_duckdb_create_bit :: Ptr DuckDBBit -> IO DuckDBValue
+
+{- | Creates a UUID value from a uhugeint
+
+Parameters:
+* @input@: The duckdb_uhugeint containing the UUID
+
+Returns The value. This must be destroyed with @duckdb_destroy_value@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_create_uuid@ but
+mirror the DuckDB C API semantics of @duckdb_create_uuid@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_create_uuid"
+    c_duckdb_create_uuid :: Ptr DuckDBUHugeInt -> IO DuckDBValue
+
+{- | Returns the boolean value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a boolean
+
+Returns A boolean, or false if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_bool"
+    c_duckdb_get_bool :: DuckDBValue -> IO CBool
+
+{- | Returns the int8_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a tinyint
+
+Returns A int8_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_int8"
+    c_duckdb_get_int8 :: DuckDBValue -> IO Int8
+
+{- | Returns the uint8_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a utinyint
+
+Returns A uint8_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_uint8"
+    c_duckdb_get_uint8 :: DuckDBValue -> IO Word8
+
+{- | Returns the int16_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a smallint
+
+Returns A int16_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_int16"
+    c_duckdb_get_int16 :: DuckDBValue -> IO Int16
+
+{- | Returns the uint16_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a usmallint
+
+Returns A uint16_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_uint16"
+    c_duckdb_get_uint16 :: DuckDBValue -> IO Word16
+
+{- | Returns the int32_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing an integer
+
+Returns A int32_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_int32"
+    c_duckdb_get_int32 :: DuckDBValue -> IO Int32
+
+{- | Returns the uint32_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a uinteger
+
+Returns A uint32_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_uint32"
+    c_duckdb_get_uint32 :: DuckDBValue -> IO Word32
+
+{- | Returns the int64_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a bigint
+
+Returns A int64_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_int64"
+    c_duckdb_get_int64 :: DuckDBValue -> IO Int64
+
+{- | Returns the uint64_t value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a ubigint
+
+Returns A uint64_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_uint64"
+    c_duckdb_get_uint64 :: DuckDBValue -> IO Word64
+
+{- | Returns the hugeint value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a hugeint
+
+Returns A duckdb_hugeint, or MinValue if the value cannot be converted
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_hugeint@ but
+mirror the DuckDB C API semantics of @duckdb_get_hugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_hugeint"
+    c_duckdb_get_hugeint :: DuckDBValue -> Ptr DuckDBHugeInt -> IO ()
+
+{- | Returns the uhugeint value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a uhugeint
+
+Returns A duckdb_uhugeint, or MinValue if the value cannot be converted
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_uhugeint@
+but mirror the DuckDB C API semantics of @duckdb_get_uhugeint@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_uhugeint"
+    c_duckdb_get_uhugeint :: DuckDBValue -> Ptr DuckDBUHugeInt -> IO ()
+
+{- | Returns the duckdb_bignum value of the given value. The @data@ field must be
+destroyed with @duckdb_free@.
+
+Parameters:
+* @val@: A duckdb_value containing a BIGNUM
+
+Returns A duckdb_bignum. The @data@ field must be destroyed with
+@duckdb_free@.
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_bignum@ but
+mirror the DuckDB C API semantics of @duckdb_get_bignum@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_bignum"
+    c_duckdb_get_bignum :: DuckDBValue -> Ptr DuckDBBignum -> IO ()
+
+{- | Returns the duckdb_decimal value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a DECIMAL
+
+Returns A duckdb_decimal, or MinValue if the value cannot be converted
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_decimal@ but
+mirror the DuckDB C API semantics of @duckdb_get_decimal@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_decimal"
+    c_duckdb_get_decimal :: DuckDBValue -> Ptr DuckDBDecimal -> IO ()
+
+{- | Returns the float value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a float
+
+Returns A float, or NAN if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_float"
+    c_duckdb_get_float :: DuckDBValue -> IO CFloat
+
+{- | Returns the double value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a double
+
+Returns A double, or NAN if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_double"
+    c_duckdb_get_double :: DuckDBValue -> IO CDouble
+
+{- | Returns the date value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a date
+
+Returns A duckdb_date, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_date"
+    c_duckdb_get_date :: DuckDBValue -> IO DuckDBDate
+
+{- | Returns the time value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a time
+
+Returns A duckdb_time, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_time"
+    c_duckdb_get_time :: DuckDBValue -> IO DuckDBTime
+
+{- | Returns the time_ns value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a time_ns
+
+Returns A duckdb_time_ns, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_time_ns"
+    c_duckdb_get_time_ns :: DuckDBValue -> IO DuckDBTimeNs
+
+{- | Returns the time_tz value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a time_tz
+
+Returns A duckdb_time_tz, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_time_tz"
+    c_duckdb_get_time_tz :: DuckDBValue -> IO DuckDBTimeTz
+
+{- | Returns the TIMESTAMP value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a TIMESTAMP
+
+Returns A duckdb_timestamp, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_timestamp"
+    c_duckdb_get_timestamp :: DuckDBValue -> IO DuckDBTimestamp
+
+{- | Returns the TIMESTAMP_TZ value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a TIMESTAMP_TZ
+
+Returns A duckdb_timestamp, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_timestamp_tz"
+    c_duckdb_get_timestamp_tz :: DuckDBValue -> IO DuckDBTimestamp
+
+{- | Returns the duckdb_timestamp_s value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a TIMESTAMP_S
+
+Returns A duckdb_timestamp_s, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_timestamp_s"
+    c_duckdb_get_timestamp_s :: DuckDBValue -> IO DuckDBTimestampS
+
+{- | Returns the duckdb_timestamp_ms value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a TIMESTAMP_MS
+
+Returns A duckdb_timestamp_ms, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_timestamp_ms"
+    c_duckdb_get_timestamp_ms :: DuckDBValue -> IO DuckDBTimestampMs
+
+{- | Returns the duckdb_timestamp_ns value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a TIMESTAMP_NS
+
+Returns A duckdb_timestamp_ns, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_timestamp_ns"
+    c_duckdb_get_timestamp_ns :: DuckDBValue -> IO DuckDBTimestampNs
+
+{- | Returns the interval value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a interval
+
+Returns A duckdb_interval, or MinValue if the value cannot be converted
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_interval@
+but mirror the DuckDB C API semantics of @duckdb_get_interval@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_interval"
+    c_duckdb_get_interval :: DuckDBValue -> Ptr DuckDBInterval -> IO ()
+
+{- | Returns the type of the given value. The type is valid as long as the value is
+not destroyed. The type itself must not be destroyed.
+
+Parameters:
+* @val@: A duckdb_value
+
+Returns A duckdb_logical_type.
+-}
+foreign import ccall unsafe "duckdb_get_value_type"
+    c_duckdb_get_value_type :: DuckDBValue -> IO DuckDBLogicalType
+
+{- | Returns the blob value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a blob
+
+Returns A duckdb_blob
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_blob@ but
+mirror the DuckDB C API semantics of @duckdb_get_blob@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_blob"
+    c_duckdb_get_blob :: DuckDBValue -> Ptr DuckDBBlob -> IO ()
+
+{- | Returns the duckdb_bit value of the given value. The @data@ field must be
+destroyed with @duckdb_free@.
+
+Parameters:
+* @val@: A duckdb_value containing a BIT
+
+Returns A duckdb_bit
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_bit@ but
+mirror the DuckDB C API semantics of @duckdb_get_bit@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_bit"
+    c_duckdb_get_bit :: DuckDBValue -> Ptr DuckDBBit -> IO ()
+
+{- | Returns a duckdb_uhugeint representing the UUID value of the given value.
+
+Parameters:
+* @val@: A duckdb_value containing a UUID
+
+Returns A duckdb_uhugeint representing the UUID value
+
+These bindings call the wrapper symbol @wrapped_duckdb_get_uuid@ but
+mirror the DuckDB C API semantics of @duckdb_get_uuid@.
+-}
+foreign import ccall unsafe "wrapped_duckdb_get_uuid"
+    c_duckdb_get_uuid :: DuckDBValue -> Ptr DuckDBUHugeInt -> IO ()
+
+{- | Obtains a string representation of the given value. The result must be
+destroyed with @duckdb_free@.
+
+Parameters:
+* @value@: The value
+
+Returns The string value. This must be destroyed with @duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_get_varchar"
+    c_duckdb_get_varchar :: DuckDBValue -> IO CString
+
+{- | Creates a struct value from a type and an array of values. Must be destroyed
+with @duckdb_destroy_value@.
+
+Parameters:
+* @type@: The type of the struct
+* @values@: The values for the struct fields
+
+Returns The struct value, or nullptr, if any child type is @DUCKDB_TYPE_ANY@
+or @DUCKDB_TYPE_INVALID@.
+-}
+foreign import ccall unsafe "duckdb_create_struct_value"
+    c_duckdb_create_struct_value :: DuckDBLogicalType -> Ptr DuckDBValue -> IO DuckDBValue
+
+{- | Creates a list value from a child (element) type and an array of values of
+length @value_count@. Must be destroyed with @duckdb_destroy_value@.
+
+Parameters:
+* @type@: The type of the list
+* @values@: The values for the list
+* @value_count@: The number of values in the list
+
+Returns The list value, or nullptr, if the child type is @DUCKDB_TYPE_ANY@ or
+@DUCKDB_TYPE_INVALID@.
+-}
+foreign import ccall unsafe "duckdb_create_list_value"
+    c_duckdb_create_list_value :: DuckDBLogicalType -> Ptr DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates an array value from a child (element) type and an array of values of
+length @value_count@. Must be destroyed with @duckdb_destroy_value@.
+
+Parameters:
+* @type@: The type of the array
+* @values@: The values for the array
+* @value_count@: The number of values in the array
+
+Returns The array value, or nullptr, if the child type is @DUCKDB_TYPE_ANY@ or
+@DUCKDB_TYPE_INVALID@.
+-}
+foreign import ccall unsafe "duckdb_create_array_value"
+    c_duckdb_create_array_value :: DuckDBLogicalType -> Ptr DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates a map value from a map type and two arrays, one for the keys and one
+for the values, each of length @entry_count@. Must be destroyed with
+@duckdb_destroy_value@.
+
+Parameters:
+* @map_type@: The map type
+* @keys@: The keys of the map
+* @values@: The values of the map
+* @entry_count@: The number of entrys (key-value pairs) in the map
+
+Returns The map value, or nullptr, if the parameters are invalid.
+-}
+foreign import ccall unsafe "duckdb_create_map_value"
+    c_duckdb_create_map_value :: DuckDBLogicalType -> Ptr DuckDBValue -> Ptr DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates a union value from a union type, a tag index, and a value. Must be
+destroyed with @duckdb_destroy_value@.
+
+Parameters:
+* @union_type@: The union type
+* @tag_index@: The index of the tag of the union
+* @value@: The value of the union for that tag
+
+Returns The union value, or nullptr, if the parameters are invalid.
+-}
+foreign import ccall unsafe "duckdb_create_union_value"
+    c_duckdb_create_union_value :: DuckDBLogicalType -> DuckDBIdx -> DuckDBValue -> IO DuckDBValue
+
+{- | Returns the number of elements in a MAP value.
+
+Parameters:
+* @value@: The MAP value.
+
+Returns The number of elements in the map.
+-}
+foreign import ccall unsafe "duckdb_get_map_size"
+    c_duckdb_get_map_size :: DuckDBValue -> IO DuckDBIdx
+
+{- | Returns the MAP key at index as a duckdb_value.
+
+Parameters:
+* @value@: The MAP value.
+* @index@: The index of the key.
+
+Returns The key as a duckdb_value.
+-}
+foreign import ccall unsafe "duckdb_get_map_key"
+    c_duckdb_get_map_key :: DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Returns the MAP value at index as a duckdb_value.
+
+Parameters:
+* @value@: The MAP value.
+* @index@: The index of the value.
+
+Returns The value as a duckdb_value.
+-}
+foreign import ccall unsafe "duckdb_get_map_value"
+    c_duckdb_get_map_value :: DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Returns whether the value's type is SQLNULL or not.
+
+Parameters:
+* @value@: The value to check.
+
+Returns True, if the value's type is SQLNULL, otherwise false.
+-}
+foreign import ccall unsafe "duckdb_is_null_value"
+    c_duckdb_is_null_value :: DuckDBValue -> IO CBool
+
+{- | Creates a value of type SQLNULL.
+
+Returns The duckdb_value representing SQLNULL. This must be destroyed with
+@duckdb_destroy_value@.
+-}
+foreign import ccall unsafe "duckdb_create_null_value"
+    c_duckdb_create_null_value :: IO DuckDBValue
+
+{- | Returns the number of elements in a LIST value.
+
+Parameters:
+* @value@: The LIST value.
+
+Returns The number of elements in the list.
+-}
+foreign import ccall unsafe "duckdb_get_list_size"
+    c_duckdb_get_list_size :: DuckDBValue -> IO DuckDBIdx
+
+{- | Returns the LIST child at index as a duckdb_value.
+
+Parameters:
+* @value@: The LIST value.
+* @index@: The index of the child.
+
+Returns The child as a duckdb_value.
+-}
+foreign import ccall unsafe "duckdb_get_list_child"
+    c_duckdb_get_list_child :: DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Creates an enum value from a type and a value. Must be destroyed with
+@duckdb_destroy_value@.
+
+Parameters:
+* @type@: The type of the enum
+* @value@: The value for the enum
+
+Returns The enum value, or nullptr.
+-}
+foreign import ccall unsafe "duckdb_create_enum_value"
+    c_duckdb_create_enum_value :: DuckDBLogicalType -> Word64 -> IO DuckDBValue
+
+{- | Returns the enum value of the given value.
+
+Parameters:
+* @value@: A duckdb_value containing an enum
+
+Returns A uint64_t, or MinValue if the value cannot be converted
+-}
+foreign import ccall unsafe "duckdb_get_enum_value"
+    c_duckdb_get_enum_value :: DuckDBValue -> IO Word64
+
+{- | Returns the STRUCT child at index as a duckdb_value.
+
+Parameters:
+* @value@: The STRUCT value.
+* @index@: The index of the child.
+
+Returns The child as a duckdb_value.
+-}
+foreign import ccall unsafe "duckdb_get_struct_child"
+    c_duckdb_get_struct_child :: DuckDBValue -> DuckDBIdx -> IO DuckDBValue
+
+{- | Returns the SQL string representation of the given value.
+
+Parameters:
+* @value@: A duckdb_value.
+
+Returns The SQL string representation as a null-terminated string. The result
+must be freed with @duckdb_free@.
+-}
+foreign import ccall unsafe "duckdb_value_to_string"
+    c_duckdb_value_to_string :: DuckDBValue -> IO CString
diff --git a/src/Database/DuckDB/FFI/Vector.hs b/src/Database/DuckDB/FFI/Vector.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/DuckDB/FFI/Vector.hs
@@ -0,0 +1,248 @@
+module Database.DuckDB.FFI.Vector (
+    c_duckdb_create_vector,
+    c_duckdb_destroy_vector,
+    c_duckdb_vector_get_column_type,
+    c_duckdb_vector_get_data,
+    c_duckdb_vector_get_validity,
+    c_duckdb_vector_ensure_validity_writable,
+    c_duckdb_vector_assign_string_element,
+    c_duckdb_vector_assign_string_element_len,
+    c_duckdb_list_vector_get_child,
+    c_duckdb_list_vector_get_size,
+    c_duckdb_list_vector_set_size,
+    c_duckdb_list_vector_reserve,
+    c_duckdb_struct_vector_get_child,
+    c_duckdb_array_vector_get_child,
+    c_duckdb_slice_vector,
+    c_duckdb_vector_copy_sel,
+    c_duckdb_vector_reference_value,
+    c_duckdb_vector_reference_vector,
+) where
+
+import Data.Word (Word64)
+import Database.DuckDB.FFI.Types
+import Foreign.C.String (CString)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+
+{- | Creates a flat vector. Must be destroyed with @duckdb_destroy_vector@.
+
+Parameters:
+* @type@: The logical type of the vector.
+* @capacity@: The capacity of the vector.
+
+Returns The vector.
+-}
+foreign import ccall unsafe "duckdb_create_vector"
+    c_duckdb_create_vector :: DuckDBLogicalType -> DuckDBIdx -> IO DuckDBVector
+
+{- | Destroys the vector and de-allocates its memory.
+
+Parameters:
+* @vector@: A pointer to the vector.
+-}
+foreign import ccall unsafe "duckdb_destroy_vector"
+    c_duckdb_destroy_vector :: Ptr DuckDBVector -> IO ()
+
+{- | Retrieves the column type of the specified vector.
+
+The result must be destroyed with @duckdb_destroy_logical_type@.
+
+Parameters:
+* @vector@: The vector get the data from
+
+Returns The type of the vector
+-}
+foreign import ccall unsafe "duckdb_vector_get_column_type"
+    c_duckdb_vector_get_column_type :: DuckDBVector -> IO DuckDBLogicalType
+
+{- | Retrieves the data pointer of the vector.
+
+The data pointer can be used to read or write values from the vector. How to
+read or write values depends on the type of the vector.
+
+Parameters:
+* @vector@: The vector to get the data from
+
+Returns The data pointer
+-}
+foreign import ccall unsafe "duckdb_vector_get_data"
+    c_duckdb_vector_get_data :: DuckDBVector -> IO (Ptr ())
+
+{- | Retrieves the validity mask pointer of the specified vector.
+
+If all values are valid, this function MIGHT return NULL!
+
+The validity mask is a bitset that signifies null-ness within the data chunk.
+It is a series of uint64_t values, where each uint64_t value contains validity
+for 64 tuples. The bit is set to 1 if the value is valid (i.e., not NULL) or 0
+if the value is invalid (i.e., NULL).
+
+Validity of a specific value can be obtained like this:
+
+idx_t entry_idx = row_idx / 64; idx_t idx_in_entry = row_idx % 64; bool
+is_valid = validity_mask[entry_idx] & (1 << idx_in_entry);
+
+Alternatively, the (slower) duckdb_validity_row_is_valid function can be used.
+
+Parameters:
+* @vector@: The vector to get the data from
+
+Returns The pointer to the validity mask, or NULL if no validity mask is
+present
+-}
+foreign import ccall unsafe "duckdb_vector_get_validity"
+    c_duckdb_vector_get_validity :: DuckDBVector -> IO (Ptr Word64)
+
+{- | Ensures the validity mask is writable by allocating it.
+
+After this function is called, @duckdb_vector_get_validity@ will ALWAYS return
+non-NULL. This allows NULL values to be written to the vector, regardless of
+whether a validity mask was present before.
+
+Parameters:
+* @vector@: The vector to alter
+-}
+foreign import ccall unsafe "duckdb_vector_ensure_validity_writable"
+    c_duckdb_vector_ensure_validity_writable :: DuckDBVector -> IO ()
+
+{- | Assigns a string element in the vector at the specified location.
+
+Parameters:
+* @vector@: The vector to alter
+* @index@: The row position in the vector to assign the string to
+* @str@: The null-terminated string
+-}
+foreign import ccall unsafe "duckdb_vector_assign_string_element"
+    c_duckdb_vector_assign_string_element :: DuckDBVector -> DuckDBIdx -> CString -> IO ()
+
+{- | Assigns a string element in the vector at the specified location. You may also
+use this function to assign BLOBs.
+
+Parameters:
+* @vector@: The vector to alter
+* @index@: The row position in the vector to assign the string to
+* @str@: The string
+* @str_len@: The length of the string (in bytes)
+-}
+foreign import ccall unsafe "duckdb_vector_assign_string_element_len"
+    c_duckdb_vector_assign_string_element_len :: DuckDBVector -> DuckDBIdx -> CString -> DuckDBIdx -> IO ()
+
+{- | Retrieves the child vector of a list vector.
+
+The resulting vector is valid as long as the parent vector is valid.
+
+Parameters:
+* @vector@: The vector
+
+Returns The child vector
+-}
+foreign import ccall unsafe "duckdb_list_vector_get_child"
+    c_duckdb_list_vector_get_child :: DuckDBVector -> IO DuckDBVector
+
+{- | Returns the size of the child vector of the list.
+
+Parameters:
+* @vector@: The vector
+
+Returns The size of the child list
+-}
+foreign import ccall unsafe "duckdb_list_vector_get_size"
+    c_duckdb_list_vector_get_size :: DuckDBVector -> IO DuckDBIdx
+
+{- | Sets the total size of the underlying child-vector of a list vector.
+
+Parameters:
+* @vector@: The list vector.
+* @size@: The size of the child list.
+
+Returns The duckdb state. Returns DuckDBError if the vector is nullptr.
+-}
+foreign import ccall unsafe "duckdb_list_vector_set_size"
+    c_duckdb_list_vector_set_size :: DuckDBVector -> DuckDBIdx -> IO DuckDBState
+
+{- | Sets the total capacity of the underlying child-vector of a list.
+
+After calling this method, you must call @duckdb_vector_get_validity@ and
+@duckdb_vector_get_data@ to obtain current data and validity pointers
+
+Parameters:
+* @vector@: The list vector.
+* @required_capacity@: the total capacity to reserve.
+
+Returns The duckdb state. Returns DuckDBError if the vector is nullptr.
+-}
+foreign import ccall unsafe "duckdb_list_vector_reserve"
+    c_duckdb_list_vector_reserve :: DuckDBVector -> DuckDBIdx -> IO DuckDBState
+
+{- | Retrieves the child vector of a struct vector. The resulting vector is valid
+as long as the parent vector is valid.
+
+Parameters:
+* @vector@: The vector
+* @index@: The child index
+
+Returns The child vector
+-}
+foreign import ccall unsafe "duckdb_struct_vector_get_child"
+    c_duckdb_struct_vector_get_child :: DuckDBVector -> DuckDBIdx -> IO DuckDBVector
+
+{- | Retrieves the child vector of an array vector. The resulting vector is valid
+as long as the parent vector is valid. The resulting vector has the size of
+the parent vector multiplied by the array size.
+
+Parameters:
+* @vector@: The vector
+
+Returns The child vector
+-}
+foreign import ccall unsafe "duckdb_array_vector_get_child"
+    c_duckdb_array_vector_get_child :: DuckDBVector -> IO DuckDBVector
+
+{- | Slice a vector with a selection vector. The length of the selection vector
+must be less than or equal to the length of the vector. Turns the vector into
+a dictionary vector.
+
+Parameters:
+* @vector@: The vector to slice.
+* @sel@: The selection vector.
+* @len@: The length of the selection vector.
+-}
+foreign import ccall unsafe "duckdb_slice_vector"
+    c_duckdb_slice_vector :: DuckDBVector -> DuckDBSelectionVector -> DuckDBIdx -> IO ()
+
+{- | Copy the src vector to the dst with a selection vector that identifies which
+indices to copy.
+
+Parameters:
+* @src@: The vector to copy from.
+* @dst@: The vector to copy to.
+* @sel@: The selection vector. The length of the selection vector should not
+  be more than the length of the src vector
+* @src_count@: The number of entries from selection vector to copy. Think of
+  this as the effective length of the selection vector starting from index 0
+* @src_offset@: The offset in the selection vector to copy from (important:
+  actual number of items copied = src_count - src_offset).
+* @dst_offset@: The offset in the dst vector to start copying to.
+-}
+foreign import ccall unsafe "duckdb_vector_copy_sel"
+    c_duckdb_vector_copy_sel :: DuckDBVector -> DuckDBVector -> DuckDBSelectionVector -> DuckDBIdx -> DuckDBIdx -> DuckDBIdx -> IO ()
+
+{- | Copies the value from @value@ to @vector@.
+
+Parameters:
+* @vector@: The receiving vector.
+* @value@: The value to copy into the vector.
+-}
+foreign import ccall unsafe "duckdb_vector_reference_value"
+    c_duckdb_vector_reference_value :: DuckDBVector -> DuckDBValue -> IO ()
+
+{- | Changes @to_vector@ to reference `from_vector. After, the vectors share
+ownership of the data.
+
+Parameters:
+* @to_vector@: The receiving vector.
+* @from_vector@: The vector to reference.
+-}
+foreign import ccall unsafe "duckdb_vector_reference_vector"
+    c_duckdb_vector_reference_vector :: DuckDBVector -> DuckDBVector -> IO ()
diff --git a/test/AggregateFunctionsTest.hs b/test/AggregateFunctionsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/AggregateFunctionsTest.hs
@@ -0,0 +1,411 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module AggregateFunctionsTest (tests) where
+
+import Control.Concurrent (runInBoundThread)
+import Control.Exception (bracket)
+import Control.Monad (forM_, when)
+import Data.Int (Int32)
+import Data.List (isInfixOf)
+import Data.Word (Word64)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca, free, mallocBytes)
+import Foreign.Ptr (Ptr, castPtr, freeHaskellFunPtr, nullFunPtr, nullPtr)
+import Foreign.Storable (peek, peekElemOff, poke, pokeElemOff, sizeOf)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase, withLogicalType, withResultCString)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Aggregate Functions"
+        [ sumAggregate
+        , extraInfoAggregate
+        , aggregateFunctionSet
+        , aggregateErrorPropagation
+        , specialHandlingNulls
+        ]
+
+-- Test cases ----------------------------------------------------------------
+
+sumAggregate :: TestTree
+sumAggregate =
+    testCase "register custom aggregate and execute" $
+        runInBoundThread do
+            withDatabase \db ->
+                withConnection db \conn ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withAggregateFunction \aggFun ->
+                            withCallbacks \cbs -> do
+                                setupAggregateFunction aggFun cbs intType "haskell_sum"
+                                c_duckdb_register_aggregate_function conn aggFun >>= (@?= DuckDBSuccess)
+                                withCString "SELECT haskell_sum(v) FROM (VALUES (1), (2), (3)) t(v)" \sql ->
+                                    withResultCString conn sql \resPtr ->
+                                        c_duckdb_value_int32 resPtr 0 0 >>= (@?= 6)
+
+extraInfoAggregate :: TestTree
+extraInfoAggregate =
+    testCase "extra info is visible inside callbacks" $
+        runInBoundThread do
+            let config = defaultAggregateConfig{cfgBonus = 2}
+            withDatabase \db ->
+                withConnection db \conn ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withAggregateConfig config \configPtr ->
+                            withAggregateFunction \aggFun ->
+                                withCallbacks \cbs -> do
+                                    setupAggregateFunction aggFun cbs intType "haskell_bonus_sum"
+                                    c_duckdb_aggregate_function_set_extra_info aggFun configPtr nullFunPtr
+
+                                    c_duckdb_register_aggregate_function conn aggFun >>= (@?= DuckDBSuccess)
+
+                                    withCString "SELECT haskell_bonus_sum(v) FROM (VALUES (1), (2), (3)) t(v)" \sql ->
+                                        withResultCString conn sql \resPtr ->
+                                            c_duckdb_value_int32 resPtr 0 0 >>= (@?= 12)
+
+aggregateFunctionSet :: TestTree
+aggregateFunctionSet =
+    testCase "aggregate function set registers overload" $
+        runInBoundThread do
+            withDatabase \db ->
+                withConnection db \conn ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withAggregateFunction \aggFun ->
+                            withCallbacks \cbs -> do
+                                setupAggregateFunction aggFun cbs intType "haskell_sum_set"
+                                withCString "haskell_sum_set" \setName ->
+                                    withAggregateFunctionSet setName \set -> do
+                                        c_duckdb_add_aggregate_function_to_set set aggFun >>= (@?= DuckDBSuccess)
+                                        c_duckdb_register_aggregate_function_set conn set >>= (@?= DuckDBSuccess)
+
+                                        withCString "SELECT haskell_sum_set(v) FROM (VALUES (4), (5), (6)) t(v)" \sql ->
+                                            withResultCString conn sql \resPtr ->
+                                                c_duckdb_value_int32 resPtr 0 0 >>= (@?= 15)
+
+aggregateErrorPropagation :: TestTree
+aggregateErrorPropagation =
+    testCase "callbacks can signal errors" $
+        runInBoundThread do
+            let config = defaultAggregateConfig{cfgFailOnNegative = 1}
+            withDatabase \db ->
+                withConnection db \conn ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withAggregateConfig config \configPtr ->
+                            withAggregateFunction \aggFun ->
+                                withCallbacks \cbs -> do
+                                    setupAggregateFunction aggFun cbs intType "no_negatives"
+                                    c_duckdb_aggregate_function_set_extra_info aggFun configPtr nullFunPtr
+                                    c_duckdb_register_aggregate_function conn aggFun >>= (@?= DuckDBSuccess)
+
+                                    withCString "SELECT no_negatives(v) FROM (VALUES (1), (-5)) t(v)" \sql ->
+                                        alloca \resPtr -> do
+                                            errState <- c_duckdb_query conn sql resPtr
+                                            errState @?= DuckDBError
+
+                                            msgPtr <- c_duckdb_result_error resPtr
+                                            errMsg <- peekCString msgPtr
+                                            assertBool "expected negative error message" ("negatives not allowed" `isInfixOf` errMsg)
+
+                                            c_duckdb_destroy_result resPtr
+
+specialHandlingNulls :: TestTree
+specialHandlingNulls =
+    testCase "special handling allows returning NULL when all inputs are NULL" $
+        runInBoundThread do
+            let config = defaultAggregateConfig{cfgNullIfAllInvalid = 1}
+            withDatabase \db ->
+                withConnection db \conn ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withAggregateConfig config \configPtr ->
+                            withAggregateFunction \aggFun ->
+                                withCallbacks \cbs -> do
+                                    setupAggregateFunction aggFun cbs intType "nullable_sum"
+                                    c_duckdb_aggregate_function_set_extra_info aggFun configPtr nullFunPtr
+                                    c_duckdb_aggregate_function_set_special_handling aggFun
+                                    c_duckdb_register_aggregate_function conn aggFun >>= (@?= DuckDBSuccess)
+
+                                    withCString "SELECT nullable_sum(v) FROM (VALUES (CAST(NULL AS INTEGER))) t(v)" \sql ->
+                                        withResultCString conn sql \resPtr -> do
+                                            isNull <- c_duckdb_value_is_null resPtr 0 0
+                                            cbToBool isNull @?= True
+
+-- Aggregate state -----------------------------------------------------------
+
+data SumState = SumState
+    { ssTotal :: Int32
+    , ssSeen :: Int32
+    , ssNulls :: Int32
+    }
+
+initialSumState :: SumState
+initialSumState = SumState 0 0 0
+
+sumStateSize :: Int
+sumStateSize = 3 * sizeOf (undefined :: Int32)
+
+type SumStatePtr = Ptr Int32
+
+readSumState :: SumStatePtr -> IO SumState
+readSumState ptr = do
+    total <- peekElemOff ptr 0
+    seen <- peekElemOff ptr 1
+    nulls <- peekElemOff ptr 2
+    pure SumState{ssTotal = total, ssSeen = seen, ssNulls = nulls}
+
+writeSumState :: SumStatePtr -> SumState -> IO ()
+writeSumState ptr SumState{ssTotal = total, ssSeen = seen, ssNulls = nulls} = do
+    pokeElemOff ptr 0 total
+    pokeElemOff ptr 1 seen
+    pokeElemOff ptr 2 nulls
+
+-- Aggregate configuration ---------------------------------------------------
+
+data AggregateConfig = AggregateConfig
+    { cfgBonus :: Int32
+    , cfgFailOnNegative :: Int32
+    , cfgReturnNullCount :: Int32
+    , cfgNullIfAllInvalid :: Int32
+    }
+
+defaultAggregateConfig :: AggregateConfig
+defaultAggregateConfig = AggregateConfig 0 0 0 0
+
+type AggregateConfigPtr = Ptr Int32
+
+aggregateConfigSize :: Int
+aggregateConfigSize = 4 * sizeOf (undefined :: Int32)
+
+writeAggregateConfig :: AggregateConfigPtr -> AggregateConfig -> IO ()
+writeAggregateConfig ptr AggregateConfig{cfgBonus = bonus, cfgFailOnNegative = failNeg, cfgReturnNullCount = returnNulls, cfgNullIfAllInvalid = nullAll} = do
+    pokeElemOff ptr 0 bonus
+    pokeElemOff ptr 1 failNeg
+    pokeElemOff ptr 2 returnNulls
+    pokeElemOff ptr 3 nullAll
+
+peekAggregateConfig :: AggregateConfigPtr -> IO AggregateConfig
+peekAggregateConfig ptr = do
+    bonus <- peekElemOff ptr 0
+    failNeg <- peekElemOff ptr 1
+    returnNulls <- peekElemOff ptr 2
+    nullAll <- peekElemOff ptr 3
+    pure
+        AggregateConfig
+            { cfgBonus = bonus
+            , cfgFailOnNegative = failNeg
+            , cfgReturnNullCount = returnNulls
+            , cfgNullIfAllInvalid = nullAll
+            }
+
+configFromInfo :: DuckDBFunctionInfo -> IO AggregateConfig
+configFromInfo info = do
+    raw <- c_duckdb_aggregate_function_get_extra_info info
+    if raw == nullPtr
+        then pure defaultAggregateConfig
+        else peekAggregateConfig (castPtr raw)
+
+withAggregateConfig :: AggregateConfig -> (Ptr () -> IO a) -> IO a
+withAggregateConfig cfg action =
+    bracket acquire freeConfig (action . castPtr)
+  where
+    acquire = do
+        ptr <- mallocBytes aggregateConfigSize :: IO AggregateConfigPtr
+        writeAggregateConfig ptr cfg
+        pure ptr
+    freeConfig = free
+
+shouldFailOnNegative :: AggregateConfig -> Bool
+shouldFailOnNegative AggregateConfig{cfgFailOnNegative = flag} = flag /= 0
+
+shouldReturnNullCount :: AggregateConfig -> Bool
+shouldReturnNullCount AggregateConfig{cfgReturnNullCount = flag} = flag /= 0
+
+shouldMarkNullWhenAllInvalid :: AggregateConfig -> Bool
+shouldMarkNullWhenAllInvalid AggregateConfig{cfgNullIfAllInvalid = flag} = flag /= 0
+
+-- Callback implementations --------------------------------------------------
+
+data Callbacks = Callbacks
+    { cbStateSize :: DuckDBAggregateStateSizeFun
+    , cbInit :: DuckDBAggregateInitFun
+    , cbUpdate :: DuckDBAggregateUpdateFun
+    , cbCombine :: DuckDBAggregateCombineFun
+    , cbFinalize :: DuckDBAggregateFinalizeFun
+    , cbDestroy :: DuckDBAggregateDestroyFun
+    }
+
+withCallbacks :: (Callbacks -> IO a) -> IO a
+withCallbacks = bracket acquire release
+  where
+    acquire = do
+        sizeFun <- mkStateSizeFun stateSizeFun
+        initFun <- mkInitFun initCallback
+        updateFun <- mkUpdateFun updateCallback
+        combineFun <- mkCombineFun combineCallback
+        finalizeFun <- mkFinalizeFun finalizeCallback
+        destroyFun <- mkDestroyFun destroyCallback
+        pure
+            Callbacks
+                { cbStateSize = sizeFun
+                , cbInit = initFun
+                , cbUpdate = updateFun
+                , cbCombine = combineFun
+                , cbFinalize = finalizeFun
+                , cbDestroy = destroyFun
+                }
+    release Callbacks{cbStateSize = sizeFun, cbInit = initFun, cbUpdate = updateFun, cbCombine = combineFun, cbFinalize = finalizeFun, cbDestroy = destroyFun} = do
+        freeHaskellFunPtr sizeFun
+        freeHaskellFunPtr initFun
+        freeHaskellFunPtr updateFun
+        freeHaskellFunPtr combineFun
+        freeHaskellFunPtr finalizeFun
+        freeHaskellFunPtr destroyFun
+
+setupAggregateFunction :: DuckDBAggregateFunction -> Callbacks -> DuckDBLogicalType -> String -> IO ()
+setupAggregateFunction aggFun Callbacks{cbStateSize = sizeFun, cbInit = initFun, cbUpdate = updateFun, cbCombine = combineFun, cbFinalize = finalizeFun, cbDestroy = destroyFun} intType name = do
+    withCString name $ \cname -> c_duckdb_aggregate_function_set_name aggFun cname
+    c_duckdb_aggregate_function_add_parameter aggFun intType
+    c_duckdb_aggregate_function_set_return_type aggFun intType
+    c_duckdb_aggregate_function_set_functions aggFun sizeFun initFun updateFun combineFun finalizeFun
+    c_duckdb_aggregate_function_set_destructor aggFun destroyFun
+
+stateSizeFun :: DuckDBFunctionInfo -> IO DuckDBIdx
+stateSizeFun _ = pure (fromIntegral (sizeOf (nullPtr :: Ptr ())))
+
+initCallback :: DuckDBFunctionInfo -> DuckDBAggregateState -> IO ()
+initCallback _ state = do
+    raw <- c_duckdb_malloc (fromIntegral sumStateSize)
+    let storage = castPtr raw :: SumStatePtr
+    writeSumState storage initialSumState
+    writeStateValuePtr state storage
+
+updateCallback :: DuckDBFunctionInfo -> DuckDBDataChunk -> Ptr DuckDBAggregateState -> IO ()
+updateCallback info chunk stateArrayPtr = do
+    rowCount <- c_duckdb_data_chunk_get_size chunk
+    vec <- c_duckdb_data_chunk_get_vector chunk 0
+    dataPtr <- vectorDataPtr vec
+    validity <- c_duckdb_vector_get_validity vec
+    config <- configFromInfo info
+
+    let rowCountInt = fromIntegral rowCount
+    forM_ [0 .. rowCountInt - 1] \i -> do
+        statePtr <- peekElemOff stateArrayPtr i
+        storage <- readStateValuePtr statePtr
+        current <- readSumState storage
+        let seen' = ssSeen current + 1
+            idx = fromIntegral i
+
+        isValid <- rowIsValid validity idx
+        if not isValid
+            then writeSumState storage current{ssSeen = seen', ssNulls = ssNulls current + 1}
+            else do
+                value <- peekElemOff dataPtr i
+                if shouldFailOnNegative config && value < 0
+                    then do
+                        withCString "negatives not allowed" $ \errMsg ->
+                            c_duckdb_aggregate_function_set_error info errMsg
+                        writeSumState storage current{ssSeen = seen'}
+                    else
+                        let total' = ssTotal current + value + cfgBonus config
+                         in writeSumState storage SumState{ssTotal = total', ssSeen = seen', ssNulls = ssNulls current}
+
+combineCallback :: DuckDBFunctionInfo -> Ptr DuckDBAggregateState -> Ptr DuckDBAggregateState -> DuckDBIdx -> IO ()
+combineCallback _ sourceStates targetStates count =
+    forM_ [0 .. fromIntegral count - 1] \i -> do
+        sourceState <- peekElemOff sourceStates i
+        targetState <- peekElemOff targetStates i
+        sourcePtr <- readStateValuePtr sourceState
+        targetPtr <- readStateValuePtr targetState
+        source <- readSumState sourcePtr
+        target <- readSumState targetPtr
+
+        let combined =
+                SumState
+                    { ssTotal = ssTotal source + ssTotal target
+                    , ssSeen = ssSeen source + ssSeen target
+                    , ssNulls = ssNulls source + ssNulls target
+                    }
+        writeSumState targetPtr combined
+
+finalizeCallback :: DuckDBFunctionInfo -> Ptr DuckDBAggregateState -> DuckDBVector -> DuckDBIdx -> DuckDBIdx -> IO ()
+finalizeCallback info stateArrayPtr outVec _ offset = do
+    state <- peekElemOff stateArrayPtr (fromIntegral offset)
+    storage <- readStateValuePtr state
+    SumState{ssTotal = total, ssSeen = seen, ssNulls = nulls} <- readSumState storage
+    config <- configFromInfo info
+
+    outPtr <- vectorDataPtr outVec
+    let resultValue = if shouldReturnNullCount config then nulls else total
+    pokeElemOff outPtr (fromIntegral offset) resultValue
+
+    let allInvalid = seen > 0 && seen == nulls
+    when (shouldMarkNullWhenAllInvalid config && allInvalid) do
+        c_duckdb_vector_ensure_validity_writable outVec
+        validity <- c_duckdb_vector_get_validity outVec
+        c_duckdb_validity_set_row_invalid validity offset
+
+destroyCallback :: Ptr DuckDBAggregateState -> DuckDBIdx -> IO ()
+destroyCallback states count =
+    forM_ [0 .. fromIntegral count - 1] \i -> do
+        state <- peekElemOff states i
+        storage <- readStateValuePtr state
+        when (storage /= nullPtr) $
+            c_duckdb_free (castPtr storage)
+
+-- Helper pointer accessors --------------------------------------------------
+
+writeStateValuePtr :: DuckDBAggregateState -> SumStatePtr -> IO ()
+writeStateValuePtr state ptr =
+    poke (castPtr state :: Ptr (Ptr ())) (castPtr ptr)
+
+readStateValuePtr :: DuckDBAggregateState -> IO SumStatePtr
+readStateValuePtr state = do
+    raw <- peek (castPtr state :: Ptr (Ptr ()))
+    pure (castPtr raw)
+
+vectorDataPtr :: DuckDBVector -> IO (Ptr Int32)
+vectorDataPtr vec = castPtr <$> c_duckdb_vector_get_data vec
+
+rowIsValid :: Ptr Word64 -> DuckDBIdx -> IO Bool
+rowIsValid validity idx
+    | validity == nullPtr = pure True
+    | otherwise = cbToBool <$> c_duckdb_validity_row_is_valid validity idx
+
+cbToBool :: CBool -> Bool
+cbToBool (CBool v) = v /= 0
+
+-- Wrapper builders ----------------------------------------------------------
+
+foreign import ccall safe "wrapper"
+    mkStateSizeFun :: (DuckDBFunctionInfo -> IO DuckDBIdx) -> IO DuckDBAggregateStateSizeFun
+
+foreign import ccall safe "wrapper"
+    mkInitFun :: (DuckDBFunctionInfo -> DuckDBAggregateState -> IO ()) -> IO DuckDBAggregateInitFun
+
+foreign import ccall safe "wrapper"
+    mkUpdateFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> Ptr DuckDBAggregateState -> IO ()) -> IO DuckDBAggregateUpdateFun
+
+foreign import ccall safe "wrapper"
+    mkCombineFun :: (DuckDBFunctionInfo -> Ptr DuckDBAggregateState -> Ptr DuckDBAggregateState -> DuckDBIdx -> IO ()) -> IO DuckDBAggregateCombineFun
+
+foreign import ccall safe "wrapper"
+    mkFinalizeFun :: (DuckDBFunctionInfo -> Ptr DuckDBAggregateState -> DuckDBVector -> DuckDBIdx -> DuckDBIdx -> IO ()) -> IO DuckDBAggregateFinalizeFun
+
+foreign import ccall safe "wrapper"
+    mkDestroyFun :: (Ptr DuckDBAggregateState -> DuckDBIdx -> IO ()) -> IO DuckDBAggregateDestroyFun
+
+-- Resource helpers ----------------------------------------------------------
+
+withAggregateFunction :: (DuckDBAggregateFunction -> IO a) -> IO a
+withAggregateFunction = bracket c_duckdb_create_aggregate_function destroy
+  where
+    destroy fun = alloca \ptr -> poke ptr fun >> c_duckdb_destroy_aggregate_function ptr
+
+withAggregateFunctionSet :: CString -> (DuckDBAggregateFunctionSet -> IO a) -> IO a
+withAggregateFunctionSet name = bracket acquire release
+  where
+    acquire = c_duckdb_create_aggregate_function_set name
+    release set = alloca \ptr -> poke ptr set >> c_duckdb_destroy_aggregate_function_set ptr
diff --git a/test/AppenderTest.hs b/test/AppenderTest.hs
new file mode 100644
--- /dev/null
+++ b/test/AppenderTest.hs
@@ -0,0 +1,553 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE TypeApplications #-}
+
+module AppenderTest (tests) where
+
+import Control.Exception (bracket, finally)
+import Control.Monad (forM_, when, (>=>))
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.List (isInfixOf)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString, withCStringLen)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray, withArray)
+import Foreign.Marshal.Utils (with)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek, poke, pokeElemOff)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (
+    destroyErrorData,
+    withConnection,
+    withDatabase,
+    withDuckValue,
+    withLogicalType,
+    withResultCString,
+ )
+
+tests :: TestTree
+tests =
+    testGroup
+        "Appender"
+        [ appenderRowwiseLifecycle
+        , appenderColumnSubset
+        , appenderDataChunkInsert
+        , appenderQueryAppender
+        , appenderNumericAndFloatScalars
+        , appenderTemporalTypes
+        , appenderStringAndBlob
+        , appenderChunkDefaults
+        , appenderErrorDataInspection
+        ]
+
+appenderRowwiseLifecycle :: TestTree
+appenderRowwiseLifecycle =
+    testCase "append rows using scalar APIs and inspect results" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement conn "CREATE TABLE appender_demo(id INTEGER, name VARCHAR, active BOOLEAN DEFAULT TRUE)"
+
+                withTableAppender conn "appender_demo" \app -> do
+                    c_duckdb_appender_column_count app >>= (@?= 3)
+                    checkColumnType app 0 DuckDBTypeInteger
+                    checkColumnType app 1 DuckDBTypeVarchar
+                    checkColumnType app 2 DuckDBTypeBoolean
+
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int32 app 1 >>= (@?= DuckDBSuccess)
+                    withCString "alice" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                    c_duckdb_append_bool app (CBool 1) >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int32 app 2 >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_null app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_bool app (CBool 0) >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int32 app 3 >>= (@?= DuckDBSuccess)
+                    withDuckValue (withCString "via_value" c_duckdb_create_varchar) (c_duckdb_append_value app >=> (@?= DuckDBSuccess))
+                    c_duckdb_append_default app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    errPtr0 <- c_duckdb_appender_error app
+                    when (errPtr0 /= nullPtr) $ do
+                        msg <- peekCString errPtr0
+                        msg @?= ""
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                    withCString "SELECT id, name, active FROM appender_demo ORDER BY id" \sql ->
+                        withResultCString conn sql \resPtr -> do
+                            c_duckdb_row_count resPtr >>= (@?= 3)
+
+                            c_duckdb_value_int32 resPtr 0 0 >>= (@?= 1)
+                            fetchString resPtr 1 0 >>= (@?= "alice")
+                            fetchBool resPtr 2 0 >>= (@?= True)
+
+                            c_duckdb_value_int32 resPtr 0 1 >>= (@?= 2)
+                            c_duckdb_value_is_null resPtr 1 1 >>= (@?= CBool 1)
+                            fetchBool resPtr 2 1 >>= (@?= False)
+
+                            c_duckdb_value_int32 resPtr 0 2 >>= (@?= 3)
+                            fetchString resPtr 1 2 >>= (@?= "via_value")
+                            fetchBool resPtr 2 2 >>= (@?= True)
+
+appenderColumnSubset :: TestTree
+appenderColumnSubset =
+    testCase "restrict active columns and rely on table defaults" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE subset_demo(id INTEGER DEFAULT 100, name VARCHAR, note VARCHAR, active BOOLEAN DEFAULT FALSE)"
+
+                withTableAppender conn "subset_demo" \app -> do
+                    c_duckdb_appender_clear_columns app >>= (@?= DuckDBSuccess)
+                    withCString "name" (c_duckdb_appender_add_column app >=> (@?= DuckDBSuccess))
+                    withCString "note" (c_duckdb_appender_add_column app >=> (@?= DuckDBSuccess))
+
+                    c_duckdb_appender_column_count app >>= (@?= 2)
+
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    withCString "subset-one" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                    withCString "note one" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    withCString "subset-two" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                    c_duckdb_append_null app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                    withCString "SELECT id, name, note, active FROM subset_demo ORDER BY rowid" \sql ->
+                        withResultCString conn sql \resPtr -> do
+                            c_duckdb_row_count resPtr >>= (@?= 2)
+
+                            c_duckdb_value_int32 resPtr 0 0 >>= (@?= 100)
+                            fetchString resPtr 1 0 >>= (@?= "subset-one")
+                            fetchString resPtr 2 0 >>= (@?= "note one")
+                            fetchBool resPtr 3 0 >>= (@?= False)
+
+                            c_duckdb_value_int32 resPtr 0 1 >>= (@?= 100)
+                            fetchString resPtr 1 1 >>= (@?= "subset-two")
+                            c_duckdb_value_is_null resPtr 2 1 >>= (@?= CBool 1)
+                            fetchBool resPtr 3 1 >>= (@?= False)
+
+appenderDataChunkInsert :: TestTree
+appenderDataChunkInsert =
+    testCase "append via data chunk using extended constructor" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement conn "CREATE TABLE chunk_demo(id INTEGER, label VARCHAR)"
+
+                withTableAppenderExt conn "chunk_demo" \app -> do
+                    c_duckdb_appender_column_count app >>= (@?= 2)
+
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \textType ->
+                            withArray [intType, textType] \typeArray ->
+                                withDataChunk (c_duckdb_create_data_chunk typeArray 2) \chunk -> do
+                                    intVec <- c_duckdb_data_chunk_get_vector chunk 0
+                                    fillIntVector intVec [10, 11]
+
+                                    textVec <- c_duckdb_data_chunk_get_vector chunk 1
+                                    assignStrings textVec ["ten", "eleven"]
+
+                                    c_duckdb_data_chunk_set_size chunk 2
+                                    c_duckdb_append_data_chunk app chunk >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                    withCString "SELECT id, label FROM chunk_demo ORDER BY id" \sql ->
+                        withResultCString conn sql \resPtr -> do
+                            c_duckdb_row_count resPtr >>= (@?= 2)
+                            c_duckdb_value_int32 resPtr 0 0 >>= (@?= 10)
+                            fetchString resPtr 1 0 >>= (@?= "ten")
+                            c_duckdb_value_int32 resPtr 0 1 >>= (@?= 11)
+                            fetchString resPtr 1 1 >>= (@?= "eleven")
+
+appenderQueryAppender :: TestTree
+appenderQueryAppender =
+    testCase "append rows through query-based appender" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement conn "CREATE TABLE query_target(id INTEGER, label VARCHAR)"
+
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \textType -> do
+                        let types = [intType, textType]
+                        withQueryAppender conn "INSERT INTO query_target SELECT * FROM appended_data" types \app -> do
+                            c_duckdb_appender_column_count app >>= (@?= 2)
+
+                            c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                            c_duckdb_append_int32 app 21 >>= (@?= DuckDBSuccess)
+                            withCString "twenty-one" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                            c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                            c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                            c_duckdb_append_int32 app 22 >>= (@?= DuckDBSuccess)
+                            withCString "twenty-two" (c_duckdb_append_varchar app >=> (@?= DuckDBSuccess))
+                            c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                            c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                            c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                        withCString "SELECT id, label FROM query_target ORDER BY id" \sql ->
+                            withResultCString conn sql \resPtr -> do
+                                c_duckdb_row_count resPtr >>= (@?= 2)
+                                c_duckdb_value_int32 resPtr 0 0 >>= (@?= 21)
+                                fetchString resPtr 1 0 >>= (@?= "twenty-one")
+                                c_duckdb_value_int32 resPtr 0 1 >>= (@?= 22)
+                                fetchString resPtr 1 1 >>= (@?= "twenty-two")
+
+appenderNumericAndFloatScalars :: TestTree
+appenderNumericAndFloatScalars =
+    testCase "append numeric scalar types" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    ( unlines
+                        [ "CREATE TABLE numeric_scalars_demo("
+                        , "  signed8 TINYINT,"
+                        , "  signed16 SMALLINT,"
+                        , "  signed64 BIGINT,"
+                        , "  signed128 HUGEINT,"
+                        , "  unsigned8 UTINYINT,"
+                        , "  unsigned16 USMALLINT,"
+                        , "  unsigned32 UINTEGER,"
+                        , "  unsigned64 UBIGINT,"
+                        , "  unsigned128 UHUGEINT,"
+                        , "  real32 FLOAT,"
+                        , "  real64 DOUBLE"
+                        , ")"
+                        ]
+                    )
+
+                let int8Val = (-12) :: Int8
+                    int16Val = (-32000) :: Int16
+                    int64Val = (-9876543210) :: Int64
+                    hugeVal = DuckDBHugeInt 0xFEDCBA9876543210 0
+                    uint8Val = 200 :: Word8
+                    uint16Val = 60000 :: Word16
+                    uint32Val = 4000000000 :: Word32
+                    uint64Val = 12345678901234567890 :: Word64
+                    uhugeVal = DuckDBUHugeInt 0x0123456789ABCDEF 0x0011223344556677
+                    floatVal = (-12.5) :: Float
+                    doubleVal = 1234.5678 :: Double
+
+                withTableAppender conn "numeric_scalars_demo" \app -> do
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int8 app int8Val >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int16 app int16Val >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int64 app int64Val >>= (@?= DuckDBSuccess)
+                    with hugeVal (c_duckdb_append_hugeint app >=> (@?= DuckDBSuccess))
+                    c_duckdb_append_uint8 app uint8Val >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_uint16 app uint16Val >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_uint32 app uint32Val >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_uint64 app uint64Val >>= (@?= DuckDBSuccess)
+                    with uhugeVal (c_duckdb_append_uhugeint app >=> (@?= DuckDBSuccess))
+                    c_duckdb_append_float app (realToFrac floatVal) >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_double app (realToFrac doubleVal) >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                let query =
+                        unlines
+                            [ "SELECT"
+                            , "  signed8,"
+                            , "  signed16,"
+                            , "  signed64,"
+                            , "  signed128,"
+                            , "  unsigned8,"
+                            , "  unsigned16,"
+                            , "  unsigned32,"
+                            , "  unsigned64,"
+                            , "  unsigned128,"
+                            , "  real32,"
+                            , "  real64"
+                            , "FROM numeric_scalars_demo"
+                            ]
+
+                withCString query \sql ->
+                    withResultCString conn sql \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 1)
+                        c_duckdb_value_int8 resPtr 0 0 >>= (@?= int8Val)
+                        c_duckdb_value_int16 resPtr 1 0 >>= (@?= int16Val)
+                        c_duckdb_value_int64 resPtr 2 0 >>= (@?= int64Val)
+                        alloca \ptr -> do
+                            c_duckdb_value_hugeint resPtr 3 0 ptr
+                            peek ptr >>= (@?= hugeVal)
+                        c_duckdb_value_uint8 resPtr 4 0 >>= (@?= uint8Val)
+                        c_duckdb_value_uint16 resPtr 5 0 >>= (@?= uint16Val)
+                        c_duckdb_value_uint32 resPtr 6 0 >>= (@?= uint32Val)
+                        c_duckdb_value_uint64 resPtr 7 0 >>= (@?= uint64Val)
+                        alloca \ptr -> do
+                            c_duckdb_value_uhugeint resPtr 8 0 ptr
+                            peek ptr >>= (@?= uhugeVal)
+                        fv <- c_duckdb_value_float resPtr 9 0
+                        realToFrac fv @?= floatVal
+                        dv <- c_duckdb_value_double resPtr 10 0
+                        realToFrac dv @?= doubleVal
+
+appenderTemporalTypes :: TestTree
+appenderTemporalTypes =
+    testCase "append temporal values" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE temporal_demo(d DATE, t TIME, ts TIMESTAMP, iv INTERVAL)"
+
+                let dateStruct = DuckDBDateStruct 2024 3 31
+                dateVal <- with dateStruct c_duckdb_to_date
+
+                let timeStruct = DuckDBTimeStruct 12 34 56 987654
+                timeVal <- with timeStruct c_duckdb_to_time
+
+                let timestampStruct = DuckDBTimestampStruct dateStruct timeStruct
+                timestampVal <- with timestampStruct c_duckdb_to_timestamp
+
+                let intervalVal = DuckDBInterval 5 12 3456789
+
+                withTableAppender conn "temporal_demo" \app -> do
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_date app dateVal >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_time app timeVal >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_timestamp app timestampVal >>= (@?= DuckDBSuccess)
+                    with intervalVal (c_duckdb_append_interval app >=> (@?= DuckDBSuccess))
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                withCString "SELECT d, t, ts, iv FROM temporal_demo" \sql ->
+                    withResultCString conn sql \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 1)
+
+                        fetchedDate <- c_duckdb_value_date resPtr 0 0
+                        alloca \structPtr -> do
+                            c_duckdb_from_date fetchedDate structPtr
+                            peek structPtr >>= (@?= dateStruct)
+
+                        fetchedTime <- c_duckdb_value_time resPtr 1 0
+                        alloca \structPtr -> do
+                            c_duckdb_from_time fetchedTime structPtr
+                            peek structPtr >>= (@?= timeStruct)
+
+                        fetchedTimestamp <- c_duckdb_value_timestamp resPtr 2 0
+                        alloca \structPtr -> do
+                            c_duckdb_from_timestamp fetchedTimestamp structPtr
+                            peek structPtr >>= (@?= timestampStruct)
+
+                        alloca \intervalPtr -> do
+                            c_duckdb_value_interval resPtr 3 0 intervalPtr
+                            peek intervalPtr >>= (@?= intervalVal)
+
+appenderStringAndBlob :: TestTree
+appenderStringAndBlob =
+    testCase "append bounded varchar and blob values" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE string_blob_demo(text_fragment VARCHAR, payload BLOB)"
+
+                let blobBytes = [0xde, 0xad, 0xbe, 0xef] :: [Word8]
+
+                withTableAppender conn "string_blob_demo" \app -> do
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    withCStringLen "hello world" \(ptr, _len) -> do
+                        c_duckdb_append_varchar_length app ptr (fromIntegral @Integer 5) >>= (@?= DuckDBSuccess)
+                    withArray blobBytes \ptr -> do
+                        c_duckdb_append_blob app (castPtr ptr) (fromIntegral (length blobBytes)) >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                withCString "SELECT text_fragment, payload FROM string_blob_demo" \sql ->
+                    withResultCString conn sql \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 1)
+                        fetchString resPtr 0 0 >>= (@?= "hello")
+                        fetchBlob resPtr 1 0 >>= (@?= blobBytes)
+
+appenderChunkDefaults :: TestTree
+appenderChunkDefaults =
+    testCase "populate chunk entries using column defaults" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE chunk_defaults_demo(val INTEGER DEFAULT 99, note VARCHAR DEFAULT 'fallback')"
+
+                withTableAppender conn "chunk_defaults_demo" \app -> do
+                    c_duckdb_appender_column_count app >>= (@?= 2)
+
+                    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+                        withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \textType ->
+                            withArray [intType, textType] \typeArray ->
+                                withDataChunk (c_duckdb_create_data_chunk typeArray 2) \chunk -> do
+                                    intVec <- c_duckdb_data_chunk_get_vector chunk 0
+                                    fillIntVector intVec [10]
+
+                                    textVec <- c_duckdb_data_chunk_get_vector chunk 1
+                                    assignStrings textVec ["explicit"]
+
+                                    c_duckdb_append_default_to_chunk app chunk 0 1 >>= (@?= DuckDBSuccess)
+                                    c_duckdb_append_default_to_chunk app chunk 1 1 >>= (@?= DuckDBSuccess)
+
+                                    c_duckdb_data_chunk_set_size chunk 2
+                                    c_duckdb_append_data_chunk app chunk >>= (@?= DuckDBSuccess)
+
+                    c_duckdb_appender_flush app >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_close app >>= (@?= DuckDBSuccess)
+
+                withCString "SELECT val, note FROM chunk_defaults_demo ORDER BY rowid" \sql ->
+                    withResultCString conn sql \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 2)
+                        c_duckdb_value_int32 resPtr 0 0 >>= (@?= 10)
+                        fetchString resPtr 1 0 >>= (@?= "explicit")
+                        c_duckdb_value_int32 resPtr 0 1 >>= (@?= 99)
+                        fetchString resPtr 1 1 >>= (@?= "fallback")
+
+appenderErrorDataInspection :: TestTree
+appenderErrorDataInspection =
+    testCase "retrieve appender error payloads" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE error_demo(val INTEGER CHECK (val > 0))"
+
+                withTableAppender conn "error_demo" \app -> do
+                    c_duckdb_appender_begin_row app >>= (@?= DuckDBSuccess)
+                    c_duckdb_append_int32 app (-1) >>= (@?= DuckDBSuccess)
+                    c_duckdb_appender_end_row app >>= (@?= DuckDBSuccess)
+
+                    flushState <- c_duckdb_appender_flush app
+                    flushState @?= DuckDBError
+
+                    errData <- c_duckdb_appender_error_data app
+                    CBool hasError <- c_duckdb_error_data_has_error errData
+                    assertBool "error data indicates failure" (hasError /= 0)
+
+                    errType <- c_duckdb_error_data_error_type errData
+                    errType @?= DuckDBErrorConstraint
+
+                    errMsgPtr <- c_duckdb_error_data_message errData
+                    errMsg <- peekCString errMsgPtr
+                    assertBool "constraint violation message mentions CHECK" ("CHECK" `isInfixOf` errMsg)
+
+                    destroyErrorData errData
+
+                withCString "SELECT COUNT(*) FROM error_demo" \sql ->
+                    withResultCString conn sql \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 1)
+                        c_duckdb_value_int64 resPtr 0 0 >>= (@?= 0)
+
+-- Helpers -------------------------------------------------------------------
+
+runStatement :: DuckDBConnection -> String -> IO ()
+runStatement conn sql =
+    withCString sql \sqlPtr ->
+        alloca \resPtr -> do
+            state <- c_duckdb_query conn sqlPtr resPtr
+            if state == DuckDBSuccess
+                then c_duckdb_destroy_result resPtr
+                else do
+                    errPtr <- c_duckdb_result_error resPtr
+                    errMsg <-
+                        if errPtr == nullPtr
+                            then pure "unknown error"
+                            else peekCString errPtr
+                    c_duckdb_destroy_result resPtr
+                    assertFailure ("duckdb_query failed: " <> errMsg)
+
+withTableAppender :: DuckDBConnection -> String -> (DuckDBAppender -> IO a) -> IO a
+withTableAppender conn tableName action =
+    withCString tableName \tablePtr ->
+        withAppenderAcquire
+            (c_duckdb_appender_create conn nullPtr tablePtr)
+            action
+
+withTableAppenderExt :: DuckDBConnection -> String -> (DuckDBAppender -> IO a) -> IO a
+withTableAppenderExt conn tableName action =
+    withCString tableName \tablePtr ->
+        withAppenderAcquire
+            (c_duckdb_appender_create_ext conn nullPtr nullPtr tablePtr)
+            action
+
+withQueryAppender :: DuckDBConnection -> String -> [DuckDBLogicalType] -> (DuckDBAppender -> IO a) -> IO a
+withQueryAppender conn query types action =
+    withCString query \queryPtr ->
+        withArray types \typeArray ->
+            withAppenderAcquire
+                (c_duckdb_appender_create_query conn queryPtr (fromIntegral (length types)) typeArray nullPtr nullPtr)
+                action
+
+withAppenderAcquire :: (Ptr DuckDBAppender -> IO DuckDBState) -> (DuckDBAppender -> IO a) -> IO a
+withAppenderAcquire acquire action =
+    alloca \appPtr -> do
+        state <- acquire appPtr
+        state @?= DuckDBSuccess
+        app <- peek appPtr
+        let release = do
+                destroyState <- c_duckdb_appender_destroy appPtr
+                assertBool "destroy returns success or error" (destroyState == DuckDBSuccess || destroyState == DuckDBError)
+        action app `finally` release
+
+withDataChunk :: IO DuckDBDataChunk -> (DuckDBDataChunk -> IO a) -> IO a
+withDataChunk acquire = bracket acquire destroyChunk
+  where
+    destroyChunk chunk = alloca \ptr -> poke ptr chunk >> c_duckdb_destroy_data_chunk ptr
+
+fillIntVector :: DuckDBVector -> [Int32] -> IO ()
+fillIntVector vec values = do
+    dataPtrRaw <- c_duckdb_vector_get_data vec
+    let dataPtr = castPtr dataPtrRaw :: Ptr Int32
+    forM_ (zip [0 ..] values) (uncurry (pokeElemOff dataPtr))
+
+assignStrings :: DuckDBVector -> [String] -> IO ()
+assignStrings vec values =
+    forM_ (zip [0 ..] values) \(idx, val) ->
+        withCString val $ \str ->
+            c_duckdb_vector_assign_string_element vec (fromIntegral @Integer idx) str
+
+checkColumnType :: DuckDBAppender -> DuckDBIdx -> DuckDBType -> IO ()
+checkColumnType app idx expected =
+    do
+        logicalType <- c_duckdb_appender_column_type app idx
+        withLogicalType (pure logicalType) (c_duckdb_get_type_id >=> (@?= expected))
+
+fetchString :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO String
+fetchString resPtr col row = do
+    strPtr <- c_duckdb_value_varchar resPtr col row
+    text <- peekCString strPtr
+    c_duckdb_free (castPtr strPtr)
+    pure text
+
+fetchBlob :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO [Word8]
+fetchBlob resPtr col row =
+    alloca \blobPtr -> do
+        c_duckdb_value_blob resPtr col row blobPtr
+        DuckDBBlob dat len <- peek blobPtr
+        let dataPtr = castPtr dat :: Ptr Word8
+        bytes <- peekArray (fromIntegral len) dataPtr
+        c_duckdb_free dat
+        pure bytes
+
+fetchBool :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO Bool
+fetchBool resPtr col row = do
+    CBool val <- c_duckdb_value_boolean resPtr col row
+    pure (val /= 0)
diff --git a/test/ArrowInterfaceDeprecatedTests.hs b/test/ArrowInterfaceDeprecatedTests.hs
new file mode 100644
--- /dev/null
+++ b/test/ArrowInterfaceDeprecatedTests.hs
@@ -0,0 +1,489 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module ArrowInterfaceDeprecatedTests (tests) where
+
+import Control.Exception (bracket, finally)
+import Control.Monad (unless, when)
+import Data.Int (Int32, Int64)
+import Data.List (isInfixOf)
+import Data.Maybe (isNothing)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, peekCStringLen, withCString)
+import Foreign.C.Types (CChar)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (FunPtr, Ptr, castPtr, nullFunPtr, nullPtr, plusPtr)
+import Foreign.Storable (peek, peekElemOff, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase, withResult)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Deprecated Arrow Interface"
+        [ queryArrowExposesSchemaAndArrays
+        , queryArrowReportsErrors
+        , preparedArrowSchemaMatchesStatement
+        , executePreparedArrowProducesRows
+        , resultArrowArrayMirrorsChunk
+        , arrowRowsChangedReflectsMutations
+        , arrowArrayScanRegistersView
+        , arrowStreamScanRegistersView
+        , arrowPointerHelpersClearInternalState
+        ]
+
+-- basic query ---------------------------------------------------------------
+
+queryArrowExposesSchemaAndArrays :: TestTree
+queryArrowExposesSchemaAndArrays =
+    testCase "query_arrow exposes schema metadata and arrays" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withSuccessfulArrow conn "SELECT 1::INTEGER AS id, 'duck'::VARCHAR AS label" \arrow -> do
+                    columnCount <- c_duckdb_arrow_column_count arrow
+                    columnCount @?= 2
+
+                    rowCount <- c_duckdb_arrow_row_count arrow
+                    rowCount @?= 1
+
+                    rowsChanged <- c_duckdb_arrow_rows_changed arrow
+                    rowsChanged @?= 0
+
+                    errPtr <- c_duckdb_query_arrow_error arrow
+                    when (errPtr /= nullPtr) $ do
+                        errMsg <- peekCString errPtr
+                        errMsg @?= ""
+
+-- schema/array access validated in dedicated scan test
+
+-- error handling ------------------------------------------------------------
+
+queryArrowReportsErrors :: TestTree
+queryArrowReportsErrors =
+    testCase "query_arrow surfaces execution errors" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withCString "SELECT * FROM missing_table" \querySql ->
+                    alloca \arrowPtr -> do
+                        poke arrowPtr nullPtr
+                        state <- c_duckdb_query_arrow conn querySql arrowPtr
+                        state @?= DuckDBError
+
+                        arrow <- peek arrowPtr
+                        assertBool "arrow result should still be allocated on error" (arrow /= nullPtr)
+
+                        errPtr <- c_duckdb_query_arrow_error arrow
+                        assertBool "error message should be present" (errPtr /= nullPtr)
+                        errMsg <- peekCString errPtr
+                        assertBool "error message should mention missing_table" ("missing_table" `isInfixOf` errMsg)
+
+                        c_duckdb_destroy_arrow arrowPtr
+
+-- prepared statements -------------------------------------------------------
+
+preparedArrowSchemaMatchesStatement :: TestTree
+preparedArrowSchemaMatchesStatement =
+    testCase "prepared_arrow_schema reflects projected columns" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withPrepared conn "SELECT id, label FROM (VALUES (1, 'a')) AS t(id, label)" \stmt -> do
+                    alloca \schemaOut -> do
+                        poke schemaOut nullPtr
+                        stSchema <- c_duckdb_prepared_arrow_schema stmt schemaOut
+                        case stSchema of
+                            DuckDBSuccess -> do
+                                schemaWrapper <- peek schemaOut
+                                assertBool "prepared arrow schema pointer should not be null" (schemaWrapper /= nullPtr)
+                            DuckDBError -> do
+                                schemaWrapper <- peek schemaOut
+                                schemaWrapper @?= nullPtr
+                                errPtr <- c_duckdb_prepare_error stmt
+                                when (errPtr /= nullPtr) $ do
+                                    errMsg <- peekCString errPtr
+                                    errMsg @?= ""
+
+executePreparedArrowProducesRows :: TestTree
+executePreparedArrowProducesRows =
+    testCase "execute_prepared_arrow materialises a result set" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withPrepared conn "SELECT ?::INTEGER + 5 AS computed" \stmt -> do
+                    c_duckdb_bind_int32 stmt 1 (5 :: Int32) >>= (@?= DuckDBSuccess)
+
+                    withPreparedArrow stmt \arrow -> do
+                        rowCount <- c_duckdb_arrow_row_count arrow
+                        rowCount @?= 1
+
+                        colCount <- c_duckdb_arrow_column_count arrow
+                        colCount @?= 1
+
+-- result conversion ---------------------------------------------------------
+
+resultArrowArrayMirrorsChunk :: TestTree
+resultArrowArrayMirrorsChunk =
+    -- NOTE: duckdb_result_arrow_array writes release callbacks into the
+    -- provided ArrowArray storage. Always zero-initialise the struct
+    -- (see zeroArrowArray) and honour the release function before the
+    -- stack memory goes out of scope.
+    testCase "result_arrow_array converts materialised chunks to Arrow arrays" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                execStatement conn "CREATE TABLE arrow_chunks(id BIGINT, label VARCHAR);"
+                execStatement conn "INSERT INTO arrow_chunks VALUES (10, 'ten'), (11, 'eleven');"
+
+                withResult conn "SELECT id, label FROM arrow_chunks ORDER BY id" \resPtr -> do
+                    chunk <- c_duckdb_result_get_chunk resPtr 0
+                    assertBool "fetch_chunk returned a null chunk" (chunk /= nullPtr)
+                    chunkSize <- c_duckdb_data_chunk_get_size chunk
+                    chunkCols <- c_duckdb_data_chunk_get_column_count chunk
+
+                    alloca \arrowArrayPtr -> do
+                        poke arrowArrayPtr zeroArrowArray
+                        let duckArray :: DuckDBArrowArray
+                            duckArray = castPtr arrowArrayPtr
+
+                        alloca \arrayOut -> do
+                            poke arrayOut duckArray
+                            c_duckdb_result_arrow_array resPtr chunk arrayOut
+
+                            array <- peek arrowArrayPtr
+
+                            arrowArrayLength array @?= fromIntegral chunkSize
+                            arrowArrayChildCount array @?= fromIntegral chunkCols
+
+                            validateChunkChildren array
+
+                            releaseArrowArray arrowArrayPtr
+
+                    destroyChunk chunk
+
+-- rows changed --------------------------------------------------------------
+
+arrowRowsChangedReflectsMutations :: TestTree
+arrowRowsChangedReflectsMutations =
+    testCase "arrow_rows_changed reports mutation counts" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                execStatement conn "CREATE TABLE arrow_changes(val INTEGER);"
+
+                withSuccessfulArrow conn "INSERT INTO arrow_changes VALUES (1), (2), (3)" \arrow -> do
+                    rowCount <- c_duckdb_arrow_row_count arrow
+                    assertBool "modification result should not report negative rows" (rowCount >= 0)
+
+                    changed <- c_duckdb_arrow_rows_changed arrow
+                    assertBool "rows_changed should report positive count" (changed > 0)
+
+-- arrow scans ----------------------------------------------------------------
+
+arrowArrayScanRegistersView :: TestTree
+arrowArrayScanRegistersView =
+    -- NOTE: Both the schema and array buffers must be initialised to
+    -- zeroed Arrow structures before calling the query helpers. DuckDB
+    -- fills in release callbacks and expects us to invoke
+    -- duckdb_destroy_arrow_stream on the out stream.
+    testCase "arrow_array_scan registers a view and yields a release stream" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                execStatement conn "CREATE TABLE arrow_scan_source(i BIGINT, label VARCHAR);"
+                execStatement conn "INSERT INTO arrow_scan_source VALUES (5, 'five'), (6, 'six');"
+
+                withSuccessfulArrow conn "SELECT i, label FROM arrow_scan_source ORDER BY i" \arrow -> do
+                    alloca \schemaStorage -> do
+                        poke schemaStorage zeroArrowSchema
+                        let schemaHandle = castPtr schemaStorage :: DuckDBArrowSchema
+                        alloca \schemaOut -> do
+                            poke schemaOut schemaHandle
+                            schemaState <- c_duckdb_query_arrow_schema arrow schemaOut
+                            schemaState @?= DuckDBSuccess
+
+                            alloca \arrayStorage -> do
+                                poke arrayStorage zeroArrowArray
+                                let arrayHandle = castPtr arrayStorage :: DuckDBArrowArray
+                                alloca \arrayOut -> do
+                                    poke arrayOut arrayHandle
+                                    arrayState <- c_duckdb_query_arrow_array arrow arrayOut
+                                    arrayState @?= DuckDBSuccess
+
+                                    withCString "arrow_array_view" \viewName ->
+                                        alloca \streamOut -> do
+                                            poke streamOut nullPtr
+                                            scanState <- c_duckdb_arrow_array_scan conn viewName schemaHandle arrayHandle streamOut
+                                            scanState @?= DuckDBSuccess
+
+                                            streamWrapper <- peek streamOut
+                                            assertBool "arrow_array_scan returned a null stream handle" (streamWrapper /= nullPtr)
+
+                                            withResult conn "SELECT COUNT(*) FROM arrow_array_view" \resPtr -> do
+                                                count <- c_duckdb_value_int64 resPtr 0 0
+                                                count @?= 2
+
+                                            c_duckdb_destroy_arrow_stream streamOut
+                                    releaseArrowArray arrayStorage
+                        releaseArrowSchema schemaStorage
+
+arrowStreamScanRegistersView :: TestTree
+arrowStreamScanRegistersView =
+    -- NOTE: duckdb_arrow_array_scan returns DuckDBError if the target
+    -- view name already exists (even if it is a table). Use a fresh
+    -- view name that will be dropped implicitly when the connection
+    -- closes.
+    testCase "arrow_scan registers a view from an Arrow stream" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                execStatement conn "CREATE TABLE arrow_stream_source(i BIGINT, label VARCHAR);"
+                execStatement conn "INSERT INTO arrow_stream_source VALUES (7, 'seven'), (8, 'eight');"
+
+                withSuccessfulArrow conn "SELECT i, label FROM arrow_stream_source ORDER BY i" \arrow -> do
+                    alloca \schemaStorage ->
+                        finally
+                            ( do
+                                poke schemaStorage zeroArrowSchema
+                                let schemaHandle = castPtr schemaStorage :: DuckDBArrowSchema
+                                alloca \schemaOut -> do
+                                    poke schemaOut schemaHandle
+                                    schemaState <- c_duckdb_query_arrow_schema arrow schemaOut
+                                    schemaState @?= DuckDBSuccess
+
+                                    alloca \arrayStorage ->
+                                        finally
+                                            ( do
+                                                poke arrayStorage zeroArrowArray
+                                                let arrayHandle = castPtr arrayStorage :: DuckDBArrowArray
+                                                alloca \arrayOut -> do
+                                                    poke arrayOut arrayHandle
+                                                    arrayState <- c_duckdb_query_arrow_array arrow arrayOut
+                                                    arrayState @?= DuckDBSuccess
+
+                                                    withCString "arrow_stream_array_view" \sourceView ->
+                                                        alloca \streamOut -> do
+                                                            poke streamOut nullPtr
+                                                            arrayScanState <- c_duckdb_arrow_array_scan conn sourceView schemaHandle arrayHandle streamOut
+                                                            arrayScanState @?= DuckDBSuccess
+
+                                                            streamHandle <- peek streamOut
+                                                            assertBool "arrow_array_scan returned a null stream" (streamHandle /= nullPtr)
+
+                                                            withCString "arrow_stream_view" \streamView -> do
+                                                                streamScanState <- c_duckdb_arrow_scan conn streamView streamHandle
+                                                                streamScanState @?= DuckDBSuccess
+
+                                                            withResult conn "SELECT COUNT(*) FROM arrow_stream_view" \resPtr -> do
+                                                                count <- c_duckdb_value_int64 resPtr 0 0
+                                                                count @?= 2
+
+                                                            c_duckdb_destroy_arrow_stream streamOut
+                                            )
+                                            (releaseArrowArray arrayStorage)
+                            )
+                            (releaseArrowSchema schemaStorage)
+
+arrowPointerHelpersClearInternalState :: TestTree
+arrowPointerHelpersClearInternalState =
+    -- NOTE: The deprecated Arrow handles are thin wrappers whose
+    -- internal_ptr fields can be inspected/reset via the exported
+    -- duckdbArrow{Schema,Array,Stream}{Internal,Clear} helpers. We avoid
+    -- direct foreign imports here so future changes to the shims only
+    -- require updates in the Arrow module.
+    testCase "arrow pointer helper functions read and clear internal_ptr fields" $
+        alloca \schemaFieldPtr ->
+            alloca \arrayFieldPtr ->
+                alloca \streamFieldPtr -> do
+                    let schemaField = schemaFieldPtr :: Ptr (Ptr ())
+                        schemaHandle = castPtr schemaField :: DuckDBArrowSchema
+                        schemaSentinel = ArrowSchemaPtr (castPtr schemaField)
+                    poke schemaField (castPtr (unArrowSchemaPtr schemaSentinel))
+                    schemaPtr <- duckdbArrowSchemaInternal schemaHandle
+                    fmap unArrowSchemaPtr schemaPtr @?= Just (unArrowSchemaPtr schemaSentinel)
+
+                    duckdbArrowSchemaClear schemaHandle
+                    schemaCleared <- duckdbArrowSchemaInternal schemaHandle
+                    assertBool "schema internal_ptr should be cleared" (isNothing schemaCleared)
+
+                    let arrayField = arrayFieldPtr :: Ptr (Ptr ())
+                        arrayHandle = castPtr arrayField :: DuckDBArrowArray
+                        arraySentinel = ArrowArrayPtr (castPtr arrayField)
+                    poke arrayField (castPtr (unArrowArrayPtr arraySentinel))
+                    arrayPtr <- duckdbArrowArrayInternal arrayHandle
+                    fmap unArrowArrayPtr arrayPtr @?= Just (unArrowArrayPtr arraySentinel)
+
+                    duckdbArrowArrayClear arrayHandle
+                    arrayCleared <- duckdbArrowArrayInternal arrayHandle
+                    assertBool "array internal_ptr should be cleared" (isNothing arrayCleared)
+
+                    let streamField = streamFieldPtr :: Ptr (Ptr ())
+                        streamHandle = castPtr streamField :: DuckDBArrowStream
+                        streamSentinel = ArrowStreamPtr (castPtr streamField)
+                    poke streamField (castPtr (unArrowStreamPtr streamSentinel))
+                    streamPtr <- duckdbArrowStreamInternal streamHandle
+                    fmap unArrowStreamPtr streamPtr @?= Just (unArrowStreamPtr streamSentinel)
+
+                    duckdbArrowStreamClear streamHandle
+                    streamCleared <- duckdbArrowStreamInternal streamHandle
+                    assertBool "stream internal_ptr should be cleared" (isNothing streamCleared)
+
+zeroArrowArray :: ArrowArray
+zeroArrowArray =
+    ArrowArray
+        { arrowArrayLength = 0
+        , arrowArrayNullCount = 0
+        , arrowArrayOffset = 0
+        , arrowArrayBufferCount = 0
+        , arrowArrayChildCount = 0
+        , arrowArrayBuffers = nullPtr
+        , arrowArrayChildren = nullPtr
+        , arrowArrayDictionary = nullPtr
+        , arrowArrayRelease = nullFunPtr
+        , arrowArrayPrivateData = nullPtr
+        }
+
+zeroArrowSchema :: ArrowSchema
+zeroArrowSchema =
+    ArrowSchema
+        { arrowSchemaFormat = nullPtr
+        , arrowSchemaName = nullPtr
+        , arrowSchemaMetadata = nullPtr
+        , arrowSchemaFlags = 0
+        , arrowSchemaChildCount = 0
+        , arrowSchemaChildren = nullPtr
+        , arrowSchemaDictionary = nullPtr
+        , arrowSchemaRelease = nullFunPtr
+        , arrowSchemaPrivateData = nullPtr
+        }
+
+validateChunkChildren :: ArrowArray -> IO ()
+validateChunkChildren array = do
+    let expectedIds = [10, 11] :: [Int64]
+        expectedLabels = ["ten", "eleven"]
+        childCount = fromIntegral (arrowArrayChildCount array) :: Int
+        rowCount = fromIntegral (arrowArrayLength array) :: Int
+    childCount @?= 2
+    rowCount @?= length expectedIds
+
+    let childrenPtr = arrowArrayChildren array
+    assertBool "Arrow array did not expose child arrays" (childrenPtr /= nullPtr)
+
+    let ensurePtr name ptrPred =
+            unless ptrPred $
+                assertFailure ("Arrow array " ++ name ++ " pointer is null")
+
+    intChildPtr <- peekElemOff childrenPtr 0
+    ensurePtr "integer child" (intChildPtr /= nullPtr)
+    intChild <- peek intChildPtr
+    arrowArrayNullCount intChild @?= 0
+    let intBufferCount = fromIntegral (arrowArrayBufferCount intChild) :: Int
+    assertBool "Integer child did not expose the expected buffers" (intBufferCount >= 2)
+
+    let intBuffers = arrowArrayBuffers intChild
+    ensurePtr "integer buffers" (intBuffers /= nullPtr)
+    valueBufferRaw <- peekElemOff intBuffers 1
+    ensurePtr "integer value buffer" (valueBufferRaw /= nullPtr)
+    let valueBuffer = castPtr valueBufferRaw :: Ptr Int64
+    values <- mapM (peekElemOff valueBuffer) [0 .. rowCount - 1]
+    values @?= expectedIds
+
+    strChildPtr <- peekElemOff childrenPtr 1
+    ensurePtr "varchar child" (strChildPtr /= nullPtr)
+    strChild <- peek strChildPtr
+    arrowArrayNullCount strChild @?= 0
+    let strBufferCount = fromIntegral (arrowArrayBufferCount strChild) :: Int
+    assertBool "Varchar child did not expose the expected buffers" (strBufferCount >= 3)
+
+    let strBuffers = arrowArrayBuffers strChild
+    ensurePtr "varchar buffers" (strBuffers /= nullPtr)
+    offsetsRaw <- peekElemOff strBuffers 1
+    dataRaw <- peekElemOff strBuffers 2
+    ensurePtr "varchar offsets buffer" (offsetsRaw /= nullPtr)
+    ensurePtr "varchar data buffer" (dataRaw /= nullPtr)
+
+    let offsetsPtr = castPtr offsetsRaw :: Ptr Int32
+        dataPtr = castPtr dataRaw :: Ptr CChar
+    labels <-
+        mapM
+            ( \idx -> do
+                start <- fromIntegral <$> peekElemOff offsetsPtr idx
+                end <- fromIntegral <$> peekElemOff offsetsPtr (idx + 1)
+                peekCStringLen (dataPtr `plusPtr` start, end - start)
+            )
+            [0 .. rowCount - 1]
+    labels @?= expectedLabels
+
+releaseArrowArray :: Ptr ArrowArray -> IO ()
+releaseArrowArray arrayPtr = do
+    array <- peek arrayPtr
+    let releaseFun = arrowArrayRelease array
+    when (releaseFun /= nullFunPtr) $ do
+        let release = mkArrowArrayRelease releaseFun
+        release arrayPtr
+
+releaseArrowSchema :: Ptr ArrowSchema -> IO ()
+releaseArrowSchema schemaPtr = do
+    schema <- peek schemaPtr
+    let releaseFun = arrowSchemaRelease schema
+    when (releaseFun /= nullFunPtr) $ do
+        let release = mkArrowSchemaRelease releaseFun
+        release schemaPtr
+
+foreign import ccall "dynamic"
+    mkArrowArrayRelease :: FunPtr (Ptr ArrowArray -> IO ()) -> Ptr ArrowArray -> IO ()
+
+foreign import ccall "dynamic"
+    mkArrowSchemaRelease :: FunPtr (Ptr ArrowSchema -> IO ()) -> Ptr ArrowSchema -> IO ()
+
+withSuccessfulArrow :: DuckDBConnection -> String -> (DuckDBArrow -> IO a) -> IO a
+withSuccessfulArrow conn sql action =
+    withCString sql \sqlPtr ->
+        alloca \arrowPtr ->
+            bracket
+                ( do
+                    poke arrowPtr nullPtr
+                    state <- c_duckdb_query_arrow conn sqlPtr arrowPtr
+                    state @?= DuckDBSuccess
+                    arrow <- peek arrowPtr
+                    assertBool "duckdb_query_arrow returned null result" (arrow /= nullPtr)
+                    pure arrow
+                )
+                (\_ -> c_duckdb_destroy_arrow arrowPtr)
+                action
+
+withPrepared :: DuckDBConnection -> String -> (DuckDBPreparedStatement -> IO a) -> IO a
+withPrepared conn sql action =
+    withCString sql \sqlPtr ->
+        alloca \stmtPtr ->
+            bracket
+                ( do
+                    state <- c_duckdb_prepare conn sqlPtr stmtPtr
+                    state @?= DuckDBSuccess
+                    stmt <- peek stmtPtr
+                    assertBool "prepare should produce a statement" (stmt /= nullPtr)
+                    pure stmt
+                )
+                (\_ -> c_duckdb_destroy_prepare stmtPtr)
+                action
+
+withPreparedArrow :: DuckDBPreparedStatement -> (DuckDBArrow -> IO a) -> IO a
+withPreparedArrow stmt action =
+    alloca \arrowPtr ->
+        bracket
+            ( do
+                poke arrowPtr nullPtr
+                state <- c_duckdb_execute_prepared_arrow stmt arrowPtr
+                state @?= DuckDBSuccess
+                arrow <- peek arrowPtr
+                assertBool "execute_prepared_arrow returned null result" (arrow /= nullPtr)
+                pure arrow
+            )
+            (\_ -> c_duckdb_destroy_arrow arrowPtr)
+            action
+
+destroyChunk :: DuckDBDataChunk -> IO ()
+destroyChunk chunk =
+    alloca \ptr -> poke ptr chunk >> c_duckdb_destroy_data_chunk ptr
+
+execStatement :: DuckDBConnection -> String -> IO ()
+execStatement conn sql =
+    withCString sql \sqlPtr ->
+        alloca \resPtr -> do
+            st <- c_duckdb_query conn sqlPtr resPtr
+            st @?= DuckDBSuccess
+            c_duckdb_destroy_result resPtr
diff --git a/test/ArrowInterfaceTest.hs b/test/ArrowInterfaceTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ArrowInterfaceTest.hs
@@ -0,0 +1,259 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ArrowInterfaceTest (tests) where
+
+import Control.Exception (bracket)
+import Control.Monad (when)
+import Data.Bits (testBit)
+import Data.Int (Int32)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray, withArray)
+import Foreign.Marshal.Utils (withMany)
+import Foreign.Ptr (FunPtr, Ptr, castPtr, nullFunPtr, nullPtr)
+import Foreign.Storable (Storable (..), peek, peekElemOff, poke, pokeElemOff)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Arrow Interface"
+        [ arrowSchemaRoundtrip
+        , arrowChunkRoundtrip
+        ]
+
+arrowSchemaRoundtrip :: TestTree
+arrowSchemaRoundtrip =
+    testCase "to_arrow_schema exposes children and converts back" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withArrowOptions conn \arrowOpts ->
+                    withLogicalTypes [DuckDBTypeInteger, DuckDBTypeVarchar] \logicalTypes ->
+                        withArray logicalTypes \logicalArray ->
+                            withColumnNames ["id", "label"] \nameArray ->
+                                withArrowSchema \schemaPtr -> do
+                                    errData <- c_duckdb_to_arrow_schema arrowOpts logicalArray nameArray (fromIntegral (length logicalTypes)) (castPtr schemaPtr)
+                                    assertNoError errData
+
+                                    schema <- peek schemaPtr
+                                    formatStr <- peekCString (arrowSchemaFormat schema)
+                                    formatStr @?= "+s"
+                                    arrowSchemaChildCount schema @?= fromIntegral (length logicalTypes)
+
+                                    let childCount = fromIntegral (arrowSchemaChildCount schema)
+                                    let childArrayPtr = arrowSchemaChildren schema
+                                    assertBool "children pointer should not be null" (childArrayPtr /= nullPtr)
+                                    [fc, sc] <- peekArray childCount childArrayPtr >>= mapM peek
+                                    peekCString (arrowSchemaName fc) >>= (@?= "id")
+                                    peekCString (arrowSchemaName sc) >>= (@?= "label")
+                                    assertBool "schema release pointer should be set" (arrowSchemaRelease schema /= nullFunPtr)
+
+                                    withConvertedSchema conn schemaPtr (const (pure ()))
+
+                                    releaseArrowSchema schemaPtr
+
+arrowChunkRoundtrip :: TestTree
+arrowChunkRoundtrip =
+    testCase "data_chunk_to_arrow and from_arrow preserve values and nulls" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withArrowOptions conn \arrowOpts ->
+                    withLogicalTypes [DuckDBTypeInteger] \logicalTypes ->
+                        withArray logicalTypes \logicalArray ->
+                            withColumnNames ["val"] \nameArray ->
+                                withArrowSchema \schemaPtr -> do
+                                    errSchema <- c_duckdb_to_arrow_schema arrowOpts logicalArray nameArray 1 (castPtr schemaPtr)
+                                    assertNoError errSchema
+
+                                    withConvertedSchema conn schemaPtr \convertedSchema ->
+                                        do
+                                            chunk <- c_duckdb_create_data_chunk logicalArray 1
+                                            assertBool "create_data_chunk should return chunk" (chunk /= nullPtr)
+
+                                            withOwnedChunk chunk \ownedChunk -> do
+                                                c_duckdb_data_chunk_set_size ownedChunk 2
+                                                vector <- c_duckdb_data_chunk_get_vector ownedChunk 0
+                                                dataPtr <- c_duckdb_vector_get_data vector
+                                                let intPtr = castPtr dataPtr :: Ptr Int32
+                                                pokeElemOff intPtr 0 42
+                                                pokeElemOff intPtr 1 0
+
+                                                c_duckdb_vector_ensure_validity_writable vector
+                                                maskPtr <- c_duckdb_vector_get_validity vector
+                                                assertBool "validity mask pointer should not be null" (maskPtr /= nullPtr)
+                                                c_duckdb_validity_set_row_valid maskPtr 0
+                                                c_duckdb_validity_set_row_invalid maskPtr 1
+
+                                                withArrowArray \arrayPtr ->
+                                                    do
+                                                        errArray <- c_duckdb_data_chunk_to_arrow arrowOpts ownedChunk (castPtr arrayPtr)
+                                                        assertNoError errArray
+
+                                                        array <- peek arrayPtr
+                                                        arrowArrayLength array @?= 2
+                                                        assertBool "release pointer should not be null before transfer" (arrowArrayRelease array /= nullFunPtr)
+
+                                                        alloca \outChunkPtr -> do
+                                                            poke outChunkPtr nullPtr
+                                                            errFromArrow <- c_duckdb_data_chunk_from_arrow conn (castPtr arrayPtr) convertedSchema outChunkPtr
+                                                            assertNoError errFromArrow
+                                                            restoredChunk <- peek outChunkPtr
+                                                            assertBool "restored chunk should not be null" (restoredChunk /= nullPtr)
+
+                                                            withOwnedChunk restoredChunk $ \restored -> do
+                                                                restoredSize <- c_duckdb_data_chunk_get_size restored
+                                                                restoredSize @?= 2
+
+                                                                restoredVector <- c_duckdb_data_chunk_get_vector restored 0
+                                                                restoredDataPtr <- c_duckdb_vector_get_data restoredVector
+                                                                let restoredIntPtr = castPtr restoredDataPtr :: Ptr Int32
+                                                                restoredVal <- peekElemOff restoredIntPtr 0
+                                                                restoredVal @?= 42
+
+                                                                restoredMaskPtr <- c_duckdb_vector_get_validity restoredVector
+                                                                assertBool "restored validity mask pointer should not be null" (restoredMaskPtr /= nullPtr)
+                                                                restoredMaskWord <- peek restoredMaskPtr
+                                                                assertBool "first row should be valid" (testBit restoredMaskWord 0)
+                                                                assertBool "second row should be null" (not (testBit restoredMaskWord 1))
+
+                                                            arrayAfter <- peek arrayPtr
+                                                            arrowArrayRelease arrayAfter @?= nullFunPtr
+
+                                    releaseArrowSchema schemaPtr
+
+withArrowOptions :: DuckDBConnection -> (DuckDBArrowOptions -> IO a) -> IO a
+withArrowOptions conn action =
+    alloca \optsPtr -> do
+        let acquire = do
+                poke optsPtr nullPtr
+                c_duckdb_connection_get_arrow_options conn optsPtr
+                opts <- peek optsPtr
+                when (opts == nullPtr) $ assertFailure "duckdb_connection_get_arrow_options returned null"
+                pure opts
+            release _ = c_duckdb_destroy_arrow_options optsPtr
+        bracket acquire release action
+
+withLogicalTypes :: [DuckDBType] -> ([DuckDBLogicalType] -> IO a) -> IO a
+withLogicalTypes [] action = action []
+withLogicalTypes (t : ts) action =
+    bracket (c_duckdb_create_logical_type t) destroyLogicalType \lt ->
+        withLogicalTypes ts \rest -> action (lt : rest)
+
+withColumnNames :: [String] -> (Ptr CString -> IO a) -> IO a
+withColumnNames names action =
+    withMany withCString names $ \cNames -> withArray cNames action
+
+withArrowSchema :: (Ptr ArrowSchema -> IO a) -> IO a
+withArrowSchema = withStruct zeroArrowSchema
+
+withArrowArray :: (Ptr ArrowArray -> IO a) -> IO a
+withArrowArray action =
+    withStruct zeroArrowArray \ptr -> do
+        result <- action ptr
+        releaseArrowArray ptr
+        pure result
+
+withStruct :: (Storable a) => a -> (Ptr a -> IO b) -> IO b
+withStruct initial action =
+    alloca \ptr -> do
+        poke ptr initial
+        action ptr
+
+withOwnedChunk :: DuckDBDataChunk -> (DuckDBDataChunk -> IO a) -> IO a
+withOwnedChunk chunk = bracket (pure chunk) destroyChunk
+
+withConvertedSchema :: DuckDBConnection -> Ptr ArrowSchema -> (DuckDBArrowConvertedSchema -> IO a) -> IO a
+withConvertedSchema conn schemaPtr action =
+    alloca \convertedPtr -> do
+        poke convertedPtr nullPtr
+        err <- c_duckdb_schema_from_arrow conn (castPtr schemaPtr) convertedPtr
+        assertNoError err
+        converted <- peek convertedPtr
+        assertBool "converted schema pointer should not be null" (converted /= nullPtr)
+        bracket (pure converted) destroyArrowConvertedSchema action
+
+destroyLogicalType :: DuckDBLogicalType -> IO ()
+destroyLogicalType lt =
+    alloca \ptr -> do
+        poke ptr lt
+        c_duckdb_destroy_logical_type ptr
+
+destroyChunk :: DuckDBDataChunk -> IO ()
+destroyChunk chunk =
+    alloca \ptr -> do
+        poke ptr chunk
+        c_duckdb_destroy_data_chunk ptr
+
+destroyArrowConvertedSchema :: DuckDBArrowConvertedSchema -> IO ()
+destroyArrowConvertedSchema schema =
+    alloca \ptr -> do
+        poke ptr schema
+        c_duckdb_destroy_arrow_converted_schema ptr
+
+destroyErrorData :: DuckDBErrorData -> IO ()
+destroyErrorData err =
+    alloca \ptr -> do
+        poke ptr err
+        c_duckdb_destroy_error_data ptr
+
+assertNoError :: DuckDBErrorData -> IO ()
+assertNoError err =
+    when (err /= nullPtr) $ do
+        msgPtr <- c_duckdb_error_data_message err
+        msg <- peekCString msgPtr
+        destroyErrorData err
+        assertFailure ("DuckDB reported error: " <> msg)
+
+releaseArrowSchema :: Ptr ArrowSchema -> IO ()
+releaseArrowSchema schemaPtr = do
+    schema <- peek schemaPtr
+    let releaseFun = arrowSchemaRelease schema
+    when (releaseFun /= nullFunPtr) $ do
+        let release = mkArrowSchemaRelease releaseFun
+        release schemaPtr
+
+releaseArrowArray :: Ptr ArrowArray -> IO ()
+releaseArrowArray arrayPtr = do
+    array <- peek arrayPtr
+    let releaseFun = arrowArrayRelease array
+    when (releaseFun /= nullFunPtr) $ do
+        let release = mkArrowArrayRelease releaseFun
+        release arrayPtr
+
+zeroArrowSchema :: ArrowSchema
+zeroArrowSchema =
+    ArrowSchema
+        { arrowSchemaFormat = nullPtr
+        , arrowSchemaName = nullPtr
+        , arrowSchemaMetadata = nullPtr
+        , arrowSchemaFlags = 0
+        , arrowSchemaChildCount = 0
+        , arrowSchemaChildren = nullPtr
+        , arrowSchemaDictionary = nullPtr
+        , arrowSchemaRelease = nullFunPtr
+        , arrowSchemaPrivateData = nullPtr
+        }
+
+zeroArrowArray :: ArrowArray
+zeroArrowArray =
+    ArrowArray
+        { arrowArrayLength = 0
+        , arrowArrayNullCount = 0
+        , arrowArrayOffset = 0
+        , arrowArrayBufferCount = 0
+        , arrowArrayChildCount = 0
+        , arrowArrayBuffers = nullPtr
+        , arrowArrayChildren = nullPtr
+        , arrowArrayDictionary = nullPtr
+        , arrowArrayRelease = nullFunPtr
+        , arrowArrayPrivateData = nullPtr
+        }
+
+foreign import ccall "dynamic"
+    mkArrowSchemaRelease :: FunPtr (Ptr ArrowSchema -> IO ()) -> Ptr ArrowSchema -> IO ()
+
+foreign import ccall "dynamic"
+    mkArrowArrayRelease :: FunPtr (Ptr ArrowArray -> IO ()) -> Ptr ArrowArray -> IO ()
diff --git a/test/BindValuesTest.hs b/test/BindValuesTest.hs
new file mode 100644
--- /dev/null
+++ b/test/BindValuesTest.hs
@@ -0,0 +1,275 @@
+{-# LANGUAGE BlockArguments #-}
+
+module BindValuesTest (tests) where
+
+import Control.Monad (when)
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.List (intercalate)
+import Data.Time.Calendar (diffDays, fromGregorian)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, peekCStringLen, withCString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray, withArray)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Bind Values"
+        [bindValuesRoundtrip]
+
+bindValuesRoundtrip :: TestTree
+bindValuesRoundtrip =
+    testCase "bind every supported value type" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                withCString createSQL \ddl ->
+                    alloca \resPtr -> do
+                        st <- c_duckdb_query conn ddl resPtr
+                        st @?= DuckDBSuccess
+                        c_duckdb_destroy_result resPtr
+
+                -- values used for binding and verification
+                let boolValue = CBool 1
+                    tinyValue = (-5 :: Int8)
+                    smallValue = (-300 :: Int16)
+                    intValue = (-4000000 :: Int32)
+                    bigValue = (-5000000000000 :: Int64)
+                    u8Value = 200 :: Word8
+                    u16Value = 60000 :: Word16
+                    u32Value = 4000000000 :: Word32
+                    u64Value = maxBound :: Word64
+                    floatValue = CFloat 1.5
+                    doubleValue = CDouble 2.5
+                    dateDay = fromGregorian 2021 7 20
+                    epoch = fromGregorian 1970 1 1
+                    dateValue = DuckDBDate (fromIntegral (diffDays dateDay epoch))
+                    timeMicros :: Integer
+                    timeMicros = ((12 * 60 + 34) * 60 + 56) * 1000000
+                    timeValue = DuckDBTime (fromIntegral timeMicros)
+                    timestampValue = DuckDBTimestamp (fromIntegral (duckDBDateDays dateValue) * 86400000000 + fromIntegral timeMicros)
+                    intervalValue = DuckDBInterval{duckDBIntervalMonths = 0, duckDBIntervalDays = 1, duckDBIntervalMicros = 7200000000}
+                    decimalValue = DuckDBDecimal{duckDBDecimalWidth = 18, duckDBDecimalScale = 2, duckDBDecimalValue = DuckDBHugeInt{duckDBHugeIntLower = 1234567, duckDBHugeIntUpper = 0}}
+                    hugeValue = DuckDBHugeInt{duckDBHugeIntLower = 9223372036854775809, duckDBHugeIntUpper = 0}
+                    uhugeValue = DuckDBUHugeInt{duckDBUHugeIntLower = 123456789, duckDBUHugeIntUpper = 1}
+                    varcharValue = "varchar binding"
+                    varcharLenValue = "varchar length binding"
+                    blobBytes :: [Word8]
+                    blobBytes = map (fromIntegral . fromEnum) "abc"
+
+                withCString insertSQL \cInsert ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn cInsert stmtPtr
+                        stmt <- peek stmtPtr
+                        when (st /= DuckDBSuccess) $ do
+                            msg <- if stmt == nullPtr then pure "prepare failed" else c_duckdb_prepare_error stmt >>= peekCString
+                            assertFailure msg
+                        st @?= DuckDBSuccess
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        withCString varcharValue \varcharPtr ->
+                            withCString varcharLenValue \varcharLenPtr ->
+                                withArray blobBytes \blobPtr ->
+                                    alloca \hugePtr ->
+                                        alloca \uhugePtr ->
+                                            alloca \decimalPtr ->
+                                                alloca \intervalPtr ->
+                                                    alloca \valuePtr -> do
+                                                        poke hugePtr hugeValue
+                                                        poke uhugePtr uhugeValue
+                                                        poke decimalPtr decimalValue
+                                                        poke intervalPtr intervalValue
+
+                                                        duckValue <- c_duckdb_create_bool boolValue
+                                                        poke valuePtr duckValue
+                                                        c_duckdb_bind_value stmt 1 duckValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_destroy_value valuePtr
+
+                                                        c_duckdb_bind_boolean stmt 2 boolValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_int8 stmt 3 tinyValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_int16 stmt 4 smallValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_int32 stmt 5 intValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_int64 stmt 6 bigValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_hugeint stmt 7 hugePtr >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_uint8 stmt 8 u8Value >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_uint16 stmt 9 u16Value >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_uint32 stmt 10 u32Value >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_uint64 stmt 11 u64Value >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_uhugeint stmt 12 uhugePtr >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_float stmt 13 floatValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_double stmt 14 doubleValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_date stmt 15 dateValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_time stmt 16 timeValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_timestamp stmt 17 timestampValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_timestamp_tz stmt 18 timestampValue >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_interval stmt 19 intervalPtr >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_decimal stmt 20 decimalPtr >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_varchar stmt 21 varcharPtr >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_varchar_length stmt 22 varcharLenPtr (fromIntegral (length varcharLenValue) :: DuckDBIdx) >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_blob stmt 23 (castPtr blobPtr) (fromIntegral (length blobBytes) :: DuckDBIdx) >>= (@?= DuckDBSuccess)
+                                                        c_duckdb_bind_null stmt 24 >>= (@?= DuckDBSuccess)
+
+                                                        alloca \execResPtr -> do
+                                                            stExec <- c_duckdb_execute_prepared stmt execResPtr
+                                                            stExec @?= DuckDBSuccess
+                                                            c_duckdb_destroy_result execResPtr
+
+                                                        c_duckdb_destroy_prepare stmtPtr
+
+                -- Validate inserted row
+                withCString selectSQL \cSelect ->
+                    alloca \resPtr -> do
+                        st <- c_duckdb_query conn cSelect resPtr
+                        st @?= DuckDBSuccess
+
+                        rowCount <- c_duckdb_row_count resPtr
+                        rowCount @?= 1
+
+                        c_duckdb_value_boolean resPtr 0 0 >>= (@?= CBool 1)
+                        c_duckdb_value_boolean resPtr 1 0 >>= (@?= CBool 1)
+                        c_duckdb_value_int8 resPtr 2 0 >>= (@?= (-5 :: Int8))
+                        c_duckdb_value_int16 resPtr 3 0 >>= (@?= (-300 :: Int16))
+                        c_duckdb_value_int32 resPtr 4 0 >>= (@?= (-4000000 :: Int32))
+                        c_duckdb_value_int64 resPtr 5 0 >>= (@?= (-5000000000000 :: Int64))
+
+                        alloca \hugePtr -> do
+                            c_duckdb_value_hugeint resPtr 6 0 hugePtr
+                            peek hugePtr >>= (@?= DuckDBHugeInt{duckDBHugeIntLower = 9223372036854775809, duckDBHugeIntUpper = 0})
+
+                        c_duckdb_value_uint8 resPtr 7 0 >>= (@?= (200 :: Word8))
+                        c_duckdb_value_uint16 resPtr 8 0 >>= (@?= (60000 :: Word16))
+                        c_duckdb_value_uint32 resPtr 9 0 >>= (@?= (4000000000 :: Word32))
+                        c_duckdb_value_uint64 resPtr 10 0 >>= (@?= maxBound)
+
+                        alloca \uhugePtr -> do
+                            c_duckdb_value_uhugeint resPtr 11 0 uhugePtr
+                            peek uhugePtr >>= (@?= DuckDBUHugeInt{duckDBUHugeIntLower = 123456789, duckDBUHugeIntUpper = 1})
+
+                        valFloat <- c_duckdb_value_float resPtr 12 0
+                        realToFrac valFloat @?= (1.5 :: Double)
+                        valDouble <- c_duckdb_value_double resPtr 13 0
+                        realToFrac valDouble @?= (2.5 :: Double)
+
+                        DuckDBDate fetchedDate <- c_duckdb_value_date resPtr 14 0
+                        fetchedDate @?= duckDBDateDays dateValue
+
+                        DuckDBTime fetchedTime <- c_duckdb_value_time resPtr 15 0
+                        fetchedTime @?= duckDBTimeMicros timeValue
+
+                        DuckDBTimestamp fetchedTs <- c_duckdb_value_timestamp resPtr 16 0
+                        fetchedTs @?= duckDBTimestampMicros timestampValue
+
+                        DuckDBTimestamp fetchedTsTz <- c_duckdb_value_timestamp resPtr 17 0
+                        let tzDifference = fetchedTsTz - duckDBTimestampMicros timestampValue
+                        tzDifference @?= 7200000000
+
+                        alloca \intervalPtr -> do
+                            c_duckdb_value_interval resPtr 18 0 intervalPtr
+                            peek intervalPtr >>= (@?= intervalValue)
+
+                        alloca \decimalPtr -> do
+                            c_duckdb_value_decimal resPtr 19 0 decimalPtr
+                            DuckDBDecimal{duckDBDecimalWidth = width, duckDBDecimalScale = scale} <- peek decimalPtr
+                            (width, scale) @?= (18, 2)
+
+                        varchar <- c_duckdb_value_varchar resPtr 20 0
+                        peekCString varchar >>= (@?= varcharValue)
+                        c_duckdb_free (castPtr varchar)
+
+                        alloca \stringPtr -> do
+                            c_duckdb_value_string resPtr 21 0 stringPtr
+                            DuckDBString{duckDBStringData = datPtr, duckDBStringSize = datSize} <- peek stringPtr
+                            peekCStringLen (datPtr, fromIntegral datSize) >>= (@?= varcharLenValue)
+                            when (datPtr /= nullPtr) $ c_duckdb_free (castPtr datPtr)
+
+                        alloca \blobPtr -> do
+                            c_duckdb_value_blob resPtr 22 0 blobPtr
+                            DuckDBBlob{duckDBBlobData = blobDataPtr, duckDBBlobSize = blobSize} <- peek blobPtr
+                            peekArray (fromIntegral blobSize) (castPtr blobDataPtr :: Ptr Word8) >>= (@?= blobBytes)
+                            c_duckdb_free (castPtr blobDataPtr)
+
+                        c_duckdb_value_is_null resPtr 23 0 >>= (@?= CBool 1)
+
+                        c_duckdb_destroy_result resPtr
+
+                -- Named parameter index lookup (separate statement)
+                withCString "SELECT $named_param" \namedSQL ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn namedSQL stmtPtr
+                        stmt <- peek stmtPtr
+                        when (st /= DuckDBSuccess) $ do
+                            errPtr <- c_duckdb_prepare_error stmt
+                            msg <- if errPtr == nullPtr then pure "prepare failed" else peekCString errPtr
+                            assertFailure msg
+                        st @?= DuckDBSuccess
+                        assertBool "named statement" (stmt /= nullPtr)
+
+                        alloca \idxPtr -> do
+                            stIdx <- withCString "named_param" $ \name -> c_duckdb_bind_parameter_index stmt idxPtr name
+                            when (stIdx /= DuckDBSuccess) $ do
+                                errPtr <- c_duckdb_prepare_error stmt
+                                msg <- if errPtr == nullPtr then pure "bind_parameter_index failed" else peekCString errPtr
+                                assertFailure msg
+                            stIdx @?= DuckDBSuccess
+                            idx <- peek idxPtr
+                            idx @?= 1
+                            bindState <- c_duckdb_bind_int32 stmt idx 42
+                            when (bindState /= DuckDBSuccess) $ do
+                                errPtr <- c_duckdb_prepare_error stmt
+                                msg <- if errPtr == nullPtr then pure "bind failed" else peekCString errPtr
+                                assertFailure msg
+                            bindState @?= DuckDBSuccess
+
+                        alloca \execResPtr -> do
+                            stExec <- c_duckdb_execute_prepared stmt execResPtr
+                            stExec @?= DuckDBSuccess
+                            c_duckdb_row_count execResPtr >>= (@?= 1)
+                            c_duckdb_value_int32 execResPtr 0 0 >>= (@?= 42)
+                            c_duckdb_destroy_result execResPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+  where
+    createSQL =
+        "CREATE TABLE bind_values ("
+            <> "via_value BOOLEAN,"
+            <> "bool_col BOOLEAN,"
+            <> "tiny_col TINYINT,"
+            <> "small_col SMALLINT,"
+            <> "int_col INTEGER,"
+            <> "big_col BIGINT,"
+            <> "huge_col HUGEINT,"
+            <> "uint8_col UTINYINT,"
+            <> "uint16_col USMALLINT,"
+            <> "uint32_col UINTEGER,"
+            <> "uint64_col UBIGINT,"
+            <> "uhuge_col HUGEINT,"
+            <> "float_col FLOAT,"
+            <> "double_col DOUBLE,"
+            <> "date_col DATE,"
+            <> "time_col TIME,"
+            <> "ts_col TIMESTAMP,"
+            <> "tstz_col TIMESTAMP,"
+            <> "interval_col INTERVAL,"
+            <> "decimal_col DECIMAL(18,2),"
+            <> "varchar_col VARCHAR,"
+            <> "varchar_len_col VARCHAR,"
+            <> "blob_col BLOB,"
+            <> "named_null INTEGER"
+            <> ")"
+
+    insertSQL =
+        "INSERT INTO bind_values VALUES ("
+            <> intercalate ", " (replicate 24 "?")
+            <> ")"
+
+    selectSQL = "SELECT * FROM bind_values"
+
+    duckDBDateDays (DuckDBDate d) = d
+    duckDBTimeMicros (DuckDBTime t) = t
+    duckDBTimestampMicros (DuckDBTimestamp t) = t
diff --git a/test/CastFunctionsTest.hs b/test/CastFunctionsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/CastFunctionsTest.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+module CastFunctionsTest (tests) where
+
+import Control.Exception (bracket)
+import Control.Monad (when)
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.Int (Int32)
+import Data.List (isInfixOf)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString, withCStringLen)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, castPtr, freeHaskellFunPtr, nullFunPtr, nullPtr)
+import Foreign.StablePtr (StablePtr, castPtrToStablePtr, castStablePtrToPtr, deRefStablePtr, freeStablePtr, newStablePtr)
+import Foreign.Storable (peekElemOff, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase, withLogicalType, withResult)
+
+data CastHarness = CastHarness
+    { chLastMode :: IO DuckDBCastMode
+    , chExtraSeen :: IO Bool
+    }
+
+tests :: TestTree
+tests =
+    testGroup
+        "Cast Functions"
+        [ castFunctionOverridesBuiltin
+        , castFunctionReportsErrors
+        , castFunctionTryMode
+        ]
+
+castFunctionOverridesBuiltin :: TestTree
+castFunctionOverridesBuiltin =
+    testCase "custom cast overrides builtin conversion" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withTestCast conn \_ -> do
+                    withResult conn "SELECT CAST(v AS VARCHAR) FROM (VALUES (1), (5)) AS t(v)" \resPtr -> do
+                        fetchString resPtr 0 0 >>= (@?= "value: 1")
+                        fetchString resPtr 0 1 >>= (@?= "value: 5")
+                        errPtr <- c_duckdb_result_error resPtr
+                        errPtr @?= nullPtr
+
+castFunctionReportsErrors :: TestTree
+castFunctionReportsErrors =
+    testCase "cast failure surfaces error message" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withTestCast conn \_ ->
+                    withCString "SELECT CAST(v AS VARCHAR) FROM (VALUES (-7)) AS t(v)" \sql ->
+                        alloca \resPtr -> do
+                            state <- c_duckdb_query conn sql resPtr
+                            state @?= DuckDBError
+                            errPtr <- c_duckdb_result_error resPtr
+                            assertBool "expected error pointer" (errPtr /= nullPtr)
+                            errMsg <- peekCString errPtr
+                            assertBool "error message should mention negative" ("negative" `isInfixOf` errMsg)
+                            c_duckdb_destroy_result resPtr
+
+castFunctionTryMode :: TestTree
+castFunctionTryMode =
+    testCase "try_cast produces null rows and reports try mode" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withTestCast conn \CastHarness{chLastMode, chExtraSeen} -> do
+                    withResult conn "SELECT TRY_CAST(v AS VARCHAR) FROM (VALUES (2), (-3)) AS t(v)" \resPtr -> do
+                        fetchString resPtr 0 0 >>= (@?= "value: 2")
+                        c_duckdb_value_is_null resPtr 0 1 >>= (@?= CBool 1)
+                        errPtr <- c_duckdb_result_error resPtr
+                        errPtr @?= nullPtr
+                    chLastMode >>= (@?= DuckDBCastTry)
+                    chExtraSeen >>= (@?= True)
+
+-- Harness ------------------------------------------------------------------
+
+withTestCast :: DuckDBConnection -> (CastHarness -> IO a) -> IO a
+withTestCast conn action = do
+    modeRef <- newIORef DuckDBCastNormal
+    extraSeenRef <- newIORef False
+    prefixStable <- newStablePtr ("value: " :: String)
+    castFunPtr <- mkCastFun (castCallback modeRef extraSeenRef prefixStable)
+    result <-
+        withCastFunction \castFun ->
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \sourceType ->
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \targetType -> do
+                    c_duckdb_cast_function_set_source_type castFun sourceType
+                    c_duckdb_cast_function_set_target_type castFun targetType
+                    c_duckdb_cast_function_set_implicit_cast_cost castFun 0
+                    c_duckdb_cast_function_set_extra_info castFun (castStablePtrToPtr prefixStable) nullFunPtr
+                    c_duckdb_cast_function_set_function castFun castFunPtr
+                    c_duckdb_register_cast_function conn castFun >>= (@?= DuckDBSuccess)
+                    action
+                        CastHarness
+                            { chLastMode = readIORef modeRef
+                            , chExtraSeen = readIORef extraSeenRef
+                            }
+    freeHaskellFunPtr castFunPtr
+    freeStablePtr prefixStable
+    pure result
+
+castCallback :: IORef DuckDBCastMode -> IORef Bool -> StablePtr String -> DuckDBFunctionInfo -> DuckDBIdx -> DuckDBVector -> DuckDBVector -> IO CBool
+castCallback modeRef extraSeenRef prefixStable info count inputVec outputVec = do
+    actualPtr <- c_duckdb_cast_function_get_extra_info info
+    when (actualPtr == castStablePtrToPtr prefixStable) $ writeIORef extraSeenRef True
+    prefix <-
+        if actualPtr == nullPtr
+            then pure "value: "
+            else deRefStablePtr (castPtrToStablePtr actualPtr)
+    mode <- c_duckdb_cast_function_get_cast_mode info
+    writeIORef modeRef mode
+    inputData <- c_duckdb_vector_get_data inputVec
+    c_duckdb_vector_ensure_validity_writable outputVec
+    let inPtr = castPtr inputData :: Ptr Int32
+        rowCount = fromIntegral count :: Int
+    success <- processRows prefix mode inPtr outputVec 0 rowCount
+    pure (if success then CBool 1 else CBool 0)
+  where
+    processRows :: String -> DuckDBCastMode -> Ptr Int32 -> DuckDBVector -> Int -> Int -> IO Bool
+    processRows prefix mode inPtr outVec idx total
+        | idx >= total = pure True
+        | otherwise = do
+            val <- peekElemOff inPtr idx
+            if val < 0
+                then withCString "negative values not allowed" \errMsg ->
+                    if mode == DuckDBCastTry
+                        then do
+                            c_duckdb_cast_function_set_row_error info errMsg (fromIntegral idx) outVec
+                            processRows prefix mode inPtr outVec (idx + 1) total
+                        else do
+                            c_duckdb_cast_function_set_error info errMsg
+                            pure False
+                else do
+                    let rendered = prefix ++ show val
+                    withCStringLen rendered \(cStr, len) ->
+                        c_duckdb_vector_assign_string_element_len outVec (fromIntegral idx) cStr (fromIntegral len)
+                    processRows prefix mode inPtr outVec (idx + 1) total
+
+-- Helpers ------------------------------------------------------------------
+
+withCastFunction :: (DuckDBCastFunction -> IO a) -> IO a
+withCastFunction = bracket c_duckdb_create_cast_function destroy
+  where
+    destroy cf = alloca \ptr -> poke ptr cf >> c_duckdb_destroy_cast_function ptr
+
+fetchString :: Ptr DuckDBResult -> DuckDBIdx -> DuckDBIdx -> IO String
+fetchString resPtr col row = do
+    cStr <- c_duckdb_value_varchar resPtr col row
+    value <- peekCString cStr
+    c_duckdb_free (castPtr cStr)
+    pure value
+
+-- Wrapper constructors ------------------------------------------------------
+
+foreign import ccall "wrapper"
+    mkCastFun :: (DuckDBFunctionInfo -> DuckDBIdx -> DuckDBVector -> DuckDBVector -> IO CBool) -> IO DuckDBCastFunctionFun
diff --git a/test/ConfigurationTest.hs b/test/ConfigurationTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ConfigurationTest.hs
@@ -0,0 +1,46 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ConfigurationTest (tests) where
+
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (peek)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+
+tests :: TestTree
+tests =
+    testGroup
+        "Configuration"
+        [configurationLifecycle]
+
+configurationLifecycle :: TestTree
+configurationLifecycle =
+    testCase "create, inspect, mutate, and destroy configuration" $
+        alloca \configPtr -> do
+            state <- c_duckdb_create_config configPtr
+            state @?= DuckDBSuccess
+
+            config <- peek configPtr
+            assertBool "config pointer should not be null" (config /= nullPtr)
+
+            count <- c_duckdb_config_count
+            assertBool "config flag count should be > 0" (count > 0)
+
+            alloca \namePtr -> alloca \descPtr -> do
+                flagState <- c_duckdb_get_config_flag 0 namePtr descPtr
+                flagState @?= DuckDBSuccess
+                flagName <- peek namePtr >>= peekCString
+                flagDesc <- peek descPtr >>= peekCString
+                assertBool "flag name should not be empty" (not (null flagName))
+                assertBool "flag description should not be empty" (not (null flagDesc))
+
+            setState <-
+                withCString "access_mode" \flag ->
+                    withCString "READ_WRITE" $ \value ->
+                        c_duckdb_set_config config flag value
+            setState @?= DuckDBSuccess
+
+            c_duckdb_destroy_config configPtr
diff --git a/test/DataChunkTest.hs b/test/DataChunkTest.hs
new file mode 100644
--- /dev/null
+++ b/test/DataChunkTest.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE BlockArguments #-}
+
+module DataChunkTest (tests) where
+
+import Control.Exception (bracket)
+import Control.Monad (forM_, void, when, (>=>))
+import Data.Int (Int32)
+import Database.DuckDB.FFI
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (withArray)
+import Foreign.Ptr (Ptr, castPtr, nullPtr, plusPtr)
+import Foreign.Storable (peek, poke, sizeOf)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (testCase, (@?=))
+import Utils (setAllValid, withLogicalType)
+
+-- | Entry point for the Data Chunk focused tests.
+tests :: TestTree
+tests =
+    testGroup
+        "Data Chunk Interface"
+        [ dataChunkLifecycle
+        , dataChunkReset
+        ]
+
+-- | Populate an integer chunk and verify the stored sequence.
+dataChunkLifecycle :: TestTree
+dataChunkLifecycle =
+    testCase "create chunk, fill via vector, and read back" $ do
+        withIntegerLogicalType \intType ->
+            withArray [intType] \typeArray ->
+                withDataChunk (c_duckdb_create_data_chunk typeArray 1) \chunk -> do
+                    c_duckdb_data_chunk_get_column_count chunk >>= (@?= 1)
+
+                    vec <- c_duckdb_data_chunk_get_vector chunk 0
+                    fillVectorWithSequence vec [1 .. 4]
+                    c_duckdb_data_chunk_set_size chunk 4
+
+                    verifyChunkContents chunk [1 .. 4]
+
+-- | Resetting should zero the size yet keep buffers reusable.
+dataChunkReset :: TestTree
+dataChunkReset =
+    testCase "reset clears size and keeps vectors reusable" $ do
+        withIntegerLogicalType \intType ->
+            withArray [intType] \typeArray ->
+                withDataChunk (c_duckdb_create_data_chunk typeArray 1) \chunk -> do
+                    vec <- c_duckdb_data_chunk_get_vector chunk 0
+                    fillVectorWithSequence vec [10, 20, 30]
+                    c_duckdb_data_chunk_set_size chunk 3
+
+                    c_duckdb_data_chunk_reset chunk
+                    c_duckdb_data_chunk_get_size chunk >>= (@?= 0)
+
+                    fillVectorWithSequence vec [7, 8]
+                    c_duckdb_data_chunk_set_size chunk 2
+                    verifyChunkContents chunk [7, 8]
+
+-- Helpers -------------------------------------------------------------------
+
+withIntegerLogicalType :: (DuckDBLogicalType -> IO a) -> IO a
+withIntegerLogicalType = withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger)
+
+withDataChunk :: IO DuckDBDataChunk -> (DuckDBDataChunk -> IO a) -> IO a
+withDataChunk acquire = bracket acquire destroyChunk
+  where
+    destroyChunk chunk = alloca \ptr -> poke ptr chunk >> c_duckdb_destroy_data_chunk ptr
+
+fillVectorWithSequence :: DuckDBVector -> [Int32] -> IO ()
+fillVectorWithSequence vec values = do
+    colType <- c_duckdb_vector_get_column_type vec
+    withLogicalType (pure colType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+    void (c_duckdb_vector_ensure_validity_writable vec)
+    dataPtrRaw <- c_duckdb_vector_get_data vec
+    let dataPtr = castPtr dataPtrRaw :: Ptr Int32
+    validity <- c_duckdb_vector_get_validity vec
+    when (validity /= nullPtr) $ setAllValid validity (length values)
+    forM_ (zip [0 ..] values) (uncurry (pokeElem dataPtr))
+
+verifyChunkContents :: DuckDBDataChunk -> [Int32] -> IO ()
+verifyChunkContents chunk expected = do
+    sz <- c_duckdb_data_chunk_get_size chunk
+    sz @?= fromIntegral (length expected)
+    vec <- c_duckdb_data_chunk_get_vector chunk 0
+    dataPtrRaw <- c_duckdb_vector_get_data vec
+    let dataPtr = castPtr dataPtrRaw :: Ptr Int32
+    forM_ (zip [0 ..] expected) \(idx, val) -> peekElem dataPtr idx >>= (@?= val)
+
+-- validity helpers ----------------------------------------------------------
+
+-- pointer utilities ---------------------------------------------------------
+
+pokeElem :: Ptr Int32 -> Int -> Int32 -> IO ()
+pokeElem base idx = poke (base `plusElem` idx)
+
+peekElem :: Ptr Int32 -> Int -> IO Int32
+peekElem base idx = peek (base `plusElem` idx)
+
+plusElem :: Ptr a -> Int -> Ptr a
+plusElem base idx = base `plusPtr` (idx * sizeOf (undefined :: Int32))
diff --git a/test/ErrorDataTest.hs b/test/ErrorDataTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ErrorDataTest.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ErrorDataTest (tests) where
+
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+
+tests :: TestTree
+tests =
+    testGroup
+        "Error Data"
+        [createInspectDestroy]
+
+createInspectDestroy :: TestTree
+createInspectDestroy =
+    testCase "create error data, inspect properties, destroy" $
+        withCString "synthetic failure" \message -> do
+            errData <- c_duckdb_create_error_data DuckDBErrorInvalid message
+            assertBool "error data pointer should not be null" (errData /= nullPtr)
+
+            errType <- c_duckdb_error_data_error_type errData
+            errType @?= DuckDBErrorInvalid
+
+            retrievedMessagePtr <- c_duckdb_error_data_message errData
+            retrievedMessage <- peekCString retrievedMessagePtr
+            retrievedMessage @?= "synthetic failure"
+
+            hasErr <- c_duckdb_error_data_has_error errData
+            hasErr @?= CBool 1
+
+            alloca \ptr -> do
+                poke ptr errData
+                c_duckdb_destroy_error_data ptr
diff --git a/test/ExecutePreparedStatementsTest.hs b/test/ExecutePreparedStatementsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ExecutePreparedStatementsTest.hs
@@ -0,0 +1,104 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ExecutePreparedStatementsTest (tests) where
+
+import Control.Monad (forM_)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (castPtr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Execute Prepared Statements"
+        [ executePreparedProducesResult
+        , executePreparedStreamingProducesChunks
+        ]
+
+setupTable :: DuckDBConnection -> IO ()
+setupTable conn = do
+    let statements =
+            [ "CREATE TABLE exec_prepared (id INTEGER, name VARCHAR);"
+            , "INSERT INTO exec_prepared VALUES (1, 'alpha'), (2, 'beta');"
+            ]
+    forM_ statements \sql ->
+        withCString sql \cSql ->
+            alloca \resPtr -> do
+                st <- c_duckdb_query conn cSql resPtr
+                st @?= DuckDBSuccess
+                c_duckdb_destroy_result resPtr
+
+executePreparedProducesResult :: TestTree
+executePreparedProducesResult =
+    testCase "execute_prepared returns regular result" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupTable conn
+
+                withCString "SELECT name FROM exec_prepared WHERE id = ?" \querySql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn querySql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        c_duckdb_bind_int32 stmt 1 2 >>= (@?= DuckDBSuccess)
+
+                        alloca \resPtr -> do
+                            execState <- c_duckdb_execute_prepared stmt resPtr
+                            execState @?= DuckDBSuccess
+
+                            streamingFlag <- c_duckdb_result_is_streaming resPtr
+                            streamingFlag @?= CBool 0
+
+                            rowCount <- c_duckdb_row_count resPtr
+                            rowCount @?= 1
+
+                            varcharPtr <- c_duckdb_value_varchar resPtr 0 0
+                            peekCString varcharPtr >>= (@?= "beta")
+                            c_duckdb_free (castPtr varcharPtr)
+
+                            c_duckdb_destroy_result resPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+executePreparedStreamingProducesChunks :: TestTree
+executePreparedStreamingProducesChunks =
+    testCase "execute_prepared_streaming produces streaming result" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupTable conn
+
+                withCString "SELECT id, name FROM exec_prepared ORDER BY id" \querySql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn querySql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "streaming prepared statement should not be null" (stmt /= nullPtr)
+
+                        alloca \resPtr -> do
+                            execState <- c_duckdb_execute_prepared_streaming stmt resPtr
+                            execState @?= DuckDBSuccess
+
+                            streamingFlag <- c_duckdb_result_is_streaming resPtr
+                            streamingFlag @?= CBool 1
+
+                            chunk <- c_duckdb_stream_fetch_chunk resPtr
+                            assertBool "streaming fetch chunk returns data" (chunk /= nullPtr)
+
+                            chunkSize <- c_duckdb_data_chunk_get_size chunk
+                            assertBool "streamed chunk should contain rows" (chunkSize > 0)
+
+                            alloca \chunkPtr -> do
+                                poke chunkPtr chunk
+                                c_duckdb_destroy_data_chunk chunkPtr
+
+                            c_duckdb_destroy_result resPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
diff --git a/test/ExpressionTest.hs b/test/ExpressionTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ExpressionTest.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+module ExpressionTest (tests) where
+
+import Control.Exception (finally)
+import Data.Char (toLower)
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+import Data.List (isInfixOf)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (castPtr, freeHaskellFunPtr, nullFunPtr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (
+    destroyDuckValue,
+    destroyErrorData,
+    destroyLogicalType,
+    withConnection,
+    withDatabase,
+    withLogicalType,
+    withResult,
+    withScalarFunction,
+ )
+
+data ExpressionHarness = ExpressionHarness
+    { ehFoldable :: IO (Maybe Bool)
+    , ehReturnType :: IO (Maybe DuckDBType)
+    , ehFoldedValue :: IO (Maybe String)
+    , ehFoldError :: IO (Maybe String)
+    }
+
+data ExpressionState = ExpressionState
+    { esFoldable :: IORef (Maybe Bool)
+    , esReturnType :: IORef (Maybe DuckDBType)
+    , esFoldValue :: IORef (Maybe String)
+    , esFoldError :: IORef (Maybe String)
+    }
+
+tests :: TestTree
+tests =
+    testGroup
+        "Expression Interface"
+        [ expressionFoldLiteral
+        , expressionNonFoldable
+        ]
+
+expressionFoldLiteral :: TestTree
+expressionFoldLiteral =
+    testCase "folds literal expressions to constant values" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withExpressionFunction conn "expr_literal" \ExpressionHarness{ehFoldable, ehReturnType, ehFoldedValue, ehFoldError} -> do
+                    withResult conn "SELECT expr_literal(42)" \_ -> pure ()
+                    ehFoldable >>= (@?= Just True)
+                    ehReturnType >>= (@?= Just DuckDBTypeInteger)
+                    ehFoldedValue >>= (@?= Just "42")
+                    ehFoldError >>= (@?= Nothing)
+
+expressionNonFoldable :: TestTree
+expressionNonFoldable =
+    testCase "detects non-foldable column references" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withExpressionFunction conn "expr_non_foldable" \ExpressionHarness{ehFoldable, ehReturnType, ehFoldedValue, ehFoldError} -> do
+                    withResult conn "SELECT expr_non_foldable(v) FROM (VALUES (7)) t(v)" \_ -> pure ()
+                    ehFoldable >>= (@?= Just False)
+                    ehReturnType >>= (@?= Just DuckDBTypeInteger)
+                    ehFoldedValue >>= (@?= Nothing)
+                    errMsg <- ehFoldError
+                    assertBool "expected fold error message" $
+                        maybe False (isInfixOf "fold" . map toLower) errMsg
+
+withExpressionFunction :: DuckDBConnection -> String -> (ExpressionHarness -> IO a) -> IO a
+withExpressionFunction conn funcName action = do
+    foldRef <- newIORef Nothing
+    typeRef <- newIORef Nothing
+    valueRef <- newIORef Nothing
+    errorRef <- newIORef Nothing
+    let state = ExpressionState foldRef typeRef valueRef errorRef
+    bindPtr <- mkScalarBindFun (expressionBind state)
+    execPtr <- mkScalarExecFun expressionExec
+    result <-
+        withScalarFunction $ \scalarFun -> do
+            withCString funcName $ \name -> c_duckdb_scalar_function_set_name scalarFun name
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) $ \intType -> do
+                c_duckdb_scalar_function_add_parameter scalarFun intType
+                c_duckdb_scalar_function_set_return_type scalarFun intType
+                c_duckdb_scalar_function_set_bind scalarFun bindPtr
+                c_duckdb_scalar_function_set_function scalarFun execPtr
+                c_duckdb_scalar_function_set_extra_info scalarFun nullPtr nullFunPtr
+                c_duckdb_register_scalar_function conn scalarFun >>= (@?= DuckDBSuccess)
+                action
+                    ExpressionHarness
+                        { ehFoldable = readIORef foldRef
+                        , ehReturnType = readIORef typeRef
+                        , ehFoldedValue = readIORef valueRef
+                        , ehFoldError = readIORef errorRef
+                        }
+    freeHaskellFunPtr bindPtr
+    freeHaskellFunPtr execPtr
+    pure result
+
+expressionBind :: ExpressionState -> DuckDBBindInfo -> IO ()
+expressionBind ExpressionState{esFoldable, esReturnType, esFoldValue, esFoldError} info = do
+    argCount <- c_duckdb_scalar_function_bind_get_argument_count info
+    argCount @?= 1
+    exprHandle <- c_duckdb_scalar_function_bind_get_argument info 0
+    finally
+        ( do
+            foldableFlag <- c_duckdb_expression_is_foldable exprHandle
+            let isFoldable = foldableFlag /= 0
+            writeIORef esFoldable (Just isFoldable)
+            retType <- c_duckdb_expression_return_type exprHandle
+            typeId <- c_duckdb_get_type_id retType
+            destroyLogicalType retType
+            writeIORef esReturnType (Just typeId)
+            alloca \ctxPtr -> do
+                c_duckdb_scalar_function_get_client_context info ctxPtr
+                ctx <- peek ctxPtr
+                alloca \valuePtr -> do
+                    poke valuePtr nullPtr
+                    errData <- c_duckdb_expression_fold ctx exprHandle valuePtr
+                    if errData == nullPtr
+                        then do
+                            valueHandle <- peek valuePtr
+                            if valueHandle == nullPtr
+                                then do
+                                    writeIORef esFoldValue Nothing
+                                    writeIORef esFoldError (Just "fold produced null value")
+                                else do
+                                    rendered <- duckValueToString valueHandle
+                                    writeIORef esFoldValue (Just rendered)
+                                    writeIORef esFoldError Nothing
+                                    destroyDuckValue valueHandle
+                        else do
+                            msgPtr <- c_duckdb_error_data_message errData
+                            msg <- peekCString msgPtr
+                            writeIORef esFoldValue Nothing
+                            writeIORef esFoldError (Just msg)
+                            destroyErrorData errData
+        )
+        (alloca \exprPtr -> poke exprPtr exprHandle >> c_duckdb_destroy_expression exprPtr)
+
+expressionExec :: DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
+expressionExec _ chunk outVec = do
+    inVec <- c_duckdb_data_chunk_get_vector chunk 0
+    c_duckdb_vector_reference_vector outVec inVec
+
+-- Helpers ------------------------------------------------------------------
+
+duckValueToString :: DuckDBValue -> IO String
+duckValueToString value = do
+    strPtr <- c_duckdb_value_to_string value
+    text <- peekCString strPtr
+    c_duckdb_free (castPtr strPtr)
+    pure text
+
+-- Wrapper constructors -----------------------------------------------------
+
+foreign import ccall "wrapper"
+    mkScalarBindFun :: (DuckDBBindInfo -> IO ()) -> IO DuckDBScalarFunctionBindFun
+
+foreign import ccall "wrapper"
+    mkScalarExecFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()) -> IO DuckDBScalarFunctionFun
diff --git a/test/ExtractStatementsTest.hs b/test/ExtractStatementsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ExtractStatementsTest.hs
@@ -0,0 +1,103 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ExtractStatementsTest (tests) where
+
+import Control.Monad (forM_, when)
+import Data.Int (Int64)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (peek)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Extract Statements"
+        [ extractPrepareAndExecuteSequence
+        , extractFailureYieldsErrorMessage
+        ]
+
+extractPrepareAndExecuteSequence :: TestTree
+extractPrepareAndExecuteSequence =
+    testCase "extract, prepare, and execute a multi-statement script" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                let script =
+                        "CREATE TABLE extracted_values(val INTEGER);"
+                            <> "INSERT INTO extracted_values VALUES (1), (2);"
+                            <> "SELECT SUM(val) FROM extracted_values;"
+
+                withCString script \scriptPtr ->
+                    alloca \exPtr -> do
+                        stmtCount <- c_duckdb_extract_statements conn scriptPtr exPtr
+                        stmtCount @?= 3
+
+                        extracted <- peek exPtr
+                        assertBool "extracted handle should not be null" (extracted /= nullPtr)
+
+                        let indices = [0 :: Integer .. fromIntegral stmtCount - 1]
+                        forM_ indices \idx ->
+                            alloca \stmtPtr -> do
+                                let duckIdx = fromIntegral idx
+                                prepState <- c_duckdb_prepare_extracted_statement conn extracted duckIdx stmtPtr
+                                prepState @?= DuckDBSuccess
+
+                                stmt <- peek stmtPtr
+                                assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                                alloca \resPtr -> do
+                                    execState <- c_duckdb_execute_prepared stmt resPtr
+                                    execState @?= DuckDBSuccess
+
+                                    case idx of
+                                        0 -> do
+                                            resultType <- c_duckdb_result_return_type resPtr
+                                            resultType @?= DuckDBResultTypeNothing
+                                        1 -> do
+                                            resultType <- c_duckdb_result_return_type resPtr
+                                            resultType @?= DuckDBResultTypeChangedRows
+                                        2 -> do
+                                            rowCount <- c_duckdb_row_count resPtr
+                                            rowCount @?= 1
+                                            total <- c_duckdb_value_int64 resPtr 0 0
+                                            (total :: Int64) @?= 3
+                                        _ -> pure ()
+
+                                    c_duckdb_destroy_result resPtr
+
+                                c_duckdb_destroy_prepare stmtPtr
+
+                        -- Preparing out-of-range should fail with an informative error
+                        alloca \stmtPtr -> do
+                            let invalidIdx = stmtCount
+                            stInvalid <- c_duckdb_prepare_extracted_statement conn extracted invalidIdx stmtPtr
+                            stInvalid @?= DuckDBError
+                            stmt <- peek stmtPtr
+                            assertBool "invalid index should not yield a statement handle" (stmt == nullPtr)
+
+                        c_duckdb_destroy_extracted exPtr
+
+extractFailureYieldsErrorMessage :: TestTree
+extractFailureYieldsErrorMessage =
+    testCase "failed extract surfaces parser error" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                withCString "SELECT * FROM invalid_table WHERE" \badSql ->
+                    alloca \exPtr -> do
+                        stmtCount <- c_duckdb_extract_statements conn badSql exPtr
+                        stmtCount @?= 0
+
+                        extracted <- peek exPtr
+                        assertBool "extracted handle should be available for errors" (extracted /= nullPtr)
+
+                        errPtr <- c_duckdb_extract_statements_error extracted
+                        when (errPtr == nullPtr) $
+                            assertFailure "expected error message pointer from failed extract"
+                        errMsg <- peekCString errPtr
+                        assertBool "error message should not be empty" (not (null errMsg))
+
+                        c_duckdb_destroy_extracted exPtr
diff --git a/test/HelpersTest.hs b/test/HelpersTest.hs
new file mode 100644
--- /dev/null
+++ b/test/HelpersTest.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE TypeApplications #-}
+
+module HelpersTest (tests) where
+
+import Control.Exception (bracket)
+import Data.Int (Int64)
+import Data.Time.Calendar (diffDays, fromGregorian)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.C.Types (CBool (..), CDouble (..), CSize (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (allocaArray)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek, poke, pokeElemOff)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withLogicalType)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Helpers"
+        [ testMallocFree
+        , testVectorSize
+        , testDateTimeHelpers
+        , testStringHelpers
+        , testHugeIntHelpers
+        , testDecimalHelpers
+        ]
+
+testMallocFree :: TestTree
+testMallocFree =
+    testCase "duckdb_malloc/duckdb_free allocate and release memory" $ do
+        ptr <- c_duckdb_malloc (CSize 128)
+        assertBool "allocation should succeed" (ptr /= nullPtr)
+        c_duckdb_free ptr
+
+testVectorSize :: TestTree
+testVectorSize =
+    testCase "duckdb_vector_size returns positive number" $ do
+        size <- c_duckdb_vector_size
+        assertBool "vector size should be positive" (size > 0)
+
+testDateTimeHelpers :: TestTree
+testDateTimeHelpers =
+    testCase "date/time/timestamp helper round-trips" $ do
+        let day = fromGregorian 2023 6 1
+            epoch = fromGregorian 1970 1 1
+            days = diffDays day epoch
+            duckDay = DuckDBDate (fromIntegral days)
+
+        alloca \dateStructPtr -> do
+            c_duckdb_from_date duckDay dateStructPtr
+            DuckDBDateStruct{duckDBDateStructYear = y, duckDBDateStructMonth = m, duckDBDateStructDay = d} <- peek dateStructPtr
+            (y, m, d) @?= (2023, 6, 1)
+            roundTrippedDay <- c_duckdb_to_date dateStructPtr
+            roundTrippedDay @?= duckDay
+
+        c_duckdb_is_finite_date duckDay >>= (@?= CBool 1)
+
+        let microsPerSecond = 1000000
+            timeMicros = ((12 * 60 + 34) * 60 + 56) * microsPerSecond
+            duckTime = DuckDBTime (fromIntegral timeMicros)
+
+        alloca \timeStructPtr -> do
+            c_duckdb_from_time duckTime timeStructPtr
+            DuckDBTimeStruct{duckDBTimeStructHour = h, duckDBTimeStructMinute = mi, duckDBTimeStructSecond = s, duckDBTimeStructMicros = mu} <- peek timeStructPtr
+            (h, mi, s, mu) @?= (12, 34, 56, 0)
+            roundTrippedTime <- c_duckdb_to_time timeStructPtr
+            roundTrippedTime @?= duckTime
+
+        DuckDBTimeTz tz <- c_duckdb_create_time_tz (fromIntegral @Integer timeMicros) 60
+        alloca \timeTzPtr -> do
+            c_duckdb_from_time_tz (DuckDBTimeTz tz) timeTzPtr
+            DuckDBTimeTzStruct{duckDBTimeTzStructTime = DuckDBTimeStruct{duckDBTimeStructHour = h', duckDBTimeStructMinute = mi', duckDBTimeStructSecond = s'}, duckDBTimeTzStructOffset = offset} <- peek timeTzPtr
+            (h', mi', s', offset) @?= (12, 34, 56, 60)
+
+        let tsMicros :: Int64
+            tsMicros = fromIntegral days * 86400000000 + fromIntegral timeMicros
+            duckTimestamp = DuckDBTimestamp tsMicros
+
+        alloca \tsStructPtr -> do
+            c_duckdb_from_timestamp duckTimestamp tsStructPtr
+            DuckDBTimestampStruct{duckDBTimestampStructDate = DuckDBDateStruct{duckDBDateStructYear = y', duckDBDateStructMonth = m', duckDBDateStructDay = d'}, duckDBTimestampStructTime = DuckDBTimeStruct{duckDBTimeStructHour = hour', duckDBTimeStructMinute = minute', duckDBTimeStructSecond = sec', duckDBTimeStructMicros = micro'}} <-
+                peek tsStructPtr
+            (y', m', d', hour', minute', sec', micro') @?= (2023, 6, 1, 12, 34, 56, 0)
+            roundTrippedTs <- c_duckdb_to_timestamp tsStructPtr
+            roundTrippedTs @?= duckTimestamp
+
+        c_duckdb_is_finite_timestamp duckTimestamp >>= (@?= CBool 1)
+        c_duckdb_is_finite_timestamp_s (DuckDBTimestampS (tsMicros `div` 1000000)) >>= (@?= CBool 1)
+        c_duckdb_is_finite_timestamp_ms (DuckDBTimestampMs (tsMicros `div` 1000)) >>= (@?= CBool 1)
+        c_duckdb_is_finite_timestamp_ns (DuckDBTimestampNs (tsMicros * 1000)) >>= (@?= CBool 1)
+
+testStringHelpers :: TestTree
+testStringHelpers =
+    testCase "string_t helpers inspect inline and heap strings" $ do
+        inspectString "short" True
+        inspectString (replicate 32 'x') False
+  where
+    inspectString text expectInline =
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \varcharType ->
+            allocaArray 1 \typesPtr -> do
+                pokeElemOff typesPtr 0 varcharType
+                bracket
+                    (c_duckdb_create_data_chunk typesPtr 1)
+                    destroyDataChunk
+                    \chunk -> do
+                        vec <- c_duckdb_data_chunk_get_vector chunk 0
+                        withCString text $ \cStr ->
+                            c_duckdb_vector_assign_string_element vec 0 cStr
+                        c_duckdb_data_chunk_set_size chunk 1
+                        dataPtr <- c_duckdb_vector_get_data vec
+                        let stringPtr = castPtr dataPtr :: Ptr DuckDBStringT
+                        inlineFlag <- c_duckdb_string_is_inlined stringPtr
+                        inlineFlag @?= if expectInline then CBool 1 else CBool 0
+                        len <- c_duckdb_string_t_length stringPtr
+                        len @?= fromIntegral (length text)
+                        textPtr <- c_duckdb_string_t_data stringPtr
+                        peekCString textPtr >>= (@?= text)
+
+testHugeIntHelpers :: TestTree
+testHugeIntHelpers =
+    testCase "hugeint/u-hugeint helpers convert values" $ do
+        alloca \hugePtr -> do
+            let hugeVal = DuckDBHugeInt{duckDBHugeIntLower = 123456789, duckDBHugeIntUpper = 0}
+            poke hugePtr hugeVal
+            CDouble dbl <- c_duckdb_hugeint_to_double hugePtr
+            dbl @?= 123456789
+            c_duckdb_double_to_hugeint (CDouble 987654321) hugePtr
+            DuckDBHugeInt{duckDBHugeIntLower = lower, duckDBHugeIntUpper = upper} <- peek hugePtr
+            (upper, lower) @?= (0, 987654321)
+
+        alloca \uhugePtr -> do
+            let uhugeVal = DuckDBUHugeInt{duckDBUHugeIntLower = 987654321, duckDBUHugeIntUpper = 1}
+            poke uhugePtr uhugeVal
+            CDouble dbl <- c_duckdb_uhugeint_to_double uhugePtr
+            dbl @?= fromIntegral @Integer (987654321 + 2 ^ (64 :: Int))
+            c_duckdb_double_to_uhugeint (CDouble 123456789) uhugePtr
+            DuckDBUHugeInt{duckDBUHugeIntLower = lower, duckDBUHugeIntUpper = upper} <- peek uhugePtr
+            (upper, lower) @?= (0, 123456789)
+
+testDecimalHelpers :: TestTree
+testDecimalHelpers =
+    testCase "decimal helper round-trip" $ do
+        let input = CDouble 12345.67
+        alloca \decimalPtr -> do
+            c_duckdb_double_to_decimal input 18 2 decimalPtr
+            DuckDBDecimal{duckDBDecimalWidth = width, duckDBDecimalScale = scale} <- peek decimalPtr
+            (width, scale) @?= (18, 2)
+            result <- c_duckdb_decimal_to_double decimalPtr
+            result @?= input
+
+-- Utilities ----------------------------------------------------------------
+
+destroyDataChunk :: DuckDBDataChunk -> IO ()
+destroyDataChunk chunk =
+    alloca \ptr -> do
+        poke ptr chunk
+        c_duckdb_destroy_data_chunk ptr
diff --git a/test/LogicalTypesTest.hs b/test/LogicalTypesTest.hs
new file mode 100644
--- /dev/null
+++ b/test/LogicalTypesTest.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE BlockArguments #-}
+
+module LogicalTypesTest (tests) where
+
+import Control.Monad (forM_, when, (>=>))
+import Data.Word (Word32, Word8)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Array (withArray)
+import Foreign.Marshal.Utils (withMany)
+import Foreign.Ptr (castPtr, nullPtr)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase, withLogicalType, withResult)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Logical Type Interface"
+        [ primitiveLogicalTypes
+        , decimalLogicalType
+        , enumLogicalType
+        , compositeLogicalTypes
+        , aliasRoundtrip
+        , registerLogicalType
+        ]
+
+primitiveLogicalTypes :: TestTree
+primitiveLogicalTypes =
+    testCase "primitive logical types report their ids" $
+        forM_ primitives \(duckType, expected) ->
+            withLogicalType (c_duckdb_create_logical_type duckType) \lt -> do
+                typeId <- c_duckdb_get_type_id lt
+                typeId @?= expected
+  where
+    primitives =
+        [ (DuckDBTypeBoolean, DuckDBTypeBoolean)
+        , (DuckDBTypeInteger, DuckDBTypeInteger)
+        , (DuckDBTypeVarchar, DuckDBTypeVarchar)
+        , (DuckDBTypeBlob, DuckDBTypeBlob)
+        ]
+
+decimalLogicalType :: TestTree
+decimalLogicalType =
+    testCase "decimal logical type exposes width/scale/internal type" $
+        withLogicalType (c_duckdb_create_decimal_type width scale) \lt -> do
+            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeDecimal)
+            c_duckdb_decimal_width lt >>= (@?= width)
+            c_duckdb_decimal_scale lt >>= (@?= scale)
+            c_duckdb_decimal_internal_type lt >>= (@?= DuckDBTypeBigInt)
+  where
+    width, scale :: Word8
+    width = 18
+    scale = 4
+
+enumLogicalType :: TestTree
+enumLogicalType =
+    testCase "enum logical type exposes dictionary" $ do
+        enumType <-
+            withMany withCString ["Small", "Medium", "Large"] \namePtrs ->
+                withArray namePtrs (`c_duckdb_create_enum_type` 3)
+        withLogicalType (pure enumType) \lt -> do
+            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeEnum)
+            c_duckdb_enum_internal_type lt >>= (@?= DuckDBTypeUTinyInt)
+            c_duckdb_enum_dictionary_size lt >>= (@?= (3 :: Word32))
+            valuePtr <- c_duckdb_enum_dictionary_value lt 1
+            peekCString valuePtr >>= (@?= "Medium")
+            c_duckdb_free (castPtr valuePtr)
+
+compositeLogicalTypes :: TestTree
+compositeLogicalTypes =
+    testCase "composite logical types expose nesting metadata" $ do
+        -- List type
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \child -> do
+            listType <- c_duckdb_create_list_type child
+            withLogicalType (pure listType) \lt -> do
+                c_duckdb_get_type_id lt >>= (@?= DuckDBTypeList)
+                listChild <- c_duckdb_list_type_child_type lt
+                withLogicalType (pure listChild) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+
+        -- Array type
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \arrayChild -> do
+            arrayType <- c_duckdb_create_array_type arrayChild 5
+            withLogicalType (pure arrayType) \lt -> do
+                c_duckdb_get_type_id lt >>= (@?= DuckDBTypeArray)
+                arrayChildType <- c_duckdb_array_type_child_type lt
+                withLogicalType (pure arrayChildType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+                c_duckdb_array_type_array_size lt >>= (@?= 5)
+
+        -- Map type
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \keyType ->
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \valType -> do
+                mapType <- c_duckdb_create_map_type keyType valType
+                withLogicalType (pure mapType) \lt -> do
+                    c_duckdb_get_type_id lt >>= (@?= DuckDBTypeMap)
+                    mapKey <- c_duckdb_map_type_key_type lt
+                    withLogicalType (pure mapKey) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))
+                    mapVal <- c_duckdb_map_type_value_type lt
+                    withLogicalType (pure mapVal) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+
+        -- Struct type
+        withMany withCString ["id", "name"] \fieldNames -> withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \idType ->
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \nameType ->
+                withArray [idType, nameType] \childArray ->
+                    withArray fieldNames \namesArray -> do
+                        structType <- c_duckdb_create_struct_type childArray namesArray 2
+                        withLogicalType (pure structType) \lt -> do
+                            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeStruct)
+                            childCount <- c_duckdb_struct_type_child_count lt
+                            childCount @?= 2
+                            childName0 <- c_duckdb_struct_type_child_name lt 0
+                            peekCString childName0 >>= (@?= "id")
+                            c_duckdb_free (castPtr childName0)
+                            structChild1 <- c_duckdb_struct_type_child_type lt 1
+                            withLogicalType (pure structChild1) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))
+
+        -- Union type
+        withMany withCString ["int_member", "text_member"] \memberNames -> withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intMember ->
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeVarchar) \textMember ->
+                withArray [intMember, textMember] \memberArray ->
+                    withArray memberNames \nameArray -> do
+                        unionType <- c_duckdb_create_union_type memberArray nameArray 2
+                        withLogicalType (pure unionType) \lt -> do
+                            c_duckdb_get_type_id lt >>= (@?= DuckDBTypeUnion)
+                            memberCount <- c_duckdb_union_type_member_count lt
+                            memberCount @?= 2
+                            memberName0 <- c_duckdb_union_type_member_name lt 0
+                            peekCString memberName0 >>= (@?= "int_member")
+                            c_duckdb_free (castPtr memberName0)
+                            memberChild <- c_duckdb_union_type_member_type lt 1
+                            withLogicalType (pure memberChild) (c_duckdb_get_type_id >=> (@?= DuckDBTypeVarchar))
+
+aliasRoundtrip :: TestTree
+aliasRoundtrip =
+    testCase "logical type aliases can be set and retrieved" $
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \lt -> do
+            aliasBefore <- c_duckdb_logical_type_get_alias lt
+            aliasBefore @?= nullPtr
+            withCString "custom_alias" $ \alias -> c_duckdb_logical_type_set_alias lt alias
+            aliasAfter <- c_duckdb_logical_type_get_alias lt
+            assertBool "alias pointer should not be null" (aliasAfter /= nullPtr)
+            when (aliasAfter == nullPtr) $
+                assertFailure "expected alias pointer"
+            peekCString aliasAfter >>= (@?= "custom_alias")
+            c_duckdb_free (castPtr aliasAfter)
+
+registerLogicalType :: TestTree
+registerLogicalType =
+    testCase "registered logical type alias is accepted in SQL" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \lt -> do
+                    let aliasName = "custom_int_alias"
+                    withCString aliasName $ \aliasPtr ->
+                        c_duckdb_logical_type_set_alias lt aliasPtr
+                    c_duckdb_register_logical_type conn lt nullPtr >>= (@?= DuckDBSuccess)
+
+                    let createSql = "CREATE TABLE logical_type_demo (val " ++ aliasName ++ ")"
+                    withResult conn createSql \_ -> pure ()
+
+                    withResult conn "PRAGMA table_info('logical_type_demo')" \resPtr -> do
+                        c_duckdb_row_count resPtr >>= (@?= 1)
+                        typePtr <- c_duckdb_value_varchar resPtr 2 0
+                        typeName <- peekCString typePtr
+                        c_duckdb_free (castPtr typePtr)
+                        typeName @?= aliasName
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,72 @@
+module Main (main) where
+
+import qualified AggregateFunctionsTest
+import qualified AppenderTest
+import qualified ArrowInterfaceDeprecatedTests
+import qualified ArrowInterfaceTest
+import qualified BindValuesTest
+import qualified CastFunctionsTest
+import qualified ConfigurationTest
+import qualified DataChunkTest
+import qualified ErrorDataTest
+import qualified ExecutePreparedStatementsTest
+import qualified ExpressionTest
+import qualified ExtractStatementsTest
+import qualified HelpersTest
+import qualified LogicalTypesTest
+import qualified OpenConnectTest
+import qualified PendingResultTest
+import qualified PreparedStatementsTest
+import qualified ProfilingInfoTest
+import qualified QueryExecutionTest
+import qualified ReplacementScansTest
+import qualified ResultFunctionsTest
+import qualified SafeFetchTest
+import qualified ScalarFunctionsTest
+import qualified SelectionVectorTest
+import qualified StreamingResultTest
+import qualified TableDescriptionTest
+import qualified TableFunctionsTest
+import Test.Tasty (defaultMain, testGroup)
+import qualified ThreadingTest
+import qualified ValidityMaskTest
+import qualified ValueInterfaceTest
+import qualified VectorTest
+
+main :: IO ()
+main =
+    defaultMain $
+        testGroup
+            "DuckDB FFI"
+            [ OpenConnectTest.tests
+            , ErrorDataTest.tests
+            , ConfigurationTest.tests
+            , PreparedStatementsTest.tests
+            , BindValuesTest.tests
+            , ExecutePreparedStatementsTest.tests
+            , ExtractStatementsTest.tests
+            , LogicalTypesTest.tests
+            , DataChunkTest.tests
+            , VectorTest.tests
+            , ValidityMaskTest.tests
+            , ScalarFunctionsTest.tests
+            , AggregateFunctionsTest.tests
+            , TableFunctionsTest.tests
+            , ProfilingInfoTest.tests
+            , SelectionVectorTest.tests
+            , PendingResultTest.tests
+            , ValueInterfaceTest.tests
+            , QueryExecutionTest.tests
+            , ResultFunctionsTest.tests
+            , SafeFetchTest.tests
+            , HelpersTest.tests
+            , TableDescriptionTest.tests
+            , AppenderTest.tests
+            , CastFunctionsTest.tests
+            , ExpressionTest.tests
+            , ReplacementScansTest.tests
+            , ArrowInterfaceDeprecatedTests.tests
+            , ArrowInterfaceTest.tests
+            , StreamingResultTest.tests
+            , ThreadingTest.tests
+            ]
diff --git a/test/OpenConnectTest.hs b/test/OpenConnectTest.hs
new file mode 100644
--- /dev/null
+++ b/test/OpenConnectTest.hs
@@ -0,0 +1,137 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module OpenConnectTest (tests) where
+
+import Control.Monad (when)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (castPtr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+
+tests :: TestTree
+tests =
+    testGroup
+        "Open Connect"
+        [ testLibraryVersion
+        , testOpenAndConnect
+        , testOpenExtWithConfig
+        , testInstanceCache
+        ]
+
+testLibraryVersion :: TestTree
+testLibraryVersion =
+    testCase "duckdb_library_version returns a non-empty string" $ do
+        versionPtr <- c_duckdb_library_version
+        version <- peekCString versionPtr
+        assertBool "library version should not be empty" (not (null version))
+
+testOpenAndConnect :: TestTree
+testOpenAndConnect =
+    testCase "open, connect, and gather connection details" $
+        withCString ":memory:" $ \path ->
+            alloca $ \dbPtr -> do
+                state <- c_duckdb_open path dbPtr
+                state @?= DuckDBSuccess
+                db <- peek dbPtr
+
+                alloca $ \connPtr -> do
+                    connState <- c_duckdb_connect db connPtr
+                    connState @?= DuckDBSuccess
+                    conn <- peek connPtr
+
+                    -- Query progress information (should succeed even without a running query)
+                    alloca $ \progressPtr -> do
+                        poke progressPtr (DuckDBQueryProgress 0 0 0)
+                        c_duckdb_query_progress conn progressPtr
+                        DuckDBQueryProgress{..} <- peek progressPtr
+                        assertBool "rows processed should be non-negative" (duckDBQueryProgressRowsProcessed >= 0 && duckDBQueryProgressTotalRows >= 0)
+
+                    -- Interrupt is a no-op but should succeed
+                    c_duckdb_interrupt conn
+
+                    -- Retrieve client context and confirm it points back to the same connection
+                    alloca $ \ctxPtr -> do
+                        poke ctxPtr nullPtr
+                        c_duckdb_connection_get_client_context conn ctxPtr
+                        ctx <- peek ctxPtr
+                        assertBool "client context should not be null" (ctx /= nullPtr)
+                        cid <- c_duckdb_client_context_get_connection_id ctx
+                        assertBool "connection id should be non-negative" (cid >= 0)
+                        c_duckdb_destroy_client_context ctxPtr
+
+                    -- Retrieve arrow options object
+                    alloca $ \arrowPtr -> do
+                        poke arrowPtr nullPtr
+                        c_duckdb_connection_get_arrow_options conn arrowPtr
+                        arrowOpts <- peek arrowPtr
+                        assertBool "arrow options should not be null" (arrowOpts /= nullPtr)
+                        c_duckdb_destroy_arrow_options arrowPtr
+
+                    -- Fetch table names (there should be none, but the call should succeed)
+                    alloca $ \valuePtr -> do
+                        tablesValue <- withCString "%" $ \filterStr -> c_duckdb_get_table_names conn filterStr (CBool 0)
+                        poke valuePtr tablesValue
+                        c_duckdb_destroy_value valuePtr
+
+                    -- Disconnect and close database
+                    c_duckdb_disconnect connPtr
+
+                c_duckdb_close dbPtr
+
+testOpenExtWithConfig :: TestTree
+testOpenExtWithConfig =
+    testCase "open_ext with configuration" $
+        withCString ":memory:" $ \path ->
+            alloca $ \configPtr -> do
+                cfgState <- c_duckdb_create_config configPtr
+                cfgState @?= DuckDBSuccess
+                config <- peek configPtr
+
+                alloca $ \errorPtr -> do
+                    poke errorPtr nullPtr
+                    alloca $ \dbPtr -> do
+                        state <- c_duckdb_open_ext path dbPtr config errorPtr
+                        state @?= DuckDBSuccess
+
+                        errMsgPtr <- peek errorPtr
+                        when (errMsgPtr /= nullPtr) $
+                            c_duckdb_free (castPtr errMsgPtr)
+
+                        c_duckdb_close dbPtr
+
+                c_duckdb_destroy_config configPtr
+
+testInstanceCache :: TestTree
+testInstanceCache =
+    testCase "create instance cache and reuse database handle" $
+        withCString ":memory:" $ \path ->
+            alloca $ \configPtr -> do
+                cfgState <- c_duckdb_create_config configPtr
+                cfgState @?= DuckDBSuccess
+                config <- peek configPtr
+
+                cache <- c_duckdb_create_instance_cache
+                assertBool "instance cache should not be null" (cache /= nullPtr)
+
+                alloca $ \dbPtr -> do
+                    alloca $ \errorPtr -> do
+                        poke errorPtr nullPtr
+                        state <- c_duckdb_get_or_create_from_cache cache path dbPtr config errorPtr
+                        state @?= DuckDBSuccess
+                        err <- peek errorPtr
+                        when (err /= nullPtr) $
+                            c_duckdb_free (castPtr err)
+
+                        -- the database returned from the cache should be usable (we close it immediately)
+                        c_duckdb_close dbPtr
+
+                alloca $ \cachePtr -> do
+                    poke cachePtr cache
+                    c_duckdb_destroy_instance_cache cachePtr
+
+                c_duckdb_destroy_config configPtr
diff --git a/test/PendingResultTest.hs b/test/PendingResultTest.hs
new file mode 100644
--- /dev/null
+++ b/test/PendingResultTest.hs
@@ -0,0 +1,212 @@
+{-# LANGUAGE BlockArguments #-}
+
+module PendingResultTest (tests) where
+
+import Control.Monad (forM_, when)
+import Data.Int (Int32, Int64)
+import Data.Maybe (isNothing)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Pending Result Interface"
+        [ pendingPreparedRoundtrip
+        , pendingPreparedStreamingRoundtrip
+        , pendingPreparedReportsError
+        ]
+
+withChunk :: DuckDBDataChunk -> (Ptr DuckDBDataChunk -> IO ()) -> IO ()
+withChunk chunk action =
+    alloca \chunkPtr -> do
+        poke chunkPtr chunk
+        action chunkPtr
+
+pendingErrorMessage :: DuckDBPendingResult -> IO (Maybe String)
+pendingErrorMessage pending = do
+    errPtr <- c_duckdb_pending_error pending
+    if errPtr == nullPtr
+        then pure Nothing
+        else Just <$> peekCString errPtr
+
+assertPendingState :: DuckDBPendingState -> IO ()
+assertPendingState state =
+    let valid =
+            state == DuckDBPendingResultReady
+                || state == DuckDBPendingResultNotReady
+                || state == DuckDBPendingError
+                || state == DuckDBPendingNoTasksAvailable
+     in assertBool "unexpected pending state" valid
+
+pendingPreparedRoundtrip :: TestTree
+pendingPreparedRoundtrip =
+    testCase "pending_prepared executes query to completion" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                forM_
+                    [ "CREATE TABLE pending_numbers(val INTEGER);"
+                    , "INSERT INTO pending_numbers VALUES (1), (2), (3);"
+                    ]
+                    \sql ->
+                        withCString sql \cSql ->
+                            alloca \resPtr -> do
+                                st <- c_duckdb_query conn cSql resPtr
+                                st @?= DuckDBSuccess
+                                c_duckdb_destroy_result resPtr
+
+                withCString "SELECT SUM(val) FROM pending_numbers;" \querySql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn querySql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        alloca \pendingPtr -> do
+                            stPending <- c_duckdb_pending_prepared stmt pendingPtr
+                            stPending @?= DuckDBSuccess
+                            pending <- peek pendingPtr
+                            assertBool "pending result should not be null" (pending /= nullPtr)
+
+                            stateBefore <- c_duckdb_pending_execute_check_state pending
+                            assertPendingState stateBefore
+                            _ <- c_duckdb_pending_execution_is_finished stateBefore
+
+                            taskState <- c_duckdb_pending_execute_task pending
+                            assertPendingState taskState
+                            _ <- c_duckdb_pending_execution_is_finished taskState
+
+                            alloca \resPtr -> do
+                                execState <- c_duckdb_execute_pending pending resPtr
+                                execState @?= DuckDBSuccess
+
+                                resultFinished <- c_duckdb_pending_execute_check_state pending
+                                assertPendingState resultFinished
+
+                                rowCount <- c_duckdb_row_count resPtr
+                                rowCount @?= 1
+                                total <- c_duckdb_value_int64 resPtr 0 0
+                                (total :: Int64) @?= 6
+
+                                c_duckdb_destroy_result resPtr
+
+                            errMsg <- pendingErrorMessage pending
+                            assertBool "no error expected for successful pending execution" (isNothing errMsg)
+
+                            c_duckdb_destroy_pending pendingPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+pendingPreparedStreamingRoundtrip :: TestTree
+pendingPreparedStreamingRoundtrip =
+    testCase "pending_prepared_streaming yields streaming duckdb_result" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                forM_
+                    [ "CREATE TABLE pending_stream(id INTEGER);"
+                    , "INSERT INTO pending_stream VALUES (1), (2), (3), (4);"
+                    ]
+                    \sql ->
+                        withCString sql \cSql ->
+                            alloca \resPtr -> do
+                                st <- c_duckdb_query conn cSql resPtr
+                                st @?= DuckDBSuccess
+                                c_duckdb_destroy_result resPtr
+
+                withCString "SELECT * FROM pending_stream ORDER BY id;" \querySql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn querySql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        alloca \pendingPtr -> do
+                            stPending <- c_duckdb_pending_prepared_streaming stmt pendingPtr
+                            stPending @?= DuckDBSuccess
+                            pending <- peek pendingPtr
+                            assertBool "pending result should not be null" (pending /= nullPtr)
+
+                            stateBefore <- c_duckdb_pending_execute_check_state pending
+                            assertPendingState stateBefore
+                            _ <- c_duckdb_pending_execution_is_finished stateBefore
+
+                            taskState <- c_duckdb_pending_execute_task pending
+                            assertPendingState taskState
+                            _ <- c_duckdb_pending_execution_is_finished taskState
+
+                            alloca \resPtr -> do
+                                execState <- c_duckdb_execute_pending pending resPtr
+                                execState @?= DuckDBSuccess
+
+                                streamingFlag <- c_duckdb_result_is_streaming resPtr
+                                streamingFlag @?= CBool 1
+
+                                chunk <- c_duckdb_stream_fetch_chunk resPtr
+                                assertBool "streaming fetch should yield a chunk" (chunk /= nullPtr)
+
+                                chunkSize <- c_duckdb_data_chunk_get_size chunk
+                                assertBool "streamed chunk should have rows" (chunkSize > 0)
+
+                                withChunk chunk c_duckdb_destroy_data_chunk
+                                c_duckdb_destroy_result resPtr
+
+                            errMsg <- pendingErrorMessage pending
+                            assertBool "no error expected for successful streaming execution" (isNothing errMsg)
+
+                            c_duckdb_destroy_pending pendingPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+pendingPreparedReportsError :: TestTree
+pendingPreparedReportsError =
+    testCase "pending execution surfaces failure details" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                forM_
+                    [ "CREATE TABLE pending_unique(val INTEGER PRIMARY KEY);"
+                    , "INSERT INTO pending_unique VALUES (1);"
+                    ]
+                    \sql ->
+                        withCString sql \cSql ->
+                            alloca \resPtr -> do
+                                st <- c_duckdb_query conn cSql resPtr
+                                st @?= DuckDBSuccess
+                                c_duckdb_destroy_result resPtr
+
+                withCString "INSERT INTO pending_unique VALUES (?);" \insertSql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn insertSql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared insert statement should not be null" (stmt /= nullPtr)
+
+                        c_duckdb_bind_int32 stmt 1 (1 :: Int32) >>= (@?= DuckDBSuccess)
+
+                        alloca \pendingPtr -> do
+                            stPending <- c_duckdb_pending_prepared stmt pendingPtr
+                            stPending @?= DuckDBSuccess
+                            pending <- peek pendingPtr
+                            assertBool "pending result should not be null" (pending /= nullPtr)
+
+                            _ <- c_duckdb_pending_execute_check_state pending
+                            taskState <- c_duckdb_pending_execute_task pending
+                            assertPendingState taskState
+
+                            alloca \resPtr -> do
+                                execState <- c_duckdb_execute_pending pending resPtr
+                                execState @?= DuckDBError
+
+                            errMsg <- pendingErrorMessage pending
+                            when (maybe False null errMsg) $
+                                assertFailure "pending error message should not be empty when present"
+
+                            c_duckdb_destroy_pending pendingPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
diff --git a/test/PreparedStatementsTest.hs b/test/PreparedStatementsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/PreparedStatementsTest.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE BlockArguments #-}
+
+module PreparedStatementsTest (tests) where
+
+import Control.Monad (when)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Prepared Statements"
+        [ preparedStatementMetadata
+        , preparedStatementError
+        ]
+
+preparedStatementMetadata :: TestTree
+preparedStatementMetadata =
+    testCase "inspect prepared statement metadata" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                -- Seed table for column metadata checks
+                withCString "CREATE TABLE numbers(value INTEGER)" \ddl -> do
+                    alloca \resPtr -> do
+                        st <- c_duckdb_query conn ddl resPtr
+                        st @?= DuckDBSuccess
+                        c_duckdb_destroy_result resPtr
+
+                let sql = "SELECT value FROM numbers WHERE value = ?"
+                withCString sql \cSql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn cSql stmtPtr
+                        st @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "statement pointer should not be null" (stmt /= nullPtr)
+
+                        -- Parameter metadata
+                        paramCount <- c_duckdb_nparams stmt
+                        paramCount @?= 1
+
+                        paramType <- c_duckdb_param_type stmt 0
+                        -- Physical type defaults to invalid until bound; logical type carries information.
+                        paramType @?= DuckDBTypeInvalid
+
+                        logicalTypePtr <- c_duckdb_param_logical_type stmt 0
+                        when (logicalTypePtr /= nullPtr) $ do
+                            alloca \typePtr -> do
+                                poke typePtr logicalTypePtr
+                                c_duckdb_destroy_logical_type typePtr
+
+                        namePtr <- c_duckdb_parameter_name stmt 0
+                        when (namePtr /= nullPtr) $ do
+                            name <- peekCString namePtr
+                            name @?= ""
+
+                        -- Statement type
+                        stmtType <- c_duckdb_prepared_statement_type stmt
+                        stmtType @?= DuckDBStatementTypeSelect
+
+                        -- Column metadata
+                        colCount <- c_duckdb_prepared_statement_column_count stmt
+                        colCount @?= 1
+
+                        colNamePtr <- c_duckdb_prepared_statement_column_name stmt 0
+                        colName <- peekCString colNamePtr
+                        colName @?= "value"
+
+                        colType <- c_duckdb_prepared_statement_column_type stmt 0
+                        colType @?= DuckDBTypeInteger
+
+                        colLogicalType <- c_duckdb_prepared_statement_column_logical_type stmt 0
+                        assertBool "column logical type should not be null" (colLogicalType /= nullPtr)
+                        alloca \colTypePtr -> do
+                            poke colTypePtr colLogicalType
+                            c_duckdb_destroy_logical_type colTypePtr
+
+                        -- Clear bindings succeeds even before binding values
+                        c_duckdb_clear_bindings stmt >>= (@?= DuckDBSuccess)
+
+                        -- No preparation error
+                        errPtr <- c_duckdb_prepare_error stmt
+                        when (errPtr /= nullPtr) $ do
+                            msg <- peekCString errPtr
+                            msg @?= ""
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+preparedStatementError :: TestTree
+preparedStatementError =
+    testCase "prepare error surfaces message" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                let badSql = "SELECT * FROM non_existing_table WHERE value = ?"
+                withCString badSql \cSql ->
+                    alloca \stmtPtr -> do
+                        st <- c_duckdb_prepare conn cSql stmtPtr
+                        st @?= DuckDBError
+                        stmt <- peek stmtPtr
+                        if stmt == nullPtr
+                            then assertFailure "expected prepared statement handle after failure"
+                            else do
+                                errMsgPtr <- c_duckdb_prepare_error stmt
+                                when (errMsgPtr == nullPtr) $ assertFailure "expected error message for failed prepare"
+                                msg <- peekCString errMsgPtr
+                                assertBool "error message should mention table" (not (null msg))
+                                c_duckdb_destroy_prepare stmtPtr
diff --git a/test/ProfilingInfoTest.hs b/test/ProfilingInfoTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ProfilingInfoTest.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ProfilingInfoTest (tests) where
+
+import Control.Monad (forM)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (castPtr, nullPtr)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (destroyDuckValue, withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Profiling Info"
+        [ profilingDisabledByDefault
+        , profilingMetricsRoundtrip
+        ]
+
+profilingDisabledByDefault :: TestTree
+profilingDisabledByDefault =
+    testCase "profiling info requires enabling profiling" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                infoPtr <- c_duckdb_get_profiling_info conn
+                infoPtr @?= nullPtr
+
+profilingMetricsRoundtrip :: TestTree
+profilingMetricsRoundtrip =
+    testCase "collect metrics and traverse profiling tree" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement conn "PRAGMA enable_profiling='no_output'"
+                runStatement conn "CREATE TABLE profiling_numbers(value INTEGER)"
+                runStatement conn "INSERT INTO profiling_numbers VALUES (1), (2), (3)"
+                runStatement conn "SELECT sum(value) FROM profiling_numbers"
+
+                infoPtr <- c_duckdb_get_profiling_info conn
+                assertBool "profiling info pointer should be non-null" (infoPtr /= nullPtr)
+
+                metricsVal <- c_duckdb_profiling_info_get_metrics infoPtr
+                entryCountIdx <- c_duckdb_get_map_size metricsVal
+                assertBool "expected at least one metric entry" (entryCountIdx > 0)
+                entries <- collectMetrics metricsVal entryCountIdx
+                destroyDuckValue metricsVal
+
+                assertBool "metrics map should contain entries" (not (null entries))
+                let (firstKey, firstValue) = head entries
+
+                withCString firstKey \keyPtr -> do
+                    valueHandle <- c_duckdb_profiling_info_get_value infoPtr keyPtr
+                    assertBool ("metric " <> firstKey <> " should be present") (valueHandle /= nullPtr)
+                    fetchedValue <- duckValueToString valueHandle
+                    destroyDuckValue valueHandle
+                    fetchedValue @?= firstValue
+
+                childCount <- c_duckdb_profiling_info_get_child_count infoPtr
+                assertBool "expected at least one child node" (childCount > 0)
+                let firstChildIdx = 0
+                childPtr <- c_duckdb_profiling_info_get_child infoPtr firstChildIdx
+                assertBool "child pointer should be non-null" (childPtr /= nullPtr)
+
+                childMetrics <- c_duckdb_profiling_info_get_metrics childPtr
+                childCountIdx <- c_duckdb_get_map_size childMetrics
+                assertBool "child node should expose metrics" (childCountIdx > 0)
+                destroyDuckValue childMetrics
+
+runStatement :: DuckDBConnection -> String -> IO ()
+runStatement conn sql =
+    withCString sql \sqlPtr ->
+        alloca \resPtr -> do
+            state <- c_duckdb_query conn sqlPtr resPtr
+            if state == DuckDBSuccess
+                then c_duckdb_destroy_result resPtr
+                else do
+                    errPtr <- c_duckdb_result_error resPtr
+                    errMsg <-
+                        if errPtr == nullPtr
+                            then pure "unknown error"
+                            else peekCString errPtr
+                    c_duckdb_destroy_result resPtr
+                    assertFailure ("duckdb_query failed: " <> errMsg)
+
+collectMetrics :: DuckDBValue -> DuckDBIdx -> IO [(String, String)]
+collectMetrics metricsVal entryCountIdx = do
+    let entryCount = fromIntegral entryCountIdx :: Int
+    forM [0 .. entryCount - 1] \i -> do
+        let idx = fromIntegral i :: DuckDBIdx
+        keyHandle <- c_duckdb_get_map_key metricsVal idx
+        keyName <- duckValueToText keyHandle
+        destroyDuckValue keyHandle
+
+        valHandle <- c_duckdb_get_map_value metricsVal idx
+        valText <- duckValueToString valHandle
+        destroyDuckValue valHandle
+
+        pure (keyName, valText)
+
+duckValueToText :: DuckDBValue -> IO String
+duckValueToText valHandle = do
+    strPtr <- c_duckdb_get_varchar valHandle
+    text <- peekCString strPtr
+    c_duckdb_free (castPtr strPtr)
+    pure text
+
+duckValueToString :: DuckDBValue -> IO String
+duckValueToString valHandle = do
+    strPtr <- c_duckdb_value_to_string valHandle
+    text <- peekCString strPtr
+    c_duckdb_free (castPtr strPtr)
+    pure text
diff --git a/test/QueryExecutionTest.hs b/test/QueryExecutionTest.hs
new file mode 100644
--- /dev/null
+++ b/test/QueryExecutionTest.hs
@@ -0,0 +1,92 @@
+{-# LANGUAGE BlockArguments #-}
+
+module QueryExecutionTest (tests) where
+
+import Control.Monad (forM_, when)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Query Execution"
+        [ queryLifecycle
+        ]
+
+queryLifecycle :: TestTree
+queryLifecycle =
+    testCase "run a query and inspect metadata" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                -- Seed table
+                let createSQL = "CREATE TABLE items(id INTEGER, name VARCHAR)"
+                    insertSQL = "INSERT INTO items VALUES (1, 'apple'), (2, 'banana')"
+                forM_ [createSQL, insertSQL] \sql ->
+                    withCString sql \cSql ->
+                        alloca \resPtr -> do
+                            st <- c_duckdb_query conn cSql resPtr
+                            st @?= DuckDBSuccess
+                            c_duckdb_destroy_result resPtr
+
+                -- Query data
+                withCString "SELECT id, name FROM items ORDER BY id" \selectSql ->
+                    alloca \resPtr -> do
+                        st <- c_duckdb_query conn selectSql resPtr
+                        st @?= DuckDBSuccess
+
+                        -- Column metadata
+                        columnCount <- c_duckdb_column_count resPtr
+                        columnCount @?= 2
+
+                        rowCount <- c_duckdb_row_count resPtr
+                        rowCount @?= 2
+
+                        rowsChanged <- c_duckdb_rows_changed resPtr
+                        rowsChanged @?= 0
+
+                        stmtType <- c_duckdb_result_statement_type resPtr
+                        stmtType @?= DuckDBStatementTypeSelect
+
+                        arrowOpts <- c_duckdb_result_get_arrow_options resPtr
+                        assertBool "arrow options should not be null" (arrowOpts /= nullPtr)
+                        alloca \arrowPtr -> do
+                            poke arrowPtr arrowOpts
+                            c_duckdb_destroy_arrow_options arrowPtr
+
+                        forM_ [0 .. columnCount - 1] \idx -> do
+                            namePtr <- c_duckdb_column_name resPtr idx
+                            colName <- peekCString namePtr
+                            let expected = if idx == 0 then "id" else "name"
+                            colName @?= expected
+
+                            colType <- c_duckdb_column_type resPtr idx
+                            let expectedType = if idx == 0 then DuckDBTypeInteger else DuckDBTypeVarchar
+                            colType @?= expectedType
+
+                            logicalType <- c_duckdb_column_logical_type resPtr idx
+                            assertBool "logical type pointer should not be null" (logicalType /= nullPtr)
+                            alloca \typePtr -> do
+                                poke typePtr logicalType
+                                c_duckdb_destroy_logical_type typePtr
+
+                            dataPtr <- c_duckdb_column_data resPtr idx
+                            assertBool "column data pointer should not be null" (dataPtr /= nullPtr)
+
+                            nullmaskPtr <- c_duckdb_nullmask_data resPtr idx
+                            assertBool "nullmask pointer should not be null" (nullmaskPtr /= nullPtr)
+
+                        errPtr <- c_duckdb_result_error resPtr
+                        when (errPtr /= nullPtr) $ do
+                            errMsg <- peekCString errPtr
+                            assertBool ("unexpected error: " <> errMsg) False
+
+                        errType <- c_duckdb_result_error_type resPtr
+                        errType @?= DuckDBErrorInvalid
+
+                        c_duckdb_destroy_result resPtr
diff --git a/test/ReplacementScansTest.hs b/test/ReplacementScansTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ReplacementScansTest.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ReplacementScansTest (tests) where
+
+import Control.Concurrent (runInBoundThread)
+import Control.Exception (bracket)
+import Control.Monad (forM_)
+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
+import Data.Int (Int64)
+import Data.List (isInfixOf)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, freeHaskellFunPtr, nullFunPtr, nullPtr)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase, withResult, withValue)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Replacement Scans"
+        [ replacementScanRewritesAndErrors
+        ]
+
+replacementScanRewritesAndErrors :: TestTree
+replacementScanRewritesAndErrors =
+    testCase "replacement scan rewrites table name and can report errors" $
+        runInBoundThread do
+            seenTablesRef <- newIORef []
+
+            let startValue = 5
+                countValue = 4
+                endValue = startValue + countValue
+
+            withReplacementCallback seenTablesRef startValue endValue \callback -> do
+                withDatabase \db -> do
+                    c_duckdb_add_replacement_scan db callback nullPtr nullFunPtr
+                    withConnection db \conn -> do
+                        assertReplacementQuery conn startValue countValue
+                        assertReplacementError conn
+
+            seenTables <- readIORef seenTablesRef
+            assertBool "replacement callback should run for rewrite target" ("haskell_magic" `elem` seenTables)
+            assertBool "replacement callback should run for error target" ("failing_magic" `elem` seenTables)
+
+withReplacementCallback ::
+    IORef [String] ->
+    Int64 ->
+    Int64 ->
+    (DuckDBReplacementCallback -> IO a) ->
+    IO a
+withReplacementCallback seenTablesRef startValue endValue =
+    bracket acquire freeHaskellFunPtr
+  where
+    acquire =
+        mkReplacementCallback (replacementCallback seenTablesRef startValue endValue)
+
+replacementCallback ::
+    IORef [String] ->
+    Int64 ->
+    Int64 ->
+    DuckDBReplacementScanInfo ->
+    CString ->
+    Ptr () ->
+    IO ()
+replacementCallback seenTablesRef startValue endValue info tableName _extra = do
+    name <- peekCString tableName
+    modifyIORef' seenTablesRef (name :)
+    case name of
+        "haskell_magic" -> do
+            withCString "range" $ \fn ->
+                c_duckdb_replacement_scan_set_function_name info fn
+            withValue (c_duckdb_create_int64 startValue) $ \startVal ->
+                c_duckdb_replacement_scan_add_parameter info startVal
+            withValue (c_duckdb_create_int64 endValue) $ \endVal ->
+                c_duckdb_replacement_scan_add_parameter info endVal
+        "failing_magic" ->
+            withCString "replacement rejected by test callback" $ \msg ->
+                c_duckdb_replacement_scan_set_error info msg
+        _ ->
+            pure ()
+
+assertReplacementQuery :: DuckDBConnection -> Int64 -> Int64 -> IO ()
+assertReplacementQuery conn startValue countValue =
+    withResult conn "SELECT range FROM haskell_magic ORDER BY range" \resPtr -> do
+        rowCount <- c_duckdb_row_count resPtr
+        rowCount @?= fromIntegral countValue
+        forM_ [0 .. countValue - 1] \idx -> do
+            value <- c_duckdb_value_int64 resPtr 0 (fromIntegral idx)
+            value @?= startValue + idx
+
+assertReplacementError :: DuckDBConnection -> IO ()
+assertReplacementError conn =
+    withCString "SELECT * FROM failing_magic" \sql ->
+        alloca \resPtr -> do
+            state <- c_duckdb_query conn sql resPtr
+            state @?= DuckDBError
+            errPtr <- c_duckdb_result_error resPtr
+            errMsg <- peekCString errPtr
+            assertBool "replacement error message should surface" ("rejected" `isInfixOf` errMsg)
+            c_duckdb_destroy_result resPtr
+
+foreign import ccall safe "wrapper"
+    mkReplacementCallback ::
+        (DuckDBReplacementScanInfo -> CString -> Ptr () -> IO ()) ->
+        IO DuckDBReplacementCallback
diff --git a/test/ResultFunctionsTest.hs b/test/ResultFunctionsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ResultFunctionsTest.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ResultFunctionsTest (tests) where
+
+import Control.Monad (forM_, when)
+import Database.DuckDB.FFI
+import Foreign.C.String (withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Result Functions"
+        [ chunkIntrospection
+        ]
+
+chunkIntrospection :: TestTree
+chunkIntrospection =
+    testCase "result chunk information and streaming state" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                let createSQL = "CREATE TABLE items(id INTEGER);"
+                    insertSQL =
+                        "INSERT INTO items VALUES (1), (2), (3), (4), (5);"
+                forM_ [createSQL, insertSQL] \sql ->
+                    withCString sql \cSql ->
+                        alloca \resPtr -> do
+                            st <- c_duckdb_query conn cSql resPtr
+                            st @?= DuckDBSuccess
+                            c_duckdb_destroy_result resPtr
+
+                withCString "SELECT * FROM items" \selectSQL ->
+                    alloca \resPtr -> do
+                        st <- c_duckdb_query conn selectSQL resPtr
+                        st @?= DuckDBSuccess
+
+                        chunkCount <- c_duckdb_result_chunk_count resPtr
+                        assertBool "chunk count should be positive" (chunkCount > 0)
+
+                        returnType <- c_duckdb_result_return_type resPtr
+                        returnType @?= DuckDBResultTypeQueryResult
+
+                        streamingFlag <- c_duckdb_result_is_streaming resPtr
+                        streamingFlag @?= CBool 0
+
+                        -- Retrieve first chunk if available
+                        when (chunkCount > 0) $ do
+                            chunk0 <- c_duckdb_result_get_chunk resPtr 0
+                            assertBool "first chunk should not be null" (chunk0 /= nullPtr)
+                            -- Requesting beyond the available chunk count should return null
+                            chunkInvalid <- c_duckdb_result_get_chunk resPtr chunkCount
+                            assertBool "out-of-range chunk should be null" (chunkInvalid == nullPtr)
+
+                        c_duckdb_destroy_result resPtr
diff --git a/test/SafeFetchTest.hs b/test/SafeFetchTest.hs
new file mode 100644
--- /dev/null
+++ b/test/SafeFetchTest.hs
@@ -0,0 +1,172 @@
+{-# LANGUAGE BlockArguments #-}
+
+module SafeFetchTest (tests) where
+
+import Control.Monad (when)
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Time.Calendar (diffDays, fromGregorian)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, peekCStringLen, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Safe Fetch Functions"
+        [safeFetchRoundtrip]
+
+safeFetchRoundtrip :: TestTree
+safeFetchRoundtrip =
+    testCase "retrieve various types via safe fetch accessors" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                let query =
+                        "SELECT "
+                            <> "TRUE AS b," -- 0
+                            <> "(-5)::TINYINT AS i8," -- 1
+                            <> "(-300)::SMALLINT AS i16," -- 2
+                            <> "(-4000000)::INTEGER AS i32," -- 3
+                            <> "(-5000000000000)::BIGINT AS i64," -- 4
+                            <> "('9223372036854775809')::HUGEINT AS huge," -- 5
+                            <> "200::UTINYINT AS u8," -- 6
+                            <> "60000::USMALLINT AS u16," -- 7
+                            <> "4000000000::UINTEGER AS u32," -- 8
+                            <> "('18446744073709551615')::UBIGINT AS u64," -- 9
+                            <> "('18446744073709551616')::HUGEINT AS uhuge," -- 10
+                            <> "1.5::REAL AS f32," -- 11
+                            <> "2.5::DOUBLE AS f64," -- 12
+                            <> "DATE '2021-07-20' AS d," -- 13
+                            <> "TIME '12:34:56' AS t," -- 14
+                            <> "TIMESTAMP '2021-07-20 12:34:56' AS ts," -- 15
+                            <> "INTERVAL '1 day 2 hours' AS iv," -- 16
+                            <> "('12345.67')::DECIMAL(18,2) AS dec," -- 17
+                            <> "'this string is definitely longer than inline storage'::VARCHAR AS txt," -- 18
+                            <> "CAST('abc' AS BLOB) AS blob_col," -- 19
+                            <> "NULL::INTEGER AS null_col" -- 20
+                withCString query \cQuery ->
+                    alloca \resPtr -> do
+                        state <- c_duckdb_query conn cQuery resPtr
+                        when (state /= DuckDBSuccess) $ do
+                            errPtr <- c_duckdb_result_error resPtr
+                            errMsg <- if errPtr == nullPtr then pure "<no error>" else peekCString errPtr
+                            assertFailure ("duckdb_query failed: " <> errMsg)
+                        state @?= DuckDBSuccess
+
+                        -- Boolean
+                        c_duckdb_value_is_null resPtr 0 0 >>= (@?= CBool 0)
+                        c_duckdb_value_boolean resPtr 0 0 >>= (@?= CBool 1)
+
+                        -- Signed integers
+                        c_duckdb_value_int8 resPtr 1 0 >>= (@?= (-5 :: Int8))
+                        c_duckdb_value_int16 resPtr 2 0 >>= (@?= (-300 :: Int16))
+                        c_duckdb_value_int32 resPtr 3 0 >>= (@?= (-4000000 :: Int32))
+                        c_duckdb_value_int64 resPtr 4 0 >>= (@?= (-5000000000000 :: Int64))
+
+                        -- HugeInt
+                        alloca \hugePtr -> do
+                            c_duckdb_value_hugeint resPtr 5 0 hugePtr
+                            DuckDBHugeInt{duckDBHugeIntLower = lowerHuge, duckDBHugeIntUpper = upperHuge} <- peek hugePtr
+                            lowerHuge @?= 9223372036854775809
+                            upperHuge @?= 0
+
+                        -- Unsigned integers
+                        c_duckdb_value_uint8 resPtr 6 0 >>= (@?= (200 :: Word8))
+                        c_duckdb_value_uint16 resPtr 7 0 >>= (@?= (60000 :: Word16))
+                        c_duckdb_value_uint32 resPtr 8 0 >>= (@?= (4000000000 :: Word32))
+                        c_duckdb_value_uint64 resPtr 9 0 >>= (@?= (maxBound :: Word64))
+
+                        alloca \uhugePtr -> do
+                            c_duckdb_value_uhugeint resPtr 10 0 uhugePtr
+                            DuckDBUHugeInt{duckDBUHugeIntLower = lowerUHuge, duckDBUHugeIntUpper = upperUHuge} <- peek uhugePtr
+                            lowerUHuge @?= 0
+                            upperUHuge @?= 1
+
+                        -- Floating point
+                        valFloat <- c_duckdb_value_float resPtr 11 0
+                        realToFrac valFloat @?= (1.5 :: Double)
+                        valDouble <- c_duckdb_value_double resPtr 12 0
+                        realToFrac valDouble @?= (2.5 :: Double)
+
+                        -- Date/time/timestamp
+                        DuckDBDate dateVal <- c_duckdb_value_date resPtr 13 0
+                        let expectedDaysInteger = diffDays targetDay epochDay
+                            expectedDays = fromIntegral expectedDaysInteger :: Int32
+                        dateVal @?= expectedDays
+
+                        DuckDBTime timeVal <- c_duckdb_value_time resPtr 14 0
+                        let expectedTimeMicros :: Int64
+                            expectedTimeMicros = ((12 * 60 + 34) * 60 + 56) * 1000000
+                        timeVal @?= expectedTimeMicros
+
+                        DuckDBTimestamp tsVal <- c_duckdb_value_timestamp resPtr 15 0
+                        let expectedTimestampMicros :: Int64
+                            expectedTimestampMicros =
+                                fromIntegral expectedDaysInteger * 86400000000 + expectedTimeMicros
+                        tsVal @?= expectedTimestampMicros
+
+                        -- Interval
+                        alloca \intervalPtr -> do
+                            c_duckdb_value_interval resPtr 16 0 intervalPtr
+                            DuckDBInterval{duckDBIntervalMonths = months, duckDBIntervalDays = days, duckDBIntervalMicros = micros} <- peek intervalPtr
+                            months @?= 0
+                            days @?= 1
+                            micros @?= 7200000000
+
+                        -- Decimal
+                        alloca \decimalPtr -> do
+                            c_duckdb_value_decimal resPtr 17 0 decimalPtr
+                            DuckDBDecimal{duckDBDecimalWidth = width, duckDBDecimalScale = scale, duckDBDecimalValue = decValue} <- peek decimalPtr
+                            width @?= 18
+                            scale @?= 2
+                            DuckDBHugeInt{duckDBHugeIntLower = decimalLower, duckDBHugeIntUpper = decimalUpper} <- pure decValue
+                            decimalLower @?= 1234567
+                            decimalUpper @?= 0
+
+                        -- Strings
+                        strVarchar <- c_duckdb_value_varchar resPtr 18 0
+                        varcharText <- peekCString strVarchar
+                        varcharText @?= stringLiteral
+                        c_duckdb_free (castPtr strVarchar)
+
+                        alloca \stringPtr -> do
+                            c_duckdb_value_string resPtr 18 0 stringPtr
+                            DuckDBString{duckDBStringData = datPtr, duckDBStringSize = datSize} <- peek stringPtr
+                            bytes <- peekCStringLen (datPtr, fromIntegral datSize)
+                            bytes @?= stringLiteral
+                            when (datPtr /= nullPtr) $ c_duckdb_free (castPtr datPtr)
+
+                        alloca \stringInternalPtr -> do
+                            c_duckdb_value_string_internal resPtr 18 0 stringInternalPtr
+                            DuckDBString{duckDBStringData = datPtr, duckDBStringSize = datSize} <- peek stringInternalPtr
+                            bytes <- peekCStringLen (datPtr, fromIntegral datSize)
+                            bytes @?= stringLiteral
+
+                        varcharInternal <- c_duckdb_value_varchar_internal resPtr 18 0
+                        peekCString varcharInternal >>= (@?= stringLiteral)
+
+                        -- Blob
+                        alloca \blobPtr -> do
+                            c_duckdb_value_blob resPtr 19 0 blobPtr
+                            DuckDBBlob{duckDBBlobData = blobData, duckDBBlobSize = blobSize} <- peek blobPtr
+                            let expectedBlob :: [Word8]
+                                expectedBlob = map (fromIntegral . fromEnum) ("abc" :: String)
+                            peekArray (fromIntegral blobSize) (castPtr blobData :: Ptr Word8) >>= (@?= expectedBlob)
+                            c_duckdb_free (castPtr blobData)
+
+                        -- Null handling
+                        c_duckdb_value_is_null resPtr 20 0 >>= (@?= CBool 1)
+
+                        c_duckdb_destroy_result resPtr
+  where
+    stringLiteral :: String
+    stringLiteral = "this string is definitely longer than inline storage"
+    targetDay = fromGregorian 2021 7 20
+    epochDay = fromGregorian 1970 1 1
diff --git a/test/ScalarFunctionsTest.hs b/test/ScalarFunctionsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ScalarFunctionsTest.hs
@@ -0,0 +1,228 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module ScalarFunctionsTest (tests) where
+
+import Control.Exception (bracket)
+import Control.Monad (forM_, unless, when)
+import Data.Char (toLower)
+import Data.Int (Int32)
+import Data.List (isInfixOf)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, withCString)
+import Foreign.Marshal.Alloc (alloca, free, mallocBytes)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek, peekElemOff, poke, pokeElemOff, sizeOf)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase, withResultCString)
+
+-- | Coverage of scalar function registration and bind/exec helpers.
+tests :: TestTree
+tests =
+    testGroup
+        "Scalar Functions"
+        [ scalarFunctionRoundtrip
+        , scalarFunctionSetFeatures
+        ]
+
+scalarFunctionRoundtrip :: TestTree
+scalarFunctionRoundtrip =
+    testCase "register custom scalar function and execute" $ do
+        withDatabase \db ->
+            withConnection db \conn -> do
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType -> do
+                    funPtr <- mkScalarFun negateCallback
+                    withScalarFunction \fun -> do
+                        withCString "negate_int" $ \name -> c_duckdb_scalar_function_set_name fun name
+                        c_duckdb_scalar_function_add_parameter fun intType
+                        c_duckdb_scalar_function_set_return_type fun intType
+                        c_duckdb_scalar_function_set_function fun funPtr
+
+                        c_duckdb_register_scalar_function conn fun >>= (@?= DuckDBSuccess)
+
+                        withCString "SELECT negate_int(5)" $ \sql ->
+                            withResultCString conn sql $ \resPtr ->
+                                c_duckdb_value_int32 resPtr 0 0 >>= (@?= (-5))
+
+-- Callback ------------------------------------------------------------------
+
+negateCallback :: DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
+negateCallback _ chunk outVec = do
+    rows <- c_duckdb_data_chunk_get_size chunk
+    inVec <- c_duckdb_data_chunk_get_vector chunk 0
+    inPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data inVec
+    outPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data outVec
+
+    forM_ [0 .. fromIntegral rows - 1] \idx -> do
+        val <- peekElemOff inPtr idx
+        pokeElemOff outPtr idx (negate val)
+
+    c_duckdb_data_chunk_set_size chunk rows
+
+varargBind :: Ptr () -> DuckDBDeleteCallback -> DuckDBCopyCallback -> Int -> DuckDBBindInfo -> IO ()
+varargBind extraPtr deleteBindCb copyCb bindDataSize info = do
+    extra <- c_duckdb_scalar_function_bind_get_extra_info info
+    assertBool "extra info propagated to bind phase" (extra == extraPtr)
+
+    argCount <- c_duckdb_scalar_function_bind_get_argument_count info
+
+    alloca \ctxPtr -> do
+        c_duckdb_scalar_function_get_client_context info ctxPtr
+        ctx <- peek ctxPtr
+        unless (ctx == nullPtr) $
+            c_duckdb_destroy_client_context ctxPtr
+
+    if argCount == 0
+        then withCString "at least one argument required" $ \msg ->
+            c_duckdb_scalar_function_bind_set_error info msg
+        else do
+            expr <- c_duckdb_scalar_function_bind_get_argument info 0
+            alloca \exprPtr -> poke exprPtr expr >> c_duckdb_destroy_expression exprPtr
+
+            bindStorage <- mallocBytes bindDataSize
+            poke (castPtr bindStorage :: Ptr Int32) (fromIntegral argCount)
+
+            c_duckdb_scalar_function_set_bind_data info bindStorage deleteBindCb
+            c_duckdb_scalar_function_set_bind_data_copy info copyCb
+
+varargExec :: Ptr () -> Int -> DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()
+varargExec extraPtr _ info chunk outVec = do
+    bindData <- c_duckdb_scalar_function_get_bind_data info
+    argCount <-
+        if bindData == nullPtr
+            then pure (0 :: Integer)
+            else fromIntegral <$> peek (castPtr bindData :: Ptr Int32)
+
+    extra <- c_duckdb_scalar_function_get_extra_info info
+    assertBool "extra info visible during execution" (extra == extraPtr)
+
+    rowCount <- fromIntegral <$> c_duckdb_data_chunk_get_size chunk
+    vectors <- mapM (c_duckdb_data_chunk_get_vector chunk . fromIntegral) [0 .. argCount - 1]
+    dataPtrs <- mapM (fmap castToInt32Ptr . c_duckdb_vector_get_data) vectors
+    outPtr <- castToInt32Ptr <$> c_duckdb_vector_get_data outVec
+
+    let loop idx
+            | idx >= rowCount = pure False
+            | otherwise = do
+                values <- mapM (`peekElemOff` idx) dataPtrs
+                let total = sum values
+                if total < 0
+                    then do
+                        withCString "negative sum not allowed" $ \msg ->
+                            c_duckdb_scalar_function_set_error info msg
+                        c_duckdb_data_chunk_set_size chunk 0
+                        pure True
+                    else do
+                        pokeElemOff outPtr idx total
+                        loop (idx + 1)
+
+    errored <- loop 0
+    unless errored $
+        c_duckdb_data_chunk_set_size chunk (fromIntegral rowCount)
+
+scalarFunctionSetFeatures :: TestTree
+scalarFunctionSetFeatures =
+    testCase "scalar function bind helpers and function sets" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType -> do
+                    let bindDataSize = sizeOf (undefined :: Int32)
+
+                    extraStorage <- mallocBytes bindDataSize
+                    poke (castPtr extraStorage) (123 :: Int32)
+                    deleteExtraCb <- mkDeleteCallback \ptr ->
+                        when (ptr /= nullPtr) (free ptr)
+
+                    deleteBindCb <- mkDeleteCallback \ptr ->
+                        when (ptr /= nullPtr) (free ptr)
+
+                    let copyBindData src
+                            | src == nullPtr = pure nullPtr
+                            | otherwise = do
+                                newPtr <- mallocBytes bindDataSize
+                                value <- peek (castPtr src :: Ptr Int32)
+                                poke (castPtr newPtr) value
+                                pure newPtr
+                    copyCb <- mkCopyCallback copyBindData
+
+                    bindFun <- mkScalarBindFun (varargBind (castPtr extraStorage) deleteBindCb copyCb bindDataSize)
+                    execFun <- mkScalarFun (varargExec (castPtr extraStorage) bindDataSize)
+
+                    let functionName = "haskell_vararg"
+
+                    withScalarFunction \fun -> do
+                        withCString functionName $ \cName ->
+                            c_duckdb_scalar_function_set_name fun cName
+                        c_duckdb_scalar_function_set_return_type fun intType
+                        c_duckdb_scalar_function_set_varargs fun intType
+                        c_duckdb_scalar_function_set_special_handling fun
+                        c_duckdb_scalar_function_set_volatile fun
+                        c_duckdb_scalar_function_set_extra_info fun (castPtr extraStorage) deleteExtraCb
+                        c_duckdb_scalar_function_set_bind fun bindFun
+                        c_duckdb_scalar_function_set_function fun execFun
+
+                        withScalarFunctionSet functionName \funSet -> do
+                            c_duckdb_add_scalar_function_to_set funSet fun >>= (@?= DuckDBSuccess)
+                            c_duckdb_register_scalar_function_set conn funSet >>= (@?= DuckDBSuccess)
+
+                            withCString "SELECT haskell_vararg(1, 2, 3)" \sql ->
+                                withResultCString conn sql \resPtr ->
+                                    c_duckdb_value_int32 resPtr 0 0 >>= (@?= 6)
+
+                            expectQueryError conn "SELECT haskell_vararg()" "at least one argument"
+                            expectQueryError conn "SELECT haskell_vararg(-5, 2)" "negative sum"
+
+                    pure ()
+
+-- Wrapper builder -----------------------------------------------------------
+
+foreign import ccall "wrapper"
+    mkScalarFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> DuckDBVector -> IO ()) -> IO DuckDBScalarFunctionFun
+
+foreign import ccall "wrapper"
+    mkScalarBindFun :: (DuckDBBindInfo -> IO ()) -> IO DuckDBScalarFunctionBindFun
+
+foreign import ccall "wrapper"
+    mkDeleteCallback :: (Ptr () -> IO ()) -> IO DuckDBDeleteCallback
+
+foreign import ccall "wrapper"
+    mkCopyCallback :: (Ptr () -> IO (Ptr ())) -> IO DuckDBCopyCallback
+
+-- Resource helpers ----------------------------------------------------------
+
+withLogicalType :: IO DuckDBLogicalType -> (DuckDBLogicalType -> IO a) -> IO a
+withLogicalType acquire = bracket acquire destroyLogicalType
+
+destroyLogicalType :: DuckDBLogicalType -> IO ()
+destroyLogicalType lt = alloca \ptr -> poke ptr lt >> c_duckdb_destroy_logical_type ptr
+
+withScalarFunction :: (DuckDBScalarFunction -> IO a) -> IO a
+withScalarFunction = bracket c_duckdb_create_scalar_function destroy
+  where
+    destroy fun = alloca \ptr -> poke ptr fun >> c_duckdb_destroy_scalar_function ptr
+
+withScalarFunctionSet :: String -> (DuckDBScalarFunctionSet -> IO a) -> IO a
+withScalarFunctionSet name action =
+    withCString name \cName ->
+        bracket (c_duckdb_create_scalar_function_set cName) destroy action
+  where
+    destroy set = alloca \ptr -> poke ptr set >> c_duckdb_destroy_scalar_function_set ptr
+
+expectQueryError :: DuckDBConnection -> String -> String -> IO ()
+expectQueryError conn sql expectedFragment =
+    withCString sql \sqlPtr ->
+        alloca \resPtr -> do
+            state <- c_duckdb_query conn sqlPtr resPtr
+            state @?= DuckDBError
+            errPtr <- c_duckdb_result_error resPtr
+            errMsg <- peekCString errPtr
+            let needle = map toLower expectedFragment
+                haystack = map toLower errMsg
+            assertBool
+                ("expected fragment \"" ++ expectedFragment ++ "\" in error message:\n" ++ errMsg)
+                (needle `isInfixOf` haystack)
+            c_duckdb_destroy_result resPtr
+
+castToInt32Ptr :: Ptr () -> Ptr Int32
+castToInt32Ptr = castPtr
diff --git a/test/SelectionVectorTest.hs b/test/SelectionVectorTest.hs
new file mode 100644
--- /dev/null
+++ b/test/SelectionVectorTest.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE BlockArguments #-}
+
+module SelectionVectorTest (tests) where
+
+import Control.Monad (forM_)
+import Data.Int (Int32)
+import Data.Word (Word32)
+import Database.DuckDB.FFI
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peekElemOff, pokeElemOff)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withLogicalType, withSelectionVector, withVector)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Selection Vector Interface"
+        [ selectionVectorPointerWritable
+        , selectionVectorCopySelection
+        ]
+
+selectionVectorPointerWritable :: TestTree
+selectionVectorPointerWritable =
+    testCase "selection vector exposes writable data pointer" $ do
+        withSelectionVector 4 \selVec -> do
+            dataPtr <- c_duckdb_selection_vector_get_data_ptr selVec
+            assertBool "data pointer should be non-null" (dataPtr /= nullPtr)
+            forM_ (zip [0 ..] [0, 2, 4, 6 :: Word32]) (uncurry (pokeElemOff dataPtr))
+            fetched <- mapM (peekElemOff dataPtr) [0 .. 3]
+            fetched @?= [0, 2, 4, 6]
+
+selectionVectorCopySelection :: TestTree
+selectionVectorCopySelection =
+    testCase "vector_copy_sel copies selected rows" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType -> do
+            withVector (c_duckdb_create_vector intType 4) \srcVec -> do
+                srcPtr <- vectorDataPtr srcVec
+                forM_ (zip [0 ..] [10, 20, 30, 40 :: Int32]) (uncurry (pokeElemOff srcPtr))
+
+                withSelectionVector 2 \selVec -> do
+                    selPtr <- c_duckdb_selection_vector_get_data_ptr selVec
+                    pokeElemOff selPtr 0 1
+                    pokeElemOff selPtr 1 3
+
+                    withVector (c_duckdb_create_vector intType 2) \dstVec -> do
+                        c_duckdb_vector_copy_sel srcVec dstVec selVec 2 0 0
+                        dstPtr <- vectorDataPtr dstVec
+                        val0 <- peekElemOff dstPtr 0
+                        val1 <- peekElemOff dstPtr 1
+                        val0 @?= 20
+                        val1 @?= 40
+
+-- Helpers -------------------------------------------------------------------
+
+vectorDataPtr :: DuckDBVector -> IO (Ptr Int32)
+vectorDataPtr vec = do
+    raw <- c_duckdb_vector_get_data vec
+    pure (castPtr raw)
diff --git a/test/StreamingResultTest.hs b/test/StreamingResultTest.hs
new file mode 100644
--- /dev/null
+++ b/test/StreamingResultTest.hs
@@ -0,0 +1,116 @@
+{-# LANGUAGE BlockArguments #-}
+
+module StreamingResultTest (tests) where
+
+import Control.Monad (void)
+import Data.Int (Int64)
+import Data.List (intercalate)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (withConnection, withDatabase, withResult)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Streaming Result Interface"
+        [ streamingFetchConsumesAllChunks
+        , materializedFetchChunkExhaustsResult
+        ]
+
+streamingFetchConsumesAllChunks :: TestTree
+streamingFetchConsumesAllChunks =
+    testCase "stream_fetch_chunk yields data until exhaustion" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupTable conn "streaming_table" 6
+
+                withCString "SELECT id FROM streaming_table ORDER BY id" \querySql ->
+                    alloca \stmtPtr -> do
+                        prepareOk <- c_duckdb_prepare conn querySql stmtPtr
+                        prepareOk @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        alloca \pendingPtr -> do
+                            stPending <- c_duckdb_pending_prepared_streaming stmt pendingPtr
+                            stPending @?= DuckDBSuccess
+                            pending <- peek pendingPtr
+                            assertBool "pending result should not be null" (pending /= nullPtr)
+
+                            void (c_duckdb_pending_execute_task pending)
+
+                            alloca \resPtr -> do
+                                execState <- c_duckdb_execute_pending pending resPtr
+                                execState @?= DuckDBSuccess
+
+                                streamingFlag <- c_duckdb_result_is_streaming resPtr
+                                streamingFlag @?= CBool 1
+
+                                totalRows <- consumeStreamingChunks resPtr 0
+                                totalRows @?= 6
+
+                                c_duckdb_destroy_result resPtr
+
+                            c_duckdb_destroy_pending pendingPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+materializedFetchChunkExhaustsResult :: TestTree
+materializedFetchChunkExhaustsResult =
+    testCase "fetch_chunk provides chunk and then null for materialized result" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupTable conn "materialized_table" 4
+
+                withResult conn "SELECT id FROM materialized_table ORDER BY id" \resPtr -> do
+                    chunk <- c_duckdb_fetch_chunk resPtr
+                    assertBool "first fetch_chunk should yield a chunk" (chunk /= nullPtr)
+
+                    chunkSize <- c_duckdb_data_chunk_get_size chunk
+                    assertBool "materialized chunk should have rows" (chunkSize > 0)
+
+                    destroyChunk chunk
+
+                    chunkNext <- c_duckdb_fetch_chunk resPtr
+                    chunkNext @?= nullPtr
+
+-- helpers ------------------------------------------------------------------
+
+consumeStreamingChunks :: Ptr DuckDBResult -> Int64 -> IO Int64
+consumeStreamingChunks resPtr acc = do
+    chunk <- c_duckdb_stream_fetch_chunk resPtr
+    if chunk == nullPtr
+        then pure acc
+        else do
+            chunkSize <- c_duckdb_data_chunk_get_size chunk
+            assertBool "streaming chunk should not be empty" (chunkSize > 0)
+            destroyChunk chunk
+            consumeStreamingChunks resPtr (acc + fromIntegral chunkSize)
+
+setupTable :: DuckDBConnection -> String -> Int -> IO ()
+setupTable conn tableName totalRows = do
+    withCString ("CREATE TABLE " <> tableName <> " (id INTEGER);") $ \createSql ->
+        execStatement conn createSql
+    let values = intercalate ", " ["(" <> show i <> ")" | i <- [1 .. totalRows]]
+        insertSql = "INSERT INTO " <> tableName <> " VALUES " <> values <> ";"
+    withCString insertSql $ \insertCStr ->
+        execStatement conn insertCStr
+
+execStatement :: DuckDBConnection -> CString -> IO ()
+execStatement conn sql =
+    alloca \resPtr -> do
+        st <- c_duckdb_query conn sql resPtr
+        st @?= DuckDBSuccess
+        c_duckdb_destroy_result resPtr
+
+destroyChunk :: DuckDBDataChunk -> IO ()
+destroyChunk chunk =
+    alloca \ptr -> do
+        poke ptr chunk
+        c_duckdb_destroy_data_chunk ptr
diff --git a/test/TableDescriptionTest.hs b/test/TableDescriptionTest.hs
new file mode 100644
--- /dev/null
+++ b/test/TableDescriptionTest.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE BlockArguments #-}
+
+module TableDescriptionTest (tests) where
+
+import Control.Exception (finally)
+import Control.Monad (when)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Table Description"
+        [ tableDescriptionLifecycle
+        , tableDescriptionExtended
+        , tableDescriptionErrorHandling
+        ]
+
+tableDescriptionLifecycle :: TestTree
+tableDescriptionLifecycle =
+    testCase "inspect column names and defaults for main schema tables" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement
+                    conn
+                    "CREATE TABLE description_demo(id INTEGER, name VARCHAR DEFAULT 'guest', active BOOLEAN DEFAULT TRUE)"
+
+                withTableDescription conn Nothing "description_demo" \desc -> do
+                    checkColumn desc 0 "id" False
+                    checkColumn desc 1 "name" True
+                    checkColumn desc 2 "active" True
+                    c_duckdb_table_description_error desc >>= (@?= nullPtr)
+
+tableDescriptionExtended :: TestTree
+tableDescriptionExtended =
+    testCase "describe table in custom schema via extended constructor" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                runStatement conn "CREATE SCHEMA custom_schema"
+                runStatement conn "CREATE TABLE custom_schema.ext_demo(id INTEGER DEFAULT 7, note VARCHAR)"
+
+                withTableDescriptionExt conn Nothing (Just "custom_schema") "ext_demo" \desc -> do
+                    checkColumn desc 0 "id" True
+                    checkColumn desc 1 "note" False
+                    c_duckdb_table_description_error desc >>= (@?= nullPtr)
+
+tableDescriptionErrorHandling :: TestTree
+tableDescriptionErrorHandling =
+    testCase "surface error information when describing missing tables" $
+        withDatabase \db ->
+            withConnection db \conn ->
+                withCString "missing_table" \tablePtr ->
+                    alloca \descPtr -> do
+                        state <- c_duckdb_table_description_create conn nullPtr tablePtr descPtr
+                        state @?= DuckDBError
+                        desc <- peek descPtr
+                        let cleanup = c_duckdb_table_description_destroy descPtr
+                        let action =
+                                if desc == nullPtr
+                                    then assertFailure "table description handle should be populated on error"
+                                    else do
+                                        errPtr <- c_duckdb_table_description_error desc
+                                        assertBool "error pointer should not be null" (errPtr /= nullPtr)
+                                        errMsg <- peekCString errPtr
+                                        assertBool "error message should not be empty" (not (null errMsg))
+                        action `finally` cleanup
+
+-- Helpers -------------------------------------------------------------------
+
+runStatement :: DuckDBConnection -> String -> IO ()
+runStatement conn sql =
+    withCString sql \sqlPtr ->
+        alloca \resPtr -> do
+            state <- c_duckdb_query conn sqlPtr resPtr
+            if state == DuckDBSuccess
+                then c_duckdb_destroy_result resPtr
+                else do
+                    errPtr <- c_duckdb_result_error resPtr
+                    errMsg <-
+                        if errPtr == nullPtr
+                            then pure "unknown error"
+                            else peekCString errPtr
+                    c_duckdb_destroy_result resPtr
+                    assertFailure ("duckdb_query failed: " <> errMsg)
+
+withTableDescription :: DuckDBConnection -> Maybe String -> String -> (DuckDBTableDescription -> IO a) -> IO a
+withTableDescription conn schema table action =
+    withMaybeCString schema \schemaPtr ->
+        withCString table \tablePtr ->
+            withDescriptionHandle (c_duckdb_table_description_create conn schemaPtr tablePtr) action
+
+withTableDescriptionExt :: DuckDBConnection -> Maybe String -> Maybe String -> String -> (DuckDBTableDescription -> IO a) -> IO a
+withTableDescriptionExt conn catalog schema table action =
+    withMaybeCString catalog \catalogPtr ->
+        withMaybeCString schema \schemaPtr ->
+            withCString table \tablePtr ->
+                withDescriptionHandle (c_duckdb_table_description_create_ext conn catalogPtr schemaPtr tablePtr) action
+
+withDescriptionHandle :: (Ptr DuckDBTableDescription -> IO DuckDBState) -> (DuckDBTableDescription -> IO a) -> IO a
+withDescriptionHandle acquire action =
+    alloca \descPtr -> do
+        state <- acquire descPtr
+        state @?= DuckDBSuccess
+        desc <- peek descPtr
+        when (desc == nullPtr) $
+            assertFailure "table description handle should not be null"
+        let cleanup = c_duckdb_table_description_destroy descPtr
+        action desc `finally` cleanup
+
+withMaybeCString :: Maybe String -> (CString -> IO a) -> IO a
+withMaybeCString Nothing action = action nullPtr
+withMaybeCString (Just txt) action = withCString txt action
+
+checkColumn :: DuckDBTableDescription -> DuckDBIdx -> String -> Bool -> IO ()
+checkColumn desc idx expectedName expectedDefault = do
+    hasDef <- columnHasDefault desc idx
+    hasDef @?= expectedDefault
+    name <- getColumnName desc idx
+    name @?= expectedName
+
+getColumnName :: DuckDBTableDescription -> DuckDBIdx -> IO String
+getColumnName desc idx = do
+    namePtr <- c_duckdb_table_description_get_column_name desc idx
+    assertBool "column name pointer should not be null" (namePtr /= nullPtr)
+    name <- peekCString namePtr
+    c_duckdb_free (castPtr namePtr)
+    pure name
+
+columnHasDefault :: DuckDBTableDescription -> DuckDBIdx -> IO Bool
+columnHasDefault desc idx =
+    alloca \outPtr -> do
+        state <- c_duckdb_column_has_default desc idx outPtr
+        state @?= DuckDBSuccess
+        CBool val <- peek outPtr
+        pure (val /= 0)
diff --git a/test/TableFunctionsTest.hs b/test/TableFunctionsTest.hs
new file mode 100644
--- /dev/null
+++ b/test/TableFunctionsTest.hs
@@ -0,0 +1,547 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module TableFunctionsTest (tests) where
+
+import Control.Concurrent (runInBoundThread)
+import Control.Exception (bracket)
+import Control.Monad (forM_, when)
+import Data.Char (isAsciiUpper)
+import Data.IORef (IORef, modifyIORef', newIORef, readIORef, writeIORef)
+import Data.Int (Int64)
+import Data.List (isInfixOf)
+import Data.Maybe (fromMaybe)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, peekCString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca, free, mallocBytes)
+import Foreign.Ptr (Ptr, castPtr, freeHaskellFunPtr, nullPtr)
+import Foreign.Storable (peek, peekElemOff, poke, pokeElemOff, sizeOf)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase, withLogicalType, withResultCString, withValue)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Table Functions"
+        [tableFunctionLifecycle]
+
+tableFunctionLifecycle :: TestTree
+tableFunctionLifecycle =
+    testCase "table function lifecycle covers core callbacks" $
+        runInBoundThread do
+            withDatabase \db ->
+                withLogicalType (c_duckdb_create_logical_type DuckDBTypeBigInt) \bigint ->
+                    withHarness bigint \harness@TableHarness{} -> do
+                        (bindError, execError, initError) <-
+                            withConnection db \conn ->
+                                withTableFunction \tableFun -> do
+                                    configureTableFunction tableFun harness
+                                    c_duckdb_register_table_function conn tableFun >>= (@?= DuckDBSuccess)
+
+                                    withCString "SELECT value, double_value FROM haskell_numbers(5, start => 2)" \sql ->
+                                        withResultCString conn sql \resPtr -> do
+                                            c_duckdb_column_count resPtr >>= (@?= 2)
+                                            c_duckdb_row_count resPtr >>= (@?= 5)
+                                            forM_ [0 .. 4] \idx -> do
+                                                c_duckdb_value_int64 resPtr 0 idx >>= (@?= 2 + fromIntegral idx)
+                                                c_duckdb_value_int64 resPtr 1 idx >>= (@?= 2 * (2 + fromIntegral idx))
+
+                                    withCString "SELECT value FROM haskell_numbers(3)" \sql ->
+                                        withResultCString conn sql \resPtr -> do
+                                            c_duckdb_column_count resPtr >>= (@?= 1)
+                                            forM_ (zip [0 ..] [0 .. 2]) \(idx, expected) ->
+                                                c_duckdb_value_int64 resPtr 0 idx >>= (@?= expected)
+
+                                    bindError <-
+                                        withCString "SELECT * FROM haskell_numbers(0)" \sql ->
+                                            alloca \resPtr -> do
+                                                state <- c_duckdb_query conn sql resPtr
+                                                errPtr <- c_duckdb_result_error resPtr
+                                                msg <- peekCStringMaybe errPtr
+                                                c_duckdb_destroy_result resPtr
+                                                pure (state, msg)
+
+                                    execError <-
+                                        withCString "SELECT * FROM haskell_numbers(5, fail_at => 3)" \sql ->
+                                            alloca \resPtr -> do
+                                                state <- c_duckdb_query conn sql resPtr
+                                                errPtr <- c_duckdb_result_error resPtr
+                                                msg <- peekCStringMaybe errPtr
+                                                c_duckdb_destroy_result resPtr
+                                                pure (state, msg)
+
+                                    initError <-
+                                        withCString "SELECT * FROM haskell_numbers(2, force_init_error => 1)" \sql ->
+                                            alloca \resPtr -> do
+                                                state <- c_duckdb_query conn sql resPtr
+                                                errPtr <- c_duckdb_result_error resPtr
+                                                msg <- peekCStringMaybe errPtr
+                                                c_duckdb_destroy_result resPtr
+                                                pure (state, msg)
+
+                                    pure (bindError, execError, initError)
+
+                        stats <- snapshotStats harness
+                        hsBindFreed stats @?= 3
+                        hsInitFreed stats @?= 3
+                        hsLocalFreed stats @?= 3
+                        assertBool "client context seen" (hsClientContextSeen stats)
+                        hsCounts stats @?= [5, 3, 0, 5, 2]
+                        hsProjectionHistory stats
+                            @?= [ ProjectionSnapshot 2 [0, 1]
+                                , ProjectionSnapshot 1 [0]
+                                , ProjectionSnapshot 2 [0, 1]
+                                ]
+                        fst bindError @?= DuckDBError
+                        assertBool "bind error mentions positivity" ("positive" `contains` snd bindError)
+                        fst execError @?= DuckDBError
+                        assertBool "execution error mentions table function" ("execution" `contains` snd execError)
+                        fst initError @?= DuckDBError
+                        assertBool "init error mentions initialization" ("init" `contains` snd initError)
+
+-- Harness --------------------------------------------------------------------
+
+data ProjectionSnapshot = ProjectionSnapshot
+    { psCount :: !Int
+    , psIndices :: ![Int]
+    }
+    deriving (Eq, Show)
+
+data ExecutionMode
+    = ModeUnset
+    | ModeNormal
+    | ModeInitError
+    | ModeRejected
+    deriving (Eq, Show)
+
+data TableHarness = TableHarness
+    { thColumnType :: !DuckDBLogicalType
+    , thExtraBuffer :: !(Ptr ())
+    , thExtraCount :: !(IORef Int)
+    , thBindCount :: !(IORef Int)
+    , thInitCount :: !(IORef Int)
+    , thLocalCount :: !(IORef Int)
+    , thBindPtr :: !(IORef (Maybe (Ptr ())))
+    , thInitPtr :: !(IORef (Maybe (Ptr ())))
+    , thLocalPtr :: !(IORef (Maybe (Ptr ())))
+    , thProjectionCurrent :: !(IORef (Maybe ProjectionSnapshot))
+    , thProjectionHistory :: !(IORef [ProjectionSnapshot])
+    , thClientContextSeen :: !(IORef Bool)
+    , thModeRef :: !(IORef ExecutionMode)
+    , thCountHistory :: !(IORef [Int64])
+    , thCallbacks :: !TableCallbacks
+    , thDeletes :: !DeleteCallbacks
+    }
+
+data TableCallbacks = TableCallbacks
+    { tcBind :: !DuckDBTableFunctionBindFun
+    , tcInit :: !DuckDBTableFunctionInitFun
+    , tcLocalInit :: !DuckDBTableFunctionInitFun
+    , tcExecute :: !DuckDBTableFunctionFun
+    }
+
+data DeleteCallbacks = DeleteCallbacks
+    { dcExtra :: !DuckDBDeleteCallback
+    , dcBind :: !DuckDBDeleteCallback
+    , dcInit :: !DuckDBDeleteCallback
+    , dcLocal :: !DuckDBDeleteCallback
+    }
+
+data HarnessStats = HarnessStats
+    { hsExtraFreed :: !Int
+    , hsBindFreed :: !Int
+    , hsInitFreed :: !Int
+    , hsLocalFreed :: !Int
+    , hsClientContextSeen :: !Bool
+    , hsProjectionHistory :: ![ProjectionSnapshot]
+    , hsCounts :: ![Int64]
+    }
+    deriving (Eq, Show)
+
+withHarness :: DuckDBLogicalType -> (TableHarness -> IO a) -> IO a
+withHarness columnType action = do
+    rawExtraBuffer <- mallocBytes sizeOfInt64
+    let extraBuffer = castPtr rawExtraBuffer :: Ptr ()
+    poke (castPtr extraBuffer :: Ptr Int64) extraInfoSentinel
+    extraCount <- newIORef 0
+    bindCount <- newIORef 0
+    initCount <- newIORef 0
+    localCount <- newIORef 0
+    bindPtrRef <- newIORef Nothing
+    initPtrRef <- newIORef Nothing
+    localPtrRef <- newIORef Nothing
+    projectionCurrent <- newIORef Nothing
+    projectionHistory <- newIORef []
+    clientContextSeen <- newIORef False
+    modeRef <- newIORef ModeUnset
+    countHistory <- newIORef []
+
+    let takeInt64 ptr = peekElemOff (castPtr ptr :: Ptr Int64)
+        putInt64 ptr = pokeElemOff (castPtr ptr :: Ptr Int64)
+
+    extraDelete <-
+        mkDeleteCallback \ptr -> do
+            modifyIORef' extraCount (+ 1)
+            when (ptr /= nullPtr) (free ptr)
+
+    bindDelete <-
+        mkDeleteCallback \ptr -> do
+            modifyIORef' bindCount (+ 1)
+            writeIORef bindPtrRef Nothing
+            when (ptr /= nullPtr) (free ptr)
+
+    initDelete <-
+        mkDeleteCallback \ptr -> do
+            modifyIORef' initCount (+ 1)
+            writeIORef initPtrRef Nothing
+            when (ptr /= nullPtr) (free ptr)
+
+    localDelete <-
+        mkDeleteCallback \ptr -> do
+            modifyIORef' localCount (+ 1)
+            writeIORef localPtrRef Nothing
+            when (ptr /= nullPtr) (free ptr)
+
+    let resetLifecycle = do
+            writeIORef projectionCurrent Nothing
+            writeIORef modeRef ModeUnset
+            writeIORef bindPtrRef Nothing
+            writeIORef initPtrRef Nothing
+            writeIORef localPtrRef Nothing
+
+        recordProjection snapshot = do
+            writeIORef projectionCurrent (Just snapshot)
+            modifyIORef' projectionHistory (<> [snapshot])
+
+        bindCallback info = do
+            resetLifecycle
+            writeIORef modeRef ModeRejected
+            extraPtr <- c_duckdb_bind_get_extra_info info
+            extraPtr @?= extraBuffer
+            sentinel <- peek (castPtr extraPtr :: Ptr Int64)
+            sentinel @?= extraInfoSentinel
+
+            alloca \ctxPtr -> do
+                poke ctxPtr nullPtr
+                c_duckdb_table_function_get_client_context info ctxPtr
+                ctx <- peek ctxPtr
+                assertBool "client context should not be null" (ctx /= nullPtr)
+                cid <- c_duckdb_client_context_get_connection_id ctx
+                assertBool "connection id should be non-negative" (cid >= 0)
+                c_duckdb_destroy_client_context ctxPtr
+                writeIORef clientContextSeen True
+
+            paramCount <- c_duckdb_bind_get_parameter_count info
+            paramCount @?= 1
+
+            withValue (c_duckdb_bind_get_parameter info 0) \value -> do
+                countValue <- c_duckdb_get_int64 value
+                modifyIORef' countHistory (<> [countValue])
+                if countValue <= 0
+                    then withCString "count must be positive" $ \msg -> c_duckdb_bind_set_error info msg
+                    else do
+                        startValue <-
+                            withCString "start" \name ->
+                                fmap (fromMaybe 0) (withOptionalValue (c_duckdb_bind_get_named_parameter info name) c_duckdb_get_int64)
+                        failAtValue <-
+                            withCString "fail_at" \name ->
+                                fmap (fromMaybe (-1)) (withOptionalValue (c_duckdb_bind_get_named_parameter info name) c_duckdb_get_int64)
+                        initErrValue <-
+                            withCString "force_init_error" \name ->
+                                fmap (fromMaybe 0) (withOptionalValue (c_duckdb_bind_get_named_parameter info name) c_duckdb_get_int64)
+                        absentValue <-
+                            withCString "missing" \name ->
+                                withOptionalValue (c_duckdb_bind_get_named_parameter info name) c_duckdb_get_int64
+                        absentValue @?= Nothing
+
+                        withCString "value" \col -> c_duckdb_bind_add_result_column info col columnType
+                        withCString "double_value" \col -> c_duckdb_bind_add_result_column info col columnType
+                        c_duckdb_bind_set_cardinality info (fromIntegral countValue) (toCBool True)
+
+                        if initErrValue /= 0
+                            then do
+                                writeIORef modeRef ModeInitError
+                                writeIORef bindPtrRef Nothing
+                            else do
+                                cfgPtr <- mallocBytes (3 * sizeOfInt64)
+                                putInt64 cfgPtr 0 startValue
+                                putInt64 cfgPtr 1 countValue
+                                putInt64 cfgPtr 2 failAtValue
+                                c_duckdb_bind_set_bind_data info (castPtr cfgPtr) bindDelete
+                                writeIORef bindPtrRef (Just (castPtr cfgPtr))
+                                writeIORef modeRef ModeNormal
+
+        initCallback info = do
+            extraPtr <- c_duckdb_init_get_extra_info info
+            extraPtr @?= extraBuffer
+            mode <- readIORef modeRef
+            case mode of
+                ModeInitError -> do
+                    bindRaw <- c_duckdb_init_get_bind_data info
+                    bindRaw @?= nullPtr
+                    withCString "init rejected missing bind data" $ \msg -> c_duckdb_init_set_error info msg
+                ModeNormal -> do
+                    bindRaw <- c_duckdb_init_get_bind_data info
+                    stored <- readIORef bindPtrRef
+                    case stored of
+                        Nothing -> assertFailure "bind data expected during init"
+                        Just expected -> bindRaw @?= expected
+
+                    startValue <- takeInt64 bindRaw 0
+                    countValue <- takeInt64 bindRaw 1
+                    failAtValue <- takeInt64 bindRaw 2
+
+                    columnCount <- fromIntegral <$> c_duckdb_init_get_column_count info
+                    indices <-
+                        mapM
+                            (fmap (fromIntegral :: DuckDBIdx -> Int) . c_duckdb_init_get_column_index info . fromIntegral)
+                            [0 .. columnCount - 1]
+                    recordProjection ProjectionSnapshot{psCount = columnCount, psIndices = indices}
+
+                    statePtr <- mallocBytes (3 * sizeOfInt64)
+                    putInt64 statePtr 0 startValue
+                    putInt64 statePtr 1 (startValue + countValue)
+                    putInt64 statePtr 2 failAtValue
+                    c_duckdb_init_set_init_data info (castPtr statePtr) initDelete
+                    writeIORef initPtrRef (Just (castPtr statePtr))
+                    c_duckdb_init_set_max_threads info 1
+                _ -> pure ()
+
+        localInitCallback info = do
+            extraPtr <- c_duckdb_init_get_extra_info info
+            extraPtr @?= extraBuffer
+            mode <- readIORef modeRef
+            case mode of
+                ModeNormal -> do
+                    bindRaw <- c_duckdb_init_get_bind_data info
+                    stored <- readIORef bindPtrRef
+                    case stored of
+                        Nothing -> assertFailure "bind data should be present during local init"
+                        Just expected -> bindRaw @?= expected
+                    localPtr <- mallocBytes sizeOfInt64
+                    poke (castPtr localPtr :: Ptr Int64) localInitSentinel
+                    c_duckdb_init_set_init_data info (castPtr localPtr) localDelete
+                    writeIORef localPtrRef (Just (castPtr localPtr))
+                ModeInitError -> withCString "local init skipped after init error" $ \msg -> c_duckdb_init_set_error info msg
+                _ -> pure ()
+
+        executeCallback info chunk = do
+            extraPtr <- c_duckdb_function_get_extra_info info
+            extraPtr @?= extraBuffer
+
+            mode <- readIORef modeRef
+            case mode of
+                ModeNormal -> do
+                    bindRaw <- c_duckdb_function_get_bind_data info
+                    storedBind <- readIORef bindPtrRef
+                    case storedBind of
+                        Nothing -> assertFailure "bind data should be recorded before execution"
+                        Just expected -> bindRaw @?= expected
+
+                    initRaw <- c_duckdb_function_get_init_data info
+                    storedInit <- readIORef initPtrRef
+                    case storedInit of
+                        Nothing -> assertFailure "init data should exist during execution"
+                        Just expected -> initRaw @?= expected
+
+                    localRaw <- c_duckdb_function_get_local_init_data info
+                    storedLocal <- readIORef localPtrRef
+                    case storedLocal of
+                        Nothing -> assertFailure "local init data should exist during execution"
+                        Just expected -> localRaw @?= expected
+                    sentinel <- peek (castPtr localRaw :: Ptr Int64)
+                    sentinel @?= localInitSentinel
+
+                    snapshot <- readIORef projectionCurrent
+                    ProjectionSnapshot{..} <-
+                        case snapshot of
+                            Nothing -> do
+                                _ <- assertFailure "projection snapshot missing during execution"
+                                pure (ProjectionSnapshot 0 [])
+                            Just snap -> pure snap
+                    assertBool "projection slots match indices" (length psIndices == psCount)
+
+                    nextVal <- takeInt64 initRaw 0
+                    endVal <- takeInt64 initRaw 1
+                    failAtValue <- takeInt64 initRaw 2
+                    if nextVal >= endVal
+                        then c_duckdb_data_chunk_set_size chunk 0
+                        else do
+                            let remaining = endVal - nextVal
+                                batch = min remaining 1024
+                                chunkEnd = nextVal + batch
+                            if failAtValue >= nextVal && failAtValue < chunkEnd && failAtValue >= 0
+                                then do
+                                    withCString "execution aborted by table function" $ \msg -> c_duckdb_function_set_error info msg
+                                    c_duckdb_data_chunk_set_size chunk 0
+                                    putInt64 initRaw 0 chunkEnd
+                                else do
+                                    forM_ (zip [0 :: Integer ..] psIndices) \(slot, schemaIdx) -> do
+                                        vec <- c_duckdb_data_chunk_get_vector chunk (fromIntegral slot)
+                                        dataPtr <- vectorDataPtr vec
+                                        forM_ [0 :: Integer .. fromIntegral batch - 1] \offset -> do
+                                            let base = nextVal + fromIntegral offset
+                                                val =
+                                                    case schemaIdx of
+                                                        0 -> base
+                                                        1 -> 2 * base
+                                                        _ -> 0
+                                            pokeElemOff dataPtr (fromIntegral offset) val
+                                    c_duckdb_data_chunk_set_size chunk (fromIntegral batch)
+                                    putInt64 initRaw 0 chunkEnd
+                ModeInitError -> do
+                    withCString "execution blocked by init error" $ \msg -> c_duckdb_function_set_error info msg
+                    c_duckdb_data_chunk_set_size chunk 0
+                ModeRejected -> do
+                    withCString "execution not expected after bind error" $ \msg -> c_duckdb_function_set_error info msg
+                    c_duckdb_data_chunk_set_size chunk 0
+                ModeUnset -> do
+                    withCString "execution invoked without prior bind" $ \msg -> c_duckdb_function_set_error info msg
+                    c_duckdb_data_chunk_set_size chunk 0
+
+    bindFun <- mkBindFun bindCallback
+    initFun <- mkInitFun initCallback
+    localInitFun <- mkInitFun localInitCallback
+    execFun <- mkFunctionFun executeCallback
+
+    let callbacks =
+            TableCallbacks
+                { tcBind = bindFun
+                , tcInit = initFun
+                , tcLocalInit = localInitFun
+                , tcExecute = execFun
+                }
+        deletes =
+            DeleteCallbacks
+                { dcExtra = extraDelete
+                , dcBind = bindDelete
+                , dcInit = initDelete
+                , dcLocal = localDelete
+                }
+
+    let harness =
+            TableHarness
+                { thColumnType = columnType
+                , thExtraBuffer = extraBuffer
+                , thExtraCount = extraCount
+                , thBindCount = bindCount
+                , thInitCount = initCount
+                , thLocalCount = localCount
+                , thBindPtr = bindPtrRef
+                , thInitPtr = initPtrRef
+                , thLocalPtr = localPtrRef
+                , thProjectionCurrent = projectionCurrent
+                , thProjectionHistory = projectionHistory
+                , thClientContextSeen = clientContextSeen
+                , thModeRef = modeRef
+                , thCountHistory = countHistory
+                , thCallbacks = callbacks
+                , thDeletes = deletes
+                }
+
+    result <- action harness
+
+    freeHaskellFunPtr bindFun
+    freeHaskellFunPtr initFun
+    freeHaskellFunPtr localInitFun
+    freeHaskellFunPtr execFun
+    -- DuckDB may still invoke delete callbacks while the table function tears down.
+    -- Rely on process teardown to reclaim them to avoid racing with finalizers.
+
+    pure result
+
+withTableFunction :: (DuckDBTableFunction -> IO a) -> IO a
+withTableFunction = bracket c_duckdb_create_table_function destroy
+  where
+    destroy fun = alloca \ptr -> poke ptr fun >> c_duckdb_destroy_table_function ptr
+
+configureTableFunction :: DuckDBTableFunction -> TableHarness -> IO ()
+configureTableFunction fun TableHarness{..} = do
+    withCString "haskell_numbers" $ \name -> c_duckdb_table_function_set_name fun name
+    c_duckdb_table_function_add_parameter fun thColumnType
+    withCString "start" \n -> c_duckdb_table_function_add_named_parameter fun n thColumnType
+    withCString "fail_at" \n -> c_duckdb_table_function_add_named_parameter fun n thColumnType
+    withCString "force_init_error" \n -> c_duckdb_table_function_add_named_parameter fun n thColumnType
+    -- NOTE: DuckDB expects the extra-info pointer to be populated alongside the other callbacks;
+    -- leaving it unset would skip the destructor and break the lifetime assertions below.
+    c_duckdb_table_function_set_extra_info fun thExtraBuffer (dcExtra thDeletes)
+    c_duckdb_table_function_set_bind fun (tcBind thCallbacks)
+    c_duckdb_table_function_set_init fun (tcInit thCallbacks)
+    c_duckdb_table_function_set_local_init fun (tcLocalInit thCallbacks)
+    c_duckdb_table_function_set_function fun (tcExecute thCallbacks)
+    c_duckdb_table_function_supports_projection_pushdown fun (toCBool True)
+
+snapshotStats :: TableHarness -> IO HarnessStats
+snapshotStats TableHarness{..} = do
+    hsExtraFreed <- readIORef thExtraCount
+    hsBindFreed <- readIORef thBindCount
+    hsInitFreed <- readIORef thInitCount
+    hsLocalFreed <- readIORef thLocalCount
+    hsClientContextSeen <- readIORef thClientContextSeen
+    history <- readIORef thProjectionHistory
+    hsCounts <- readIORef thCountHistory
+    pure
+        HarnessStats
+            { hsProjectionHistory = history
+            , ..
+            }
+
+-- Helpers --------------------------------------------------------------------
+
+extraInfoSentinel :: Int64
+extraInfoSentinel = 0xdeadbeef42
+
+localInitSentinel :: Int64
+localInitSentinel = 0x105105105
+
+sizeOfInt64 :: Int
+sizeOfInt64 = sizeOf (undefined :: Int64)
+
+withOptionalValue :: IO DuckDBValue -> (DuckDBValue -> IO a) -> IO (Maybe a)
+withOptionalValue acquire action = do
+    value <- acquire
+    if value == nullPtr
+        then pure Nothing
+        else do
+            result <- action value
+            destroy value
+            pure (Just result)
+  where
+    destroy val = alloca \ptr -> poke ptr val >> c_duckdb_destroy_value ptr
+
+vectorDataPtr :: DuckDBVector -> IO (Ptr Int64)
+vectorDataPtr vec = castPtr <$> c_duckdb_vector_get_data vec
+
+contains :: String -> String -> Bool
+contains needle haystack = mapLower needle `isInfixOf` mapLower haystack
+  where
+    mapLower = map toLowerAscii
+    toLowerAscii c
+        | isAsciiUpper c = toEnum (fromEnum c + 32)
+        | otherwise = c
+
+toCBool :: Bool -> CBool
+toCBool False = CBool 0
+toCBool True = CBool 1
+
+peekCStringMaybe :: CString -> IO String
+peekCStringMaybe ptr
+    | ptr == nullPtr = pure ""
+    | otherwise = peekCString ptr
+
+-- Wrappers -------------------------------------------------------------------
+
+foreign import ccall safe "wrapper"
+    mkBindFun :: (DuckDBBindInfo -> IO ()) -> IO DuckDBTableFunctionBindFun
+
+foreign import ccall safe "wrapper"
+    mkInitFun :: (DuckDBInitInfo -> IO ()) -> IO DuckDBTableFunctionInitFun
+
+foreign import ccall safe "wrapper"
+    mkFunctionFun :: (DuckDBFunctionInfo -> DuckDBDataChunk -> IO ()) -> IO DuckDBTableFunctionFun
+
+foreign import ccall safe "wrapper"
+    mkDeleteCallback :: (Ptr () -> IO ()) -> IO DuckDBDeleteCallback
diff --git a/test/ThreadingTest.hs b/test/ThreadingTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ThreadingTest.hs
@@ -0,0 +1,116 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ThreadingTest (tests) where
+
+import Control.Concurrent (forkFinally, newEmptyMVar, putMVar, takeMVar)
+import Data.Int (Int64)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, withCString)
+import Foreign.C.Types (CBool (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (nullPtr)
+import Foreign.Storable (peek)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
+import Utils (withConnection, withDatabase)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Threading Information"
+        [ executeTasksCompletesPendingQuery
+        , taskStateControlsExecutionLifecycle
+        ]
+
+executeTasksCompletesPendingQuery :: TestTree
+executeTasksCompletesPendingQuery =
+    testCase "execute_tasks drives pending query to completion" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupAggTable conn
+
+                withCString "SELECT SUM(val) FROM threading_numbers;" \querySql ->
+                    alloca \stmtPtr -> do
+                        stPrepare <- c_duckdb_prepare conn querySql stmtPtr
+                        stPrepare @?= DuckDBSuccess
+                        stmt <- peek stmtPtr
+                        assertBool "prepared statement should not be null" (stmt /= nullPtr)
+
+                        alloca \pendingPtr -> do
+                            stPending <- c_duckdb_pending_prepared stmt pendingPtr
+                            stPending @?= DuckDBSuccess
+                            pending <- peek pendingPtr
+                            assertBool "pending result should not be null" (pending /= nullPtr)
+
+                            finishedBefore <- c_duckdb_execution_is_finished conn
+                            finishedBefore @?= CBool 0
+
+                            driveTasks db conn
+
+                            finishedAfter <- c_duckdb_execution_is_finished conn
+                            finishedAfter @?= CBool 1
+
+                            alloca \resPtr -> do
+                                stExec <- c_duckdb_execute_pending pending resPtr
+                                stExec @?= DuckDBSuccess
+                                sumVal <- c_duckdb_value_int64 resPtr 0 0
+                                (sumVal :: Int64) @?= 15
+                                c_duckdb_destroy_result resPtr
+
+                            c_duckdb_destroy_pending pendingPtr
+
+                        c_duckdb_destroy_prepare stmtPtr
+
+taskStateControlsExecutionLifecycle :: TestTree
+taskStateControlsExecutionLifecycle =
+    testCase "task state executes batches and finishes on request" $
+        withDatabase \db ->
+            withConnection db \conn -> do
+                setupAggTable conn
+
+                taskState <- c_duckdb_create_task_state db
+                assertBool "task state should not be null" (taskState /= nullPtr)
+
+                doneVar <- newEmptyMVar
+                _ <- forkFinally (c_duckdb_execute_tasks_state taskState) (const (putMVar doneVar ()))
+
+                executed <- c_duckdb_execute_n_tasks_state taskState 0
+                assertBool "execute_n_tasks_state should not report negative work" (executed >= 0)
+
+                isFinishedBefore <- c_duckdb_task_state_is_finished taskState
+                isFinishedBefore @?= CBool 0
+
+                c_duckdb_finish_execution taskState
+
+                isFinishedAfter <- c_duckdb_task_state_is_finished taskState
+                isFinishedAfter @?= CBool 1
+
+                takeMVar doneVar
+                c_duckdb_destroy_task_state taskState
+
+setupAggTable :: DuckDBConnection -> IO ()
+setupAggTable conn = do
+    withCString "CREATE TABLE threading_numbers(val INTEGER);" $ \createSql ->
+        execStatement conn createSql
+    withCString "INSERT INTO threading_numbers VALUES (1), (2), (3), (4), (5);" $ \insertSql ->
+        execStatement conn insertSql
+
+driveTasks :: DuckDBDatabase -> DuckDBConnection -> IO ()
+driveTasks db conn = go 0
+  where
+    go :: Int -> IO ()
+    go attempts
+        | attempts > 10 = assertFailure "execute_tasks did not finish query within expected iterations"
+        | otherwise = do
+            c_duckdb_execute_tasks db 1000
+            finished <- c_duckdb_execution_is_finished conn
+            if finished == CBool 1
+                then pure ()
+                else go (attempts + 1)
+
+execStatement :: DuckDBConnection -> CString -> IO ()
+execStatement conn sql =
+    alloca \resPtr -> do
+        st <- c_duckdb_query conn sql resPtr
+        st @?= DuckDBSuccess
+        c_duckdb_destroy_result resPtr
diff --git a/test/Utils.hs b/test/Utils.hs
new file mode 100644
--- /dev/null
+++ b/test/Utils.hs
@@ -0,0 +1,125 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Utils (
+    withDatabase,
+    withConnection,
+    withResult,
+    withResultCString,
+    withValue,
+    withDuckValue,
+    destroyDuckValue,
+    withLogicalType,
+    destroyLogicalType,
+    withSelectionVector,
+    withScalarFunction,
+    withVector,
+    withVectorOfType,
+    setAllValid,
+    clearValidityBit,
+    plusWord,
+    destroyErrorData,
+) where
+
+import Control.Exception (bracket)
+import Control.Monad (forM_)
+import Data.Bits (clearBit, setBit)
+import Data.Word (Word64)
+import Database.DuckDB.FFI
+import Foreign.C.String (CString, withCString)
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (Ptr, plusPtr)
+import Foreign.Storable (peek, poke, sizeOf)
+import Test.Tasty.HUnit ((@?=))
+
+withDatabase :: (DuckDBDatabase -> IO a) -> IO a
+withDatabase action =
+    withCString ":memory:" \path ->
+        alloca \dbPtr -> do
+            c_duckdb_open path dbPtr >>= (@?= DuckDBSuccess)
+            db <- peek dbPtr
+            result <- action db
+            c_duckdb_close dbPtr
+            pure result
+
+withConnection :: DuckDBDatabase -> (DuckDBConnection -> IO a) -> IO a
+withConnection db = bracket acquire release
+  where
+    acquire =
+        alloca \connPtr -> do
+            c_duckdb_connect db connPtr >>= (@?= DuckDBSuccess)
+            peek connPtr
+    release conn =
+        alloca \connPtr -> do
+            poke connPtr conn
+            c_duckdb_disconnect connPtr
+
+withResult :: DuckDBConnection -> String -> (Ptr DuckDBResult -> IO a) -> IO a
+withResult conn sql action =
+    withCString sql \sqlPtr -> withResultCString conn sqlPtr action
+
+withResultCString :: DuckDBConnection -> CString -> (Ptr DuckDBResult -> IO a) -> IO a
+withResultCString conn sql action =
+    alloca \resPtr -> do
+        c_duckdb_query conn sql resPtr >>= (@?= DuckDBSuccess)
+        result <- action resPtr
+        c_duckdb_destroy_result resPtr
+        pure result
+
+withValue :: IO DuckDBValue -> (DuckDBValue -> IO a) -> IO a
+withValue acquire = bracket acquire destroyDuckValue
+
+withDuckValue :: IO DuckDBValue -> (DuckDBValue -> IO a) -> IO a
+withDuckValue = withValue
+
+destroyDuckValue :: DuckDBValue -> IO ()
+destroyDuckValue value =
+    alloca \ptr -> poke ptr value >> c_duckdb_destroy_value ptr
+
+withLogicalType :: IO DuckDBLogicalType -> (DuckDBLogicalType -> IO a) -> IO a
+withLogicalType acquire = bracket acquire destroyLogicalType
+
+destroyLogicalType :: DuckDBLogicalType -> IO ()
+destroyLogicalType lt =
+    alloca \ptr -> poke ptr lt >> c_duckdb_destroy_logical_type ptr
+
+withSelectionVector :: DuckDBIdx -> (DuckDBSelectionVector -> IO a) -> IO a
+withSelectionVector n = bracket (c_duckdb_create_selection_vector n) c_duckdb_destroy_selection_vector
+
+withScalarFunction :: (DuckDBScalarFunction -> IO a) -> IO a
+withScalarFunction = bracket c_duckdb_create_scalar_function destroy
+  where
+    destroy fun =
+        alloca \ptr -> poke ptr fun >> c_duckdb_destroy_scalar_function ptr
+
+withVector :: IO DuckDBVector -> (DuckDBVector -> IO a) -> IO a
+withVector acquire = bracket acquire destroyVector
+  where
+    destroyVector vec =
+        alloca \ptr -> poke ptr vec >> c_duckdb_destroy_vector ptr
+
+withVectorOfType :: DuckDBLogicalType -> DuckDBIdx -> (DuckDBVector -> IO a) -> IO a
+withVectorOfType lt capacity = withVector (c_duckdb_create_vector lt capacity)
+
+setAllValid :: Ptr Word64 -> Int -> IO ()
+setAllValid mask count =
+    let totalWords = max 1 ((count + 63) `div` 64)
+     in forM_ [0 .. totalWords - 1] \wordIdx -> do
+            let start = wordIdx * 64
+                end = min count (start + 64)
+                bits = foldl setBit 0 [0 .. end - start - 1]
+            poke (mask `plusWord` wordIdx) bits
+
+clearValidityBit :: Ptr Word64 -> Int -> IO ()
+clearValidityBit mask idx = do
+    let wordIdx = idx `div` 64
+        bitIdx = idx `mod` 64
+        entryPtr = mask `plusWord` wordIdx
+    current <- peek entryPtr
+    poke entryPtr (clearBit current bitIdx)
+
+plusWord :: Ptr Word64 -> Int -> Ptr Word64
+plusWord base idx = base `plusPtr` (idx * sizeOf (undefined :: Word64))
+
+destroyErrorData :: DuckDBErrorData -> IO ()
+destroyErrorData errData =
+    alloca \ptr -> poke ptr errData >> c_duckdb_destroy_error_data ptr
diff --git a/test/ValidityMaskTest.hs b/test/ValidityMaskTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ValidityMaskTest.hs
@@ -0,0 +1,61 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ValidityMaskTest (tests) where
+
+import Control.Monad (void)
+import Database.DuckDB.FFI
+import Foreign.C.Types (CBool (..))
+import Foreign.Ptr (nullPtr)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (setAllValid, withLogicalType, withVector)
+
+-- | Entry point for validity-mask focused tests.
+tests :: TestTree
+tests =
+    testGroup
+        "Validity Mask Functions"
+        [ validityRowHelpers
+        , validitySetOperations
+        ]
+
+validityRowHelpers :: TestTree
+validityRowHelpers =
+    testCase "row validity helpers reflect changes" $ do
+        withIntegerVector 4 \vec -> do
+            void (c_duckdb_vector_ensure_validity_writable vec)
+            mask <- c_duckdb_vector_get_validity vec
+            assertBool "validity pointer should not be null" (mask /= nullPtr)
+            setAllValid mask 4
+
+            toBool (c_duckdb_validity_row_is_valid mask 2) >>= (@?= True)
+            c_duckdb_validity_set_row_invalid mask 2
+            toBool (c_duckdb_validity_row_is_valid mask 2) >>= (@?= False)
+            c_duckdb_validity_set_row_valid mask 2
+            toBool (c_duckdb_validity_row_is_valid mask 2) >>= (@?= True)
+
+validitySetOperations :: TestTree
+validitySetOperations =
+    testCase "set_row_validity toggles state based on CBool" $ do
+        withIntegerVector 3 \vec -> do
+            void (c_duckdb_vector_ensure_validity_writable vec)
+            mask <- c_duckdb_vector_get_validity vec
+            setAllValid mask 3
+
+            c_duckdb_validity_set_row_validity mask 1 (CBool 0)
+            toBool (c_duckdb_validity_row_is_valid mask 1) >>= (@?= False)
+
+            c_duckdb_validity_set_row_validity mask 1 (CBool 1)
+            toBool (c_duckdb_validity_row_is_valid mask 1) >>= (@?= True)
+
+-- Helpers -------------------------------------------------------------------
+
+withIntegerVector :: DuckDBIdx -> (DuckDBVector -> IO a) -> IO a
+withIntegerVector capacity action =
+    withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+        withVector (c_duckdb_create_vector intType capacity) action
+
+toBool :: IO CBool -> IO Bool
+toBool action = do
+    CBool v <- action
+    pure (v /= 0)
diff --git a/test/ValueInterfaceTest.hs b/test/ValueInterfaceTest.hs
new file mode 100644
--- /dev/null
+++ b/test/ValueInterfaceTest.hs
@@ -0,0 +1,321 @@
+{-# LANGUAGE BlockArguments #-}
+
+module ValueInterfaceTest (tests) where
+
+import Control.Monad (when, (>=>))
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.Word (Word16, Word32, Word64, Word8)
+import Database.DuckDB.FFI
+import Foreign.C.String (peekCString, peekCStringLen, withCString)
+import Foreign.C.Types (CBool (..), CDouble (..), CFloat (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Marshal.Array (peekArray, withArray)
+import Foreign.Marshal.Utils (with, withMany)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.Storable (peek, poke)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (testCase, (@?=))
+import Utils (destroyDuckValue, destroyLogicalType, withDuckValue)
+
+tests :: TestTree
+tests =
+    testGroup
+        "Value Interface"
+        [ scalarCreatesRoundTrip
+        , valueTypeReportsLogicalType
+        , collectionValuesRoundTrip
+        ]
+
+scalarCreatesRoundTrip :: TestTree
+scalarCreatesRoundTrip =
+    testCase "scalar value constructors and accessors" $ do
+        withDuckValue (c_duckdb_create_bool (CBool 1)) (c_duckdb_get_bool >=> (@?= CBool 1))
+
+        withDuckValue (c_duckdb_create_int8 (-8)) (c_duckdb_get_int8 >=> (@?= (-8 :: Int8)))
+
+        withDuckValue (c_duckdb_create_uint8 250) (c_duckdb_get_uint8 >=> (@?= (250 :: Word8)))
+
+        withDuckValue (c_duckdb_create_int16 (-32000)) (c_duckdb_get_int16 >=> (@?= (-32000 :: Int16)))
+
+        withDuckValue (c_duckdb_create_uint16 65000) (c_duckdb_get_uint16 >=> (@?= (65000 :: Word16)))
+
+        withDuckValue
+            (c_duckdb_create_int32 (-2000000000))
+            ( c_duckdb_get_int32
+                >=> (@?= (-2000000000 :: Int32))
+            )
+
+        withDuckValue
+            (c_duckdb_create_uint32 4000000000)
+            ( c_duckdb_get_uint32
+                >=> (@?= (4000000000 :: Word32))
+            )
+
+        withDuckValue
+            (c_duckdb_create_int64 (-9000000000000000000))
+            ( c_duckdb_get_int64
+                >=> (@?= (-9000000000000000000 :: Int64))
+            )
+
+        withDuckValue
+            (c_duckdb_create_uint64 10000000000000000000)
+            ( c_duckdb_get_uint64
+                >=> (@?= (10000000000000000000 :: Word64))
+            )
+
+        alloca \hugePtr -> do
+            poke hugePtr DuckDBHugeInt{duckDBHugeIntLower = 123, duckDBHugeIntUpper = -1}
+            withDuckValue (c_duckdb_create_hugeint hugePtr) \val ->
+                alloca \out -> do
+                    c_duckdb_get_hugeint val out
+                    peek out >>= (@?= DuckDBHugeInt 123 (-1))
+
+        alloca \uhugePtr -> do
+            poke uhugePtr DuckDBUHugeInt{duckDBUHugeIntLower = 321, duckDBUHugeIntUpper = 2}
+            withDuckValue (c_duckdb_create_uhugeint uhugePtr) \val ->
+                alloca \out -> do
+                    c_duckdb_get_uhugeint val out
+                    peek out >>= (@?= DuckDBUHugeInt 321 2)
+
+        withArray (map (fromIntegral . fromEnum) "1234") \digitsPtr -> do
+            let bignum = DuckDBBignum digitsPtr 4 (CBool 0)
+            with bignum \bignumPtr ->
+                withDuckValue (c_duckdb_create_bignum bignumPtr) \val ->
+                    alloca \outPtr -> do
+                        c_duckdb_get_bignum val outPtr
+                        DuckDBBignum outData outLen outNeg <- peek outPtr
+                        outLen @?= 4
+                        outNeg @?= CBool 0
+                        peekArray (fromIntegral outLen) outData >>= (@?= map (fromIntegral . fromEnum) "1234")
+                        when (outData /= nullPtr) $ c_duckdb_free (castPtr outData)
+
+        let hugeValue = DuckDBHugeInt{duckDBHugeIntLower = 42, duckDBHugeIntUpper = 0}
+            decimalValue = DuckDBDecimal{duckDBDecimalWidth = 10, duckDBDecimalScale = 2, duckDBDecimalValue = hugeValue}
+        with decimalValue \decimalPtr ->
+            withDuckValue (c_duckdb_create_decimal decimalPtr) \val ->
+                alloca \out -> do
+                    c_duckdb_get_decimal val out
+                    DuckDBDecimal{duckDBDecimalWidth = w, duckDBDecimalScale = s, duckDBDecimalValue = v} <- peek out
+                    (w, s, v) @?= (10, 2, hugeValue)
+
+        withDuckValue (c_duckdb_create_float (CFloat 1.25)) (c_duckdb_get_float >=> (@?= CFloat 1.25))
+
+        withDuckValue (c_duckdb_create_double (CDouble 2.75)) (c_duckdb_get_double >=> (@?= CDouble 2.75))
+
+        let sampleDate = DuckDBDate 12345
+        withDuckValue (c_duckdb_create_date sampleDate) (c_duckdb_get_date >=> (@?= sampleDate))
+
+        let sampleTime = DuckDBTime 987654321
+        withDuckValue (c_duckdb_create_time sampleTime) (c_duckdb_get_time >=> (@?= sampleTime))
+
+        let sampleTimeNs = DuckDBTimeNs 876543210
+        withDuckValue (c_duckdb_create_time_ns sampleTimeNs) (c_duckdb_get_time_ns >=> (@?= sampleTimeNs))
+
+        let sampleTimeTz = DuckDBTimeTz 5555
+        withDuckValue (c_duckdb_create_time_tz_value sampleTimeTz) (c_duckdb_get_time_tz >=> (@?= sampleTimeTz))
+
+        let sampleTimestamp = DuckDBTimestamp 444444
+        withDuckValue (c_duckdb_create_timestamp sampleTimestamp) (c_duckdb_get_timestamp >=> (@?= sampleTimestamp))
+
+        withDuckValue (c_duckdb_create_timestamp_tz sampleTimestamp) (c_duckdb_get_timestamp_tz >=> (@?= sampleTimestamp))
+
+        let tsSeconds = DuckDBTimestampS 12
+        withDuckValue (c_duckdb_create_timestamp_s tsSeconds) (c_duckdb_get_timestamp_s >=> (@?= tsSeconds))
+
+        let tsMillis = DuckDBTimestampMs 12000
+        withDuckValue (c_duckdb_create_timestamp_ms tsMillis) (c_duckdb_get_timestamp_ms >=> (@?= tsMillis))
+
+        let tsNanos = DuckDBTimestampNs 12000000
+        withDuckValue (c_duckdb_create_timestamp_ns tsNanos) (c_duckdb_get_timestamp_ns >=> (@?= tsNanos))
+
+        let intervalVal = DuckDBInterval{duckDBIntervalMonths = 1, duckDBIntervalDays = 2, duckDBIntervalMicros = 3000}
+        with intervalVal \intervalPtr ->
+            withDuckValue (c_duckdb_create_interval intervalPtr) \val ->
+                alloca \out -> do
+                    c_duckdb_get_interval val out
+                    peek out >>= (@?= intervalVal)
+
+        let blobBytes = map (fromIntegral . fromEnum) "duckdb-blob"
+        withArray blobBytes \blobPtr ->
+            withDuckValue (c_duckdb_create_blob blobPtr (fromIntegral (length blobBytes))) \val ->
+                alloca \blobOut -> do
+                    c_duckdb_get_blob val blobOut
+                    DuckDBBlob{duckDBBlobData = datPtr, duckDBBlobSize = size} <- peek blobOut
+                    size @?= fromIntegral (length blobBytes)
+                    peekArray (fromIntegral size) (castPtr datPtr :: Ptr Word8) >>= (@?= blobBytes)
+                    when (datPtr /= nullPtr) $ c_duckdb_free (castPtr datPtr)
+
+        let bitBytes = [0xAA :: Word8]
+        withArray bitBytes \bitDataPtr -> do
+            let bitVal = DuckDBBit{duckDBBitData = bitDataPtr, duckDBBitSize = 8}
+            with bitVal \bitPtr ->
+                withDuckValue (c_duckdb_create_bit bitPtr) \val ->
+                    alloca \bitOut -> do
+                        c_duckdb_get_bit val bitOut
+                        DuckDBBit{duckDBBitData = datPtr, duckDBBitSize = size} <- peek bitOut
+                        size @?= 8
+                        peekArray (fromIntegral ((size + 7) `div` 8)) datPtr >>= (@?= bitBytes)
+                        when (datPtr /= nullPtr) $ c_duckdb_free (castPtr datPtr)
+
+        let uuidValue = DuckDBUHugeInt{duckDBUHugeIntLower = 0x0011223344556677, duckDBUHugeIntUpper = 0x8899aabbccddeeff}
+        with uuidValue \uuidPtr ->
+            withDuckValue (c_duckdb_create_uuid uuidPtr) \val ->
+                alloca \out -> do
+                    c_duckdb_get_uuid val out
+                    peek out >>= (@?= uuidValue)
+
+        withCString "varchar literal" \str ->
+            withDuckValue (c_duckdb_create_varchar str) \val -> do
+                cPtr <- c_duckdb_get_varchar val
+                peekCString cPtr >>= (@?= "varchar literal")
+                c_duckdb_free (castPtr cPtr)
+
+        let lenString = "hello\0world"
+        withCString lenString \cStr -> do
+            let byteLen = fromIntegral (length lenString)
+            withDuckValue (c_duckdb_create_varchar_length cStr byteLen) \val -> do
+                cPtr <- c_duckdb_get_varchar val
+                peekCStringLen (cPtr, length "hello") >>= (@?= "hello")
+                c_duckdb_free (castPtr cPtr)
+
+        withDuckValue c_duckdb_create_null_value \val -> do
+            c_duckdb_is_null_value val >>= (@?= CBool 1)
+            strPtr <- c_duckdb_value_to_string val
+            peekCString strPtr >>= (@?= "NULL")
+            c_duckdb_free (castPtr strPtr)
+
+valueTypeReportsLogicalType :: TestTree
+valueTypeReportsLogicalType =
+    testCase "value type identifiers track constructors" $ do
+        withDuckValue (c_duckdb_create_int32 42) \intVal -> do
+            intType <- c_duckdb_get_value_type intVal
+            c_duckdb_get_type_id intType >>= (@?= DuckDBTypeInteger)
+
+        withDuckValue (withCString "duckdb" c_duckdb_create_varchar) \strVal -> do
+            strType <- c_duckdb_get_value_type strVal
+            c_duckdb_get_type_id strType >>= (@?= DuckDBTypeVarchar)
+
+        listChild <- c_duckdb_create_logical_type DuckDBTypeInteger
+        listLogical <- c_duckdb_create_list_type listChild
+        elemVal <- c_duckdb_create_int32 7
+        withArray [elemVal] \valuesArray -> do
+            let count = fromIntegral (1 :: Int)
+            withDuckValue (c_duckdb_create_list_value listChild valuesArray count) \listVal -> do
+                listType <- c_duckdb_get_value_type listVal
+                c_duckdb_get_type_id listType >>= (@?= DuckDBTypeList)
+        destroyDuckValue elemVal
+        destroyLogicalType listLogical
+        destroyLogicalType listChild
+
+collectionValuesRoundTrip :: TestTree
+collectionValuesRoundTrip =
+    testCase "list/array/map/struct/enum/union constructors" $ do
+        -- List value
+        listChild <- c_duckdb_create_logical_type DuckDBTypeInteger
+        listLogical <- c_duckdb_create_list_type listChild
+        listVal1 <- c_duckdb_create_int32 1
+        listVal2 <- c_duckdb_create_int32 2
+        withArray [listVal1, listVal2] \listArray -> do
+            let entryCount = fromIntegral (2 :: Int) :: DuckDBIdx
+            withDuckValue (c_duckdb_create_list_value listChild listArray entryCount) \listVal -> do
+                _ <- c_duckdb_get_list_size listVal
+                child0 <- c_duckdb_get_list_child listVal 0
+                c_duckdb_get_int32 child0 >>= (@?= 1)
+                destroyDuckValue child0
+                child1 <- c_duckdb_get_list_child listVal 1
+                c_duckdb_get_int32 child1 >>= (@?= 2)
+                destroyDuckValue child1
+                listStr <- c_duckdb_value_to_string listVal
+                peekCString listStr >>= (@?= "[1, 2]")
+                c_duckdb_free (castPtr listStr)
+        destroyDuckValue listVal1
+        destroyDuckValue listVal2
+        destroyLogicalType listLogical
+        destroyLogicalType listChild
+
+        -- Array value
+        arrayChild <- c_duckdb_create_logical_type DuckDBTypeInteger
+        arrayLogical <- c_duckdb_create_array_type arrayChild 2
+        arrVal1 <- c_duckdb_create_int32 7
+        arrVal2 <- c_duckdb_create_int32 8
+        withArray [arrVal1, arrVal2] \arrArray -> do
+            let entryCount = fromIntegral (2 :: Int) :: DuckDBIdx
+            withDuckValue (c_duckdb_create_array_value arrayChild arrArray entryCount) \arrVal -> do
+                _ <- c_duckdb_get_list_size arrVal
+                arrStr <- c_duckdb_value_to_string arrVal
+                peekCString arrStr >>= (@?= "[7, 8]")
+                c_duckdb_free (castPtr arrStr)
+        destroyDuckValue arrVal1
+        destroyDuckValue arrVal2
+        destroyLogicalType arrayLogical
+        destroyLogicalType arrayChild
+
+        -- Map value
+        keyType <- c_duckdb_create_logical_type DuckDBTypeVarchar
+        valType <- c_duckdb_create_logical_type DuckDBTypeInteger
+        mapLogical <- c_duckdb_create_map_type keyType valType
+        keyValue <- withCString "key" c_duckdb_create_varchar
+        valValue <- c_duckdb_create_int32 99
+        withArray [keyValue] \keyArray ->
+            withArray [valValue] \valArray -> do
+                let entryCount = fromIntegral (1 :: Int) :: DuckDBIdx
+                withDuckValue (c_duckdb_create_map_value mapLogical keyArray valArray entryCount) \mapVal -> do
+                    c_duckdb_get_map_size mapVal >>= (@?= 1)
+                    keyHandle <- c_duckdb_get_map_key mapVal 0
+                    keyStrPtr <- c_duckdb_get_varchar keyHandle
+                    peekCString keyStrPtr >>= (@?= "key")
+                    c_duckdb_free (castPtr keyStrPtr)
+                    destroyDuckValue keyHandle
+                    valHandle <- c_duckdb_get_map_value mapVal 0
+                    c_duckdb_get_int32 valHandle >>= (@?= 99)
+                    destroyDuckValue valHandle
+        destroyDuckValue keyValue
+        destroyDuckValue valValue
+        destroyLogicalType mapLogical
+        destroyLogicalType keyType
+        destroyLogicalType valType
+
+        -- Struct value
+        structInt <- c_duckdb_create_logical_type DuckDBTypeInteger
+        structText <- c_duckdb_create_logical_type DuckDBTypeVarchar
+        structLogical <-
+            withMany withCString ["id", "name"] \namePtrs ->
+                withArray [structInt, structText] \typeArray ->
+                    withArray namePtrs \nameArray ->
+                        c_duckdb_create_struct_type typeArray nameArray 2
+        withDuckValue (c_duckdb_create_int32 1) \idVal ->
+            withDuckValue (withCString "Alice" c_duckdb_create_varchar) \nameVal ->
+                withArray [idVal, nameVal] \structValues ->
+                    withDuckValue (c_duckdb_create_struct_value structLogical structValues) \structVal -> do
+                        child <- c_duckdb_get_struct_child structVal 1
+                        namePtr <- c_duckdb_get_varchar child
+                        peekCString namePtr >>= (@?= "Alice")
+                        c_duckdb_free (castPtr namePtr)
+                        destroyDuckValue child
+        destroyLogicalType structLogical
+        destroyLogicalType structInt
+        destroyLogicalType structText
+
+        -- Enum value
+        enumLogical <-
+            withMany withCString ["Red", "Green", "Blue"] \namePtrs ->
+                withArray namePtrs (`c_duckdb_create_enum_type` 3)
+        withDuckValue (c_duckdb_create_enum_value enumLogical 1) (c_duckdb_get_enum_value >=> (@?= 1))
+        destroyLogicalType enumLogical
+
+        -- Union value
+        unionInt <- c_duckdb_create_logical_type DuckDBTypeInteger
+        unionText <- c_duckdb_create_logical_type DuckDBTypeVarchar
+        unionLogical <-
+            withMany withCString ["int_member", "text_member"] \namePtrs ->
+                withArray [unionInt, unionText] \typeArray ->
+                    withArray namePtrs \nameArray ->
+                        c_duckdb_create_union_type typeArray nameArray 2
+        withDuckValue (c_duckdb_create_int32 42) \unionPayload ->
+            withDuckValue (c_duckdb_create_union_value unionLogical 0 unionPayload) \unionVal -> do
+                strPtr <- c_duckdb_value_to_string unionVal
+                peekCString strPtr >>= (@?= "union_value(int_member := 42)")
+                c_duckdb_free (castPtr strPtr)
+        destroyLogicalType unionLogical
+        destroyLogicalType unionInt
+        destroyLogicalType unionText
diff --git a/test/VectorTest.hs b/test/VectorTest.hs
new file mode 100644
--- /dev/null
+++ b/test/VectorTest.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE BlockArguments #-}
+
+module VectorTest (tests) where
+
+import Control.Monad (forM_, void, (>=>))
+import Data.Bits (setBit)
+import Data.Int (Int32)
+import Data.Word (Word64)
+import Database.DuckDB.FFI
+import Foreign.C.String (withCString)
+import Foreign.Marshal.Array (allocaArray, withArray)
+import Foreign.Ptr (Ptr, castPtr, nullPtr, plusPtr)
+import Foreign.Storable (peek, peekElemOff, poke, pokeElemOff, sizeOf)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertBool, testCase, (@?=))
+import Utils (clearValidityBit, setAllValid, withLogicalType, withSelectionVector, withValue, withVectorOfType)
+
+-- | Entry point for vector-centric tests.
+tests :: TestTree
+tests =
+    testGroup
+        "Vector Interface"
+        [ vectorDataAccess
+        , vectorValidityMask
+        , listVectorChildManagement
+        , arrayVectorChildAccess
+        , vectorSliceWithSelection
+        , structVectorChildAccess
+        , vectorReferenceValue
+        ]
+
+-- | Create a numeric vector, write elements, and observe the stored values.
+vectorDataAccess :: TestTree
+vectorDataAccess =
+    testCase "write and read integer vector data" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withVectorOfType intType 4 \vec -> do
+                colType <- c_duckdb_vector_get_column_type vec
+                withLogicalType (pure colType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+
+                rawPtr <- c_duckdb_vector_get_data vec
+                let dataPtr = castPtr rawPtr :: Ptr Int32
+                forM_ (zip [0 ..] [10, 20, 30, 40]) \(idx, val) -> poke (dataPtr `plusElem` idx) val
+
+                forM_ (zip [0 ..] [10, 20, 30, 40]) \(idx, val) -> peek (dataPtr `plusElem` idx) >>= (@?= val)
+
+-- | Ensure validity mask allocation and manipulate individual bits.
+vectorValidityMask :: TestTree
+vectorValidityMask =
+    testCase "ensure validity mask and clear single entry" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withVectorOfType intType 4 \vec -> do
+                void (c_duckdb_vector_ensure_validity_writable vec)
+                validity <- c_duckdb_vector_get_validity vec
+                assertBool "validity mask should exist" (validity /= nullPtr)
+
+                setAllValid validity 4
+                clearValidityBit validity 2
+
+                current <- peek validity
+                let expected = foldl setBit (0 :: Word64) [0, 1, 3]
+                current @?= expected
+
+-- | Exercise list vector child management APIs.
+listVectorChildManagement :: TestTree
+listVectorChildManagement =
+    testCase "list vector reserves space and reports size" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withLogicalType (c_duckdb_create_list_type intType) \listType ->
+                withVectorOfType listType 2 \listVec -> do
+                    childVec0 <- c_duckdb_list_vector_get_child listVec
+                    assertBool "list child vector should be non-null" (childVec0 /= nullPtr)
+                    initialSize <- c_duckdb_list_vector_get_size listVec
+                    initialSize @?= 0
+
+                    reserveState <- c_duckdb_list_vector_reserve listVec 5
+                    reserveState @?= DuckDBSuccess
+                    sizeState <- c_duckdb_list_vector_set_size listVec 5
+                    sizeState @?= DuckDBSuccess
+                    c_duckdb_list_vector_get_size listVec >>= (@?= 5)
+
+                    childVec <- c_duckdb_list_vector_get_child listVec
+                    metaRaw <- c_duckdb_vector_get_data listVec
+                    let metaWords = castPtr metaRaw :: Ptr Word64
+                    -- row 0 -> offset 0 length 3, row 1 -> offset 3 length 2
+                    pokeElemOff metaWords 0 0
+                    pokeElemOff metaWords 1 3
+                    pokeElemOff metaWords 2 3
+                    pokeElemOff metaWords 3 2
+
+                    childRaw <- c_duckdb_vector_get_data childVec
+                    let childInts = castPtr childRaw :: Ptr Int32
+                        payload = [11, 12, 13, 21, 22 :: Int32]
+                    forM_ (zip [0 ..] payload) (uncurry (pokeElemOff childInts))
+                    fetched <- mapM (peekElemOff childInts) [0 .. length payload - 1]
+                    fetched @?= payload
+
+                    peekElemOff metaWords 1 >>= (@?= 3)
+                    peekElemOff metaWords 3 >>= (@?= 2)
+
+-- | Verify array child vector exposes a flat buffer sized by row * array length.
+arrayVectorChildAccess :: TestTree
+arrayVectorChildAccess =
+    testCase "array vector child flattens elements" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withLogicalType (c_duckdb_create_array_type intType 3) \arrayType ->
+                withVectorOfType arrayType 2 \arrayVec -> do
+                    childVec <- c_duckdb_array_vector_get_child arrayVec
+                    childType <- c_duckdb_vector_get_column_type childVec
+                    withLogicalType (pure childType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+
+                    childRaw <- c_duckdb_vector_get_data childVec
+                    let childInts = castPtr childRaw :: Ptr Int32
+                        payload = [1, 2, 3, 4, 5, 6 :: Int32]
+                    forM_ (zip [0 ..] payload) (uncurry (pokeElemOff childInts))
+                    mapM (peekElemOff childInts) [0 .. length payload - 1] >>= (@?= payload)
+
+-- | Slice a vector with a selection vector and materialize the dictionary.
+vectorSliceWithSelection :: TestTree
+vectorSliceWithSelection =
+    testCase "slice vector materializes dictionary order" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withVectorOfType intType 4 \vec -> do
+                dataRaw <- c_duckdb_vector_get_data vec
+                let vecData = castPtr dataRaw :: Ptr Int32
+                forM_ (zip [0 ..] [10, 20, 30, 40 :: Int32]) (uncurry (pokeElemOff vecData))
+
+                withSelectionVector 2 \sel -> do
+                    selPtr <- c_duckdb_selection_vector_get_data_ptr sel
+                    pokeElemOff selPtr 0 (fromIntegral (3 :: Int))
+                    pokeElemOff selPtr 1 (fromIntegral (1 :: Int))
+                    c_duckdb_slice_vector vec sel 2
+
+                    withVectorOfType intType 2 \materialized ->
+                        withSelectionVector 2 \copySel -> do
+                            copyPtr <- c_duckdb_selection_vector_get_data_ptr copySel
+                            pokeElemOff copyPtr 0 0
+                            pokeElemOff copyPtr 1 1
+                            c_duckdb_vector_copy_sel vec materialized copySel 2 0 0
+                            matRaw <- c_duckdb_vector_get_data materialized
+                            let matPtr = castPtr matRaw :: Ptr Int32
+                            mapM (peekElemOff matPtr) [0, 1] >>= (@?= [40, 20])
+
+-- | Ensure struct vectors expose individual child vectors with matching types.
+structVectorChildAccess :: TestTree
+structVectorChildAccess =
+    testCase "struct vector exposes typed child vectors" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withLogicalType (c_duckdb_create_logical_type DuckDBTypeDouble) \doubleType ->
+                allocaArray 2 \typesPtr -> do
+                    pokeElemOff typesPtr 0 intType
+                    pokeElemOff typesPtr 1 doubleType
+                    withCString "ints" \name0 ->
+                        withCString "doubles" \name1 ->
+                            withArray [name0, name1] \namesPtr ->
+                                withLogicalType (c_duckdb_create_struct_type typesPtr namesPtr 2) \structType ->
+                                    withVectorOfType structType 1 \structVec -> do
+                                        intChild <- c_duckdb_struct_vector_get_child structVec 0
+                                        dblChild <- c_duckdb_struct_vector_get_child structVec 1
+
+                                        intChildType <- c_duckdb_vector_get_column_type intChild
+                                        withLogicalType (pure intChildType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeInteger))
+                                        dblChildType <- c_duckdb_vector_get_column_type dblChild
+                                        withLogicalType (pure dblChildType) (c_duckdb_get_type_id >=> (@?= DuckDBTypeDouble))
+
+                                        intRaw <- c_duckdb_vector_get_data intChild
+                                        dblRaw <- c_duckdb_vector_get_data dblChild
+                                        let intPtr = castPtr intRaw :: Ptr Int32
+                                            dblPtr = castPtr dblRaw :: Ptr Double
+                                        pokeElemOff intPtr 0 7
+                                        pokeElemOff dblPtr 0 3.5
+                                        peekElemOff intPtr 0 >>= (@?= 7)
+                                        peekElemOff dblPtr 0 >>= (@?= 3.5)
+
+-- | Copy a scalar duckdb_value into a vector without materializing a chunk.
+vectorReferenceValue :: TestTree
+vectorReferenceValue =
+    testCase "vector_reference_value writes scalar contents" $ do
+        withLogicalType (c_duckdb_create_logical_type DuckDBTypeInteger) \intType ->
+            withVectorOfType intType 1 \vec ->
+                withValue (c_duckdb_create_int32 123) \value -> do
+                    c_duckdb_vector_reference_value vec value
+                    raw <- c_duckdb_vector_get_data vec
+                    let dataPtr = castPtr raw :: Ptr Int32
+                    peekElemOff dataPtr 0 >>= (@?= 123)
+
+-- helpers -------------------------------------------------------------------
+
+plusElem :: Ptr Int32 -> Int -> Ptr Int32
+plusElem base idx = base `plusPtr` (idx * sizeOf (undefined :: Int32))
