You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by pa...@apache.org on 2022/07/08 18:00:06 UTC

[arrow-nanoarrow] branch main updated: Add files from paleolimbot/nanoarrow (#1)

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

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-nanoarrow.git


The following commit(s) were added to refs/heads/main by this push:
     new fb8afa9  Add files from paleolimbot/nanoarrow (#1)
fb8afa9 is described below

commit fb8afa9568534784e6b6475c76ea258f70e50f44
Author: Dewey Dunnington <de...@fishandwhistle.net>
AuthorDate: Fri Jul 8 15:00:01 2022 -0300

    Add files from paleolimbot/nanoarrow (#1)
    
    * add files from paleolimbot/nanoarrow
    
    * add clang-format
    
    * Update README.md
    
    Co-authored-by: David Li <li...@gmail.com>
    
    * add missing license blocks, remove codecov badge until it gets set up
    
    Co-authored-by: David Li <li...@gmail.com>
---
 .clang-format                         |  21 +
 .github/workflows/build-and-test.yaml | 121 ++++++
 .gitignore                            |  23 +
 CMakeLists.txt                        |  86 ++++
 CMakePresets.json                     |  28 ++
 CMakeUserPresets.json.example         |  19 +
 README.md                             |  82 +++-
 src/nanoarrow/allocator.c             |  51 +++
 src/nanoarrow/allocator_test.cc       | 110 +++++
 src/nanoarrow/buffer.c                | 122 ++++++
 src/nanoarrow/buffer_test.cc          | 162 +++++++
 src/nanoarrow/error.c                 |  42 ++
 src/nanoarrow/error_test.cc           |  46 ++
 src/nanoarrow/metadata.c              | 121 ++++++
 src/nanoarrow/metadata_test.cc        |  46 ++
 src/nanoarrow/nanoarrow.c             |  23 +
 src/nanoarrow/nanoarrow.h             | 566 ++++++++++++++++++++++++
 src/nanoarrow/schema.c                | 475 +++++++++++++++++++++
 src/nanoarrow/schema_test.cc          | 417 ++++++++++++++++++
 src/nanoarrow/schema_view.c           | 677 +++++++++++++++++++++++++++++
 src/nanoarrow/schema_view_test.cc     | 782 ++++++++++++++++++++++++++++++++++
 21 files changed, 4019 insertions(+), 1 deletion(-)

diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..9448dc8
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+BasedOnStyle: Google
+ColumnLimit: 90
+DerivePointerAlignment: false
+IncludeBlocks: Preserve
diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml
new file mode 100644
index 0000000..0cd7141
--- /dev/null
+++ b/.github/workflows/build-and-test.yaml
@@ -0,0 +1,121 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+on:
+  push:
+    branches: [main, master]
+  pull_request:
+    branches: [main, master]
+
+name: Build and Test
+
+jobs:
+  build-and-test:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Install dependencies
+        run: |
+          sudo apt-get install -y cmake valgrind
+
+      - name: Cache Dependency Builds
+        id: cache-deps-build
+        uses: actions/cache@v3
+        with:
+          path: build-deps
+          key: ${{ runner.os }}-3
+
+      - name: Init build dir
+        if: steps.cache-deps-build.outputs.cache-hit != 'true'
+        run: mkdir build-deps
+
+      # There seems to be an error passing -DGTest_DIR into Arrow's build
+      # so we just build the same version of it and install
+      - name: Fetch googletest
+        if: steps.cache-deps-build.outputs.cache-hit != 'true'
+        uses: actions/checkout@v3
+        with:
+          repository: google/googletest
+          ref: release-1.11.0
+          path: build-deps/googletest
+          fetch-depth: 0
+
+      - name: Build googletest
+        if: steps.cache-deps-build.outputs.cache-hit != 'true'
+        run: |
+          cd build-deps/googletest
+          cmake .
+          cmake --build .
+          cmake --install . --prefix ../../dist
+
+      - name: Fetch Arrow
+        if: steps.cache-deps-build.outputs.cache-hit != 'true'
+        uses: actions/checkout@v3
+        with:
+          repository: apache/arrow
+          ref: apache-arrow-8.0.0
+          path: build-deps/arrow
+          fetch-depth: 0
+
+      - name: Build Arrow
+        if: steps.cache-deps-build.outputs.cache-hit != 'true'
+        run: |
+          mkdir build-deps/arrow-build
+          cd build-deps/arrow-build
+          cmake ../arrow/cpp -DARROW_JSON=ON -DARROW_TESTING=ON -DBoost_SOURCE=BUNDLED
+          cmake --build .
+          cmake --install . --prefix ../../dist
+
+      - name: Install Dependencies
+        run: |
+          cd build-deps/arrow-build
+          cmake --install . --prefix ../../dist
+          cd ../googletest
+          cmake --install . --prefix ../../dist
+
+      - name: Build nanoarrow
+        run: |
+          mkdir build
+          cd build
+          cmake .. -DCMAKE_BUILD_TYPE=Debug -DGTest_DIR=`pwd`/../dist/lib/cmake/GTest -DArrow_DIR=`pwd`/../dist/lib/cmake/arrow -DArrowTesting_DIR=`pwd`/../dist/lib/cmake/arrow -DNANOARROW_CODE_COVERAGE=ON -DNANOARROW_BUILD_TESTS=ON
+          cmake --build .
+
+      - name: Run tests
+        run: |
+          cd build
+          ctest -T test --output-on-failure .
+
+      - name: Run tests with valgrind
+        run: |
+          cd build
+          valgrind --tool=memcheck --leak-check=full ctest -T test .
+
+      - name: Calculate coverage
+        run: |
+          SOURCE_PREFIX=`pwd`
+          mkdir build/cov
+          cd build/cov
+          gcov -abcfu --source-prefix=$SOURCE_PREFIX `find ../CMakeFiles/nanoarrow.dir/ -name "*.gcno"`
+
+      - name: Upload coverage
+        uses: codecov/codecov-action@v2
+        with:
+          directory: build/cov
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..754d9b5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+build
+dist
+arrow-hpp
+.DS_Store
+CMakeUserPresets.json
+.vscode
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..9cbcf57
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+message(STATUS "Building using CMake version: ${CMAKE_VERSION}")
+cmake_minimum_required(VERSION 3.14)
+
+project(NanoArrow)
+
+option(NANOARROW_BUILD_TESTS "Build tests" OFF)
+
+option(NANOARROW_CODE_COVERAGE "Enable coverage reporting" OFF)
+add_library(coverage_config INTERFACE)
+
+include_directories(src)
+add_library(
+    nanoarrow
+    src/nanoarrow/allocator.c
+    src/nanoarrow/buffer.c
+    src/nanoarrow/error.c
+    src/nanoarrow/metadata.c
+    src/nanoarrow/schema.c
+    src/nanoarrow/schema_view.c)
+
+install(TARGETS nanoarrow DESTINATION lib)
+install(DIRECTORY src/ DESTINATION include FILES_MATCHING PATTERN "*.h")
+install(DIRECTORY src/ DESTINATION include FILES_MATCHING PATTERN "*.c")
+
+if (NANOARROW_BUILD_TESTS)
+    # For testing we use GTest + Arrow C++ (both need C++11)
+    include(FetchContent)
+    include(CTest)
+
+    set(CMAKE_CXX_STANDARD 11)
+    set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+    find_package(Arrow REQUIRED)
+    message(STATUS "Arrow version: ${ARROW_VERSION}")
+    message(STATUS "Arrow SO version: ${ARROW_FULL_SO_VERSION}")
+    find_package(ArrowTesting REQUIRED)
+
+    find_package(GTest REQUIRED)
+    enable_testing()
+
+    add_executable(allocator_test src/nanoarrow/allocator_test.cc)
+    add_executable(buffer_test src/nanoarrow/buffer_test.cc)
+    add_executable(error_test src/nanoarrow/error_test.cc)
+    add_executable(metadata_test src/nanoarrow/metadata_test.cc)
+    add_executable(schema_test src/nanoarrow/schema_test.cc)
+    add_executable(schema_view_test src/nanoarrow/schema_view_test.cc)
+
+    if (NANOARROW_CODE_COVERAGE)
+        target_compile_options(coverage_config INTERFACE -O0 -g --coverage)
+        target_link_options(coverage_config INTERFACE --coverage)
+        target_link_libraries(nanoarrow coverage_config)
+    endif()
+
+    target_link_libraries(allocator_test nanoarrow GTest::gtest_main arrow_shared arrow_testing_shared)
+    target_link_libraries(buffer_test nanoarrow GTest::gtest_main)
+    target_link_libraries(error_test nanoarrow GTest::gtest_main)
+    target_link_libraries(metadata_test nanoarrow GTest::gtest_main arrow_shared arrow_testing_shared)
+    target_link_libraries(schema_test nanoarrow GTest::gtest_main arrow_shared arrow_testing_shared)
+    target_link_libraries(schema_view_test nanoarrow GTest::gtest_main arrow_shared arrow_testing_shared)
+
+    include(GoogleTest)
+    gtest_discover_tests(allocator_test)
+    gtest_discover_tests(buffer_test)
+    gtest_discover_tests(error_test)
+    gtest_discover_tests(metadata_test)
+    gtest_discover_tests(schema_test)
+    gtest_discover_tests(schema_view_test)
+
+endif()
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000..f209935
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,28 @@
+{
+    "version": 3,
+    "cmakeMinimumRequired": {
+        "major": 3,
+        "minor": 21,
+        "patch": 0
+    },
+    "configurePresets": [
+        {
+            "name": "default",
+            "displayName": "Default Config",
+            "binaryDir": "${sourceDir}/build",
+            "installDir": "${sourceDir}/dist",
+            "cacheVariables": {}
+        },
+        {
+            "name": "default-with-tests",
+            "inherits": [
+                "default"
+            ],
+            "displayName": "Default with tests",
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Debug",
+                "NANOARROW_BUILD_TESTS": "ON"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/CMakeUserPresets.json.example b/CMakeUserPresets.json.example
new file mode 100644
index 0000000..694eb9f
--- /dev/null
+++ b/CMakeUserPresets.json.example
@@ -0,0 +1,19 @@
+{
+    "version": 3,
+    "cmakeMinimumRequired": {
+      "major": 3,
+      "minor": 21,
+      "patch": 0
+    },
+    "configurePresets": [
+        {
+            "name": "paleolimbot-local",
+            "inherits": ["default"],
+            "displayName": "(paleolimbot) local build",
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Debug",
+                "CMAKE_CXX_FLAGS_DEBUG": "-g"
+            }
+          }
+    ]
+}
diff --git a/README.md b/README.md
index 4832d5f..b1051a0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,81 @@
-# arrow-nanoarrow
+<!---
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+# nanoarrow
+
+The nanoarrow library is a set of helper functions to interpret and generate
+[Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html)
+and [Arrow C Stream Interface](https://arrow.apache.org/docs/format/CStreamInterface.html)
+structures. The library is in active development and should currently be used only
+for entertainment purposes. Everything from the name of the project to the variable
+names are up for grabs (i.e., suggest/pull request literally any ideas you may
+have!).
+
+Whereas the current suite of Arrow implementations provide the basis for a
+comprehensive data analysis toolkit, this library is intended to support clients
+that wish to produce or interpret Arrow C Data and/or Arrow C Stream structures.
+The library will:
+
+- Create, copy, parse, and validate struct ArrowSchema objects (all types mentioned
+  in the C Data interface specification)
+- Create and validate struct ArrowArray/struct ArrowSchema pairs for (all types
+  mentioned in the C Data interface specification)
+- Iterate over struct ArrowArrays element-wise (non-nested types) (i.e., is the
+  ith element null; get the ith element).
+- Build Arrays element-wise (non-nested types) (i.e., basic Array Builder logic).
+
+While it will not provide full support for nested types, it should provide enough
+infrastructure that an extension library with a similar format could implement such
+support.
+
+## Usage
+
+You can use nanoarrow in your project in two ways:
+
+1. Copy contents of the `src/nanoarrow/` into your favourite include directory and
+   `#include <nanoarrow/nanoarrow.c>` somewhere in your project exactly once.
+2. Clone and use `cmake`, `cmake --build`, and `cmake --install` to build/install
+   the static library and add `-L/path/to/nanoarrow/lib -lnanoarrow` to your favourite
+   linker flag configuration.
+
+All public functions and types are declared in `nanoarrow/nanoarrow.h`.
+
+In all cases you will want to copy this project or pin your build to a specific commit
+since it will change rapidly and regularly. The nanoarrow library does not and will
+not provide ABI stability (i.e., you must vendor or link to a private version of
+the static library).
+
+## Background
+
+The design of nanoarrow reflects the needs of a few previous libraries/prototypes
+requiring a library with a similar scope:
+
+- DuckDB’s Arrow wrappers, the details of which are in a few places
+  (e.g., [here](https://github.com/duckdb/duckdb/blob/master/src/common/arrow_wrapper.cpp),
+  [here](https://github.com/duckdb/duckdb/blob/master/src/main/query_result.cpp),
+  and a few other places)
+- An [R wrapper around the C Data interface](https://github.com/paleolimbot/narrow),
+  along which a [C-only library](https://github.com/paleolimbot/narrow/tree/master/src/narrow)
+  was prototyped.
+- An [R implementation of the draft GeoArrow specification](https://github.com/paleolimbot/geoarrow),
+  along which a [mostly header-only C++ library](https://github.com/paleolimbot/geonanoarrowpp/tree/main/src/geoarrow/internal/arrow-hpp)
+  was prototyped.
+- The [Arrow Database Connectivity](https://github.com/apache/arrow-adbc) C API, for which drivers
+  in theory can be written in C (which is currently difficult in practice because of there
+  are few if any tools to help do this properly).
diff --git a/src/nanoarrow/allocator.c b/src/nanoarrow/allocator.c
new file mode 100644
index 0000000..8495037
--- /dev/null
+++ b/src/nanoarrow/allocator.c
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "nanoarrow.h"
+
+void* ArrowMalloc(int64_t size) { return malloc(size); }
+
+void* ArrowRealloc(void* ptr, int64_t size) { return realloc(ptr, size); }
+
+void ArrowFree(void* ptr) { free(ptr); }
+
+static uint8_t* ArrowBufferAllocatorMallocAllocate(struct ArrowBufferAllocator* allocator,
+                                                   int64_t size) {
+  return ArrowMalloc(size);
+}
+
+static uint8_t* ArrowBufferAllocatorMallocReallocate(
+    struct ArrowBufferAllocator* allocator, uint8_t* ptr, int64_t old_size,
+    int64_t new_size) {
+  return ArrowRealloc(ptr, new_size);
+}
+
+static void ArrowBufferAllocatorMallocFree(struct ArrowBufferAllocator* allocator,
+                                           uint8_t* ptr, int64_t size) {
+  ArrowFree(ptr);
+}
+
+static struct ArrowBufferAllocator ArrowBufferAllocatorMalloc = {
+    &ArrowBufferAllocatorMallocAllocate, &ArrowBufferAllocatorMallocReallocate,
+    &ArrowBufferAllocatorMallocFree, NULL};
+
+struct ArrowBufferAllocator* ArrowBufferAllocatorDefault() {
+  return &ArrowBufferAllocatorMalloc;
+}
diff --git a/src/nanoarrow/allocator_test.cc b/src/nanoarrow/allocator_test.cc
new file mode 100644
index 0000000..70aaf56
--- /dev/null
+++ b/src/nanoarrow/allocator_test.cc
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <cstring>
+#include <string>
+
+#include <arrow/memory_pool.h>
+#include <gtest/gtest.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+using namespace arrow;
+
+static uint8_t* MemoryPoolAllocate(struct ArrowBufferAllocator* allocator, int64_t size) {
+  MemoryPool* pool = reinterpret_cast<MemoryPool*>(allocator->private_data);
+  uint8_t* out;
+  if (pool->Allocate(size, &out).ok()) {
+    return out;
+  } else {
+    return nullptr;
+  }
+}
+
+static uint8_t* MemoryPoolReallocate(struct ArrowBufferAllocator* allocator, uint8_t* ptr,
+                                     int64_t old_size, int64_t new_size) {
+  MemoryPool* pool = reinterpret_cast<MemoryPool*>(allocator->private_data);
+  uint8_t* out = ptr;
+  if (pool->Reallocate(old_size, new_size, &out).ok()) {
+    return out;
+  } else {
+    return nullptr;
+  }
+}
+
+static void MemoryPoolFree(struct ArrowBufferAllocator* allocator, uint8_t* ptr,
+                           int64_t size) {
+  MemoryPool* pool = reinterpret_cast<MemoryPool*>(allocator->private_data);
+  pool->Free(ptr, size);
+}
+
+static void MemoryPoolAllocatorInit(MemoryPool* pool,
+                                    struct ArrowBufferAllocator* allocator) {
+  allocator->allocate = &MemoryPoolAllocate;
+  allocator->reallocate = &MemoryPoolReallocate;
+  allocator->free = &MemoryPoolFree;
+  allocator->private_data = pool;
+}
+
+TEST(AllocatorTest, AllocatorTestDefault) {
+  struct ArrowBufferAllocator* allocator = ArrowBufferAllocatorDefault();
+
+  uint8_t* buffer = allocator->allocate(allocator, 10);
+  const char* test_str = "abcdefg";
+  memcpy(buffer, test_str, strlen(test_str) + 1);
+
+  buffer = allocator->reallocate(allocator, buffer, 10, 100);
+  EXPECT_STREQ(reinterpret_cast<const char*>(buffer), test_str);
+
+  allocator->free(allocator, buffer, 100);
+
+  buffer = allocator->allocate(allocator, std::numeric_limits<int64_t>::max());
+  EXPECT_EQ(buffer, nullptr);
+
+  buffer =
+      allocator->reallocate(allocator, buffer, 0, std::numeric_limits<int64_t>::max());
+  EXPECT_EQ(buffer, nullptr);
+}
+
+TEST(AllocatorTest, AllocatorTestMemoryPool) {
+  struct ArrowBufferAllocator arrow_allocator;
+  MemoryPoolAllocatorInit(system_memory_pool(), &arrow_allocator);
+
+  int64_t allocated0 = system_memory_pool()->bytes_allocated();
+
+  uint8_t* buffer = arrow_allocator.allocate(&arrow_allocator, 10);
+  EXPECT_EQ(system_memory_pool()->bytes_allocated() - allocated0, 10);
+  memset(buffer, 0, 10);
+
+  const char* test_str = "abcdefg";
+  memcpy(buffer, test_str, strlen(test_str) + 1);
+
+  buffer = arrow_allocator.reallocate(&arrow_allocator, buffer, 10, 100);
+  EXPECT_EQ(system_memory_pool()->bytes_allocated() - allocated0, 100);
+  EXPECT_STREQ(reinterpret_cast<const char*>(buffer), test_str);
+
+  arrow_allocator.free(&arrow_allocator, buffer, 100);
+  EXPECT_EQ(system_memory_pool()->bytes_allocated(), allocated0);
+
+  buffer =
+      arrow_allocator.allocate(&arrow_allocator, std::numeric_limits<int64_t>::max());
+  EXPECT_EQ(buffer, nullptr);
+
+  buffer = arrow_allocator.reallocate(&arrow_allocator, buffer, 0,
+                                      std::numeric_limits<int64_t>::max());
+  EXPECT_EQ(buffer, nullptr);
+}
diff --git a/src/nanoarrow/buffer.c b/src/nanoarrow/buffer.c
new file mode 100644
index 0000000..ad56a98
--- /dev/null
+++ b/src/nanoarrow/buffer.c
@@ -0,0 +1,122 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "nanoarrow.h"
+
+static int64_t ArrowGrowByFactor(int64_t current_capacity, int64_t new_capacity) {
+  int64_t doubled_capacity = current_capacity * 2;
+  if (doubled_capacity > new_capacity) {
+    return doubled_capacity;
+  } else {
+    return new_capacity;
+  }
+}
+
+void ArrowBufferInit(struct ArrowBuffer* buffer) {
+  buffer->data = NULL;
+  buffer->size_bytes = 0;
+  buffer->capacity_bytes = 0;
+  buffer->allocator = ArrowBufferAllocatorDefault();
+}
+
+ArrowErrorCode ArrowBufferSetAllocator(struct ArrowBuffer* buffer,
+                                       struct ArrowBufferAllocator* allocator) {
+  if (buffer->data == NULL) {
+    buffer->allocator = allocator;
+    return NANOARROW_OK;
+  } else {
+    return EINVAL;
+  }
+}
+
+void ArrowBufferReset(struct ArrowBuffer* buffer) {
+  if (buffer->data != NULL) {
+    buffer->allocator->free(buffer->allocator, (uint8_t*)buffer->data,
+                            buffer->capacity_bytes);
+    buffer->data = NULL;
+  }
+
+  buffer->capacity_bytes = 0;
+  buffer->size_bytes = 0;
+}
+
+void ArrowBufferMove(struct ArrowBuffer* buffer, struct ArrowBuffer* buffer_out) {
+  memcpy(buffer_out, buffer, sizeof(struct ArrowBuffer));
+  buffer->data = NULL;
+  ArrowBufferReset(buffer);
+}
+
+ArrowErrorCode ArrowBufferResize(struct ArrowBuffer* buffer, int64_t new_capacity_bytes,
+                                 char shrink_to_fit) {
+  if (new_capacity_bytes < 0) {
+    return EINVAL;
+  }
+
+  if (new_capacity_bytes > buffer->capacity_bytes || shrink_to_fit) {
+    buffer->data = buffer->allocator->reallocate(
+        buffer->allocator, buffer->data, buffer->capacity_bytes, new_capacity_bytes);
+    if (buffer->data == NULL && new_capacity_bytes > 0) {
+      buffer->capacity_bytes = 0;
+      buffer->size_bytes = 0;
+      return ENOMEM;
+    }
+
+    buffer->capacity_bytes = new_capacity_bytes;
+  }
+
+  // Ensures that when shrinking that size <= capacity
+  if (new_capacity_bytes < buffer->size_bytes) {
+    buffer->size_bytes = new_capacity_bytes;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowBufferReserve(struct ArrowBuffer* buffer,
+                                  int64_t additional_size_bytes) {
+  int64_t min_capacity_bytes = buffer->size_bytes + additional_size_bytes;
+  if (min_capacity_bytes <= buffer->capacity_bytes) {
+    return NANOARROW_OK;
+  }
+
+  return ArrowBufferResize(
+      buffer, ArrowGrowByFactor(buffer->capacity_bytes, min_capacity_bytes), 0);
+}
+
+void ArrowBufferAppendUnsafe(struct ArrowBuffer* buffer, const void* data,
+                             int64_t size_bytes) {
+  if (size_bytes > 0) {
+    memcpy(buffer->data + buffer->size_bytes, data, size_bytes);
+    buffer->size_bytes += size_bytes;
+  }
+}
+
+ArrowErrorCode ArrowBufferAppend(struct ArrowBuffer* buffer, const void* data,
+                                 int64_t size_bytes) {
+  int result = ArrowBufferReserve(buffer, size_bytes);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  ArrowBufferAppendUnsafe(buffer, data, size_bytes);
+  return NANOARROW_OK;
+}
diff --git a/src/nanoarrow/buffer_test.cc b/src/nanoarrow/buffer_test.cc
new file mode 100644
index 0000000..42c4a85
--- /dev/null
+++ b/src/nanoarrow/buffer_test.cc
@@ -0,0 +1,162 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <cerrno>
+#include <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+// This test allocator guarantees that allocator->reallocate will return
+// a new pointer so that we can test when reallocations happen whilst
+// building buffers.
+static uint8_t* TestAllocatorAllocate(struct ArrowBufferAllocator* allocator,
+                                      int64_t size) {
+  return reinterpret_cast<uint8_t*>(malloc(size));
+}
+
+static uint8_t* TestAllocatorReallocate(struct ArrowBufferAllocator* allocator,
+                                        uint8_t* ptr, int64_t old_size,
+                                        int64_t new_size) {
+  uint8_t* new_ptr = TestAllocatorAllocate(allocator, new_size);
+
+  int64_t copy_size = std::min<int64_t>(old_size, new_size);
+  if (new_ptr != nullptr && copy_size > 0) {
+    memcpy(new_ptr, ptr, copy_size);
+  }
+
+  if (ptr != nullptr) {
+    free(ptr);
+  }
+
+  return new_ptr;
+}
+
+static void TestAllocatorFree(struct ArrowBufferAllocator* allocator, uint8_t* ptr,
+                              int64_t size) {
+  free(ptr);
+}
+
+static struct ArrowBufferAllocator test_allocator = {
+    &TestAllocatorAllocate, &TestAllocatorReallocate, &TestAllocatorFree, nullptr};
+
+TEST(BufferTest, BufferTestBasic) {
+  struct ArrowBuffer buffer;
+
+  // Init
+  ArrowBufferInit(&buffer);
+  ASSERT_EQ(ArrowBufferSetAllocator(&buffer, &test_allocator), NANOARROW_OK);
+  EXPECT_EQ(buffer.data, nullptr);
+  EXPECT_EQ(buffer.capacity_bytes, 0);
+  EXPECT_EQ(buffer.size_bytes, 0);
+
+  // Reserve where capacity > current_capacity * growth_factor
+  EXPECT_EQ(ArrowBufferReserve(&buffer, 10), NANOARROW_OK);
+  EXPECT_NE(buffer.data, nullptr);
+  EXPECT_EQ(buffer.capacity_bytes, 10);
+  EXPECT_EQ(buffer.size_bytes, 0);
+
+  // Write without triggering a realloc
+  uint8_t* first_data = buffer.data;
+  EXPECT_EQ(ArrowBufferAppend(&buffer, "1234567890", 10), NANOARROW_OK);
+  EXPECT_EQ(buffer.data, first_data);
+  EXPECT_EQ(buffer.capacity_bytes, 10);
+  EXPECT_EQ(buffer.size_bytes, 10);
+
+  // Write triggering a realloc
+  EXPECT_EQ(ArrowBufferAppend(&buffer, "1", 2), NANOARROW_OK);
+  EXPECT_NE(buffer.data, first_data);
+  EXPECT_EQ(buffer.capacity_bytes, 20);
+  EXPECT_EQ(buffer.size_bytes, 12);
+  EXPECT_STREQ(reinterpret_cast<char*>(buffer.data), "12345678901");
+
+  // Resize smaller without shrinking
+  EXPECT_EQ(ArrowBufferResize(&buffer, 5, false), NANOARROW_OK);
+  EXPECT_EQ(buffer.capacity_bytes, 20);
+  EXPECT_EQ(buffer.size_bytes, 5);
+  EXPECT_EQ(strncmp(reinterpret_cast<char*>(buffer.data), "12345", 5), 0);
+
+  // Resize smaller with shrinking
+  EXPECT_EQ(ArrowBufferResize(&buffer, 4, true), NANOARROW_OK);
+  EXPECT_EQ(buffer.capacity_bytes, 4);
+  EXPECT_EQ(buffer.size_bytes, 4);
+  EXPECT_EQ(strncmp(reinterpret_cast<char*>(buffer.data), "1234", 4), 0);
+
+  // Reset the buffer
+  ArrowBufferReset(&buffer);
+  EXPECT_EQ(buffer.data, nullptr);
+  EXPECT_EQ(buffer.capacity_bytes, 0);
+  EXPECT_EQ(buffer.size_bytes, 0);
+}
+
+TEST(BufferTest, BufferTestMove) {
+  struct ArrowBuffer buffer;
+
+  ArrowBufferInit(&buffer);
+  ASSERT_EQ(ArrowBufferSetAllocator(&buffer, &test_allocator), NANOARROW_OK);
+  ASSERT_EQ(ArrowBufferAppend(&buffer, "1234567", 7), NANOARROW_OK);
+  EXPECT_EQ(buffer.size_bytes, 7);
+  EXPECT_EQ(buffer.capacity_bytes, 7);
+
+  struct ArrowBuffer buffer_out;
+  ArrowBufferMove(&buffer, &buffer_out);
+  EXPECT_EQ(buffer.size_bytes, 0);
+  EXPECT_EQ(buffer.capacity_bytes, 0);
+  EXPECT_EQ(buffer.data, nullptr);
+  EXPECT_EQ(buffer_out.size_bytes, 7);
+  EXPECT_EQ(buffer_out.capacity_bytes, 7);
+
+  ArrowBufferReset(&buffer_out);
+}
+
+TEST(BufferTest, BufferTestResize0) {
+  struct ArrowBuffer buffer;
+
+  ArrowBufferInit(&buffer);
+  ASSERT_EQ(ArrowBufferSetAllocator(&buffer, &test_allocator), NANOARROW_OK);
+  ASSERT_EQ(ArrowBufferAppend(&buffer, "1234567", 7), NANOARROW_OK);
+  EXPECT_EQ(buffer.size_bytes, 7);
+  EXPECT_EQ(buffer.capacity_bytes, 7);
+
+  EXPECT_EQ(ArrowBufferResize(&buffer, 0, false), NANOARROW_OK);
+  EXPECT_EQ(buffer.size_bytes, 0);
+  EXPECT_EQ(buffer.capacity_bytes, 7);
+
+  EXPECT_EQ(ArrowBufferResize(&buffer, 0, true), NANOARROW_OK);
+  EXPECT_EQ(buffer.size_bytes, 0);
+  EXPECT_EQ(buffer.capacity_bytes, 0);
+
+  ArrowBufferReset(&buffer);
+}
+
+TEST(BufferTest, BufferTestError) {
+  struct ArrowBuffer buffer;
+  ArrowBufferInit(&buffer);
+  EXPECT_EQ(ArrowBufferResize(&buffer, std::numeric_limits<int64_t>::max(), false),
+            ENOMEM);
+  EXPECT_EQ(ArrowBufferAppend(&buffer, nullptr, std::numeric_limits<int64_t>::max()),
+            ENOMEM);
+
+  ASSERT_EQ(ArrowBufferAppend(&buffer, "abcd", 4), NANOARROW_OK);
+  EXPECT_EQ(ArrowBufferSetAllocator(&buffer, ArrowBufferAllocatorDefault()), EINVAL);
+
+  EXPECT_EQ(ArrowBufferResize(&buffer, -1, false), EINVAL);
+
+  ArrowBufferReset(&buffer);
+}
diff --git a/src/nanoarrow/error.c b/src/nanoarrow/error.c
new file mode 100644
index 0000000..74539d3
--- /dev/null
+++ b/src/nanoarrow/error.c
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "nanoarrow.h"
+
+int ArrowErrorSet(struct ArrowError* error, const char* fmt, ...) {
+  memset(error->message, 0, sizeof(error->message));
+
+  va_list args;
+  va_start(args, fmt);
+  int chars_needed = vsnprintf(error->message, sizeof(error->message), fmt, args);
+  va_end(args);
+
+  if (chars_needed < 0) {
+    return EINVAL;
+  } else if (chars_needed >= sizeof(error->message)) {
+    return ERANGE;
+  } else {
+    return NANOARROW_OK;
+  }
+}
+
+const char* ArrowErrorMessage(struct ArrowError* error) { return error->message; }
diff --git a/src/nanoarrow/error_test.cc b/src/nanoarrow/error_test.cc
new file mode 100644
index 0000000..0bdd7ce
--- /dev/null
+++ b/src/nanoarrow/error_test.cc
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <cerrno>
+#include <cstring>
+#include <string>
+
+#include <gtest/gtest.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+TEST(ErrorTest, ErrorTestSet) {
+  ArrowError error;
+  EXPECT_EQ(ArrowErrorSet(&error, "there were %d foxes", 4), NANOARROW_OK);
+  EXPECT_STREQ(ArrowErrorMessage(&error), "there were 4 foxes");
+}
+
+TEST(ErrorTest, ErrorTestSetOverrun) {
+  ArrowError error;
+  char big_error[2048];
+  const char* a_few_chars = "abcdefg";
+  for (int i = 0; i < 2047; i++) {
+    big_error[i] = a_few_chars[i % strlen(a_few_chars)];
+  }
+  big_error[2047] = '\0';
+
+  EXPECT_EQ(ArrowErrorSet(&error, "%s", big_error), ERANGE);
+  EXPECT_EQ(std::string(ArrowErrorMessage(&error)), std::string(big_error, 1023));
+
+  wchar_t bad_string[] = {0xFFFF, 0};
+  EXPECT_EQ(ArrowErrorSet(&error, "%ls", bad_string), EINVAL);
+}
diff --git a/src/nanoarrow/metadata.c b/src/nanoarrow/metadata.c
new file mode 100644
index 0000000..123a8d8
--- /dev/null
+++ b/src/nanoarrow/metadata.c
@@ -0,0 +1,121 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "nanoarrow.h"
+
+ArrowErrorCode ArrowMetadataReaderInit(struct ArrowMetadataReader* reader,
+                                       const char* metadata) {
+  reader->metadata = metadata;
+
+  if (reader->metadata == NULL) {
+    reader->offset = 0;
+    reader->remaining_keys = 0;
+  } else {
+    memcpy(&reader->remaining_keys, reader->metadata, sizeof(int32_t));
+    reader->offset = sizeof(int32_t);
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowMetadataReaderRead(struct ArrowMetadataReader* reader,
+                                       struct ArrowStringView* key_out,
+                                       struct ArrowStringView* value_out) {
+  if (reader->remaining_keys <= 0) {
+    return EINVAL;
+  }
+
+  int64_t pos = 0;
+
+  int32_t key_size;
+  memcpy(&key_size, reader->metadata + reader->offset + pos, sizeof(int32_t));
+  pos += sizeof(int32_t);
+
+  key_out->data = reader->metadata + reader->offset + pos;
+  key_out->n_bytes = key_size;
+  pos += key_size;
+
+  int32_t value_size;
+  memcpy(&value_size, reader->metadata + reader->offset + pos, sizeof(int32_t));
+  pos += sizeof(int32_t);
+
+  value_out->data = reader->metadata + reader->offset + pos;
+  value_out->n_bytes = value_size;
+  pos += value_size;
+
+  reader->offset += pos;
+  reader->remaining_keys--;
+  return NANOARROW_OK;
+}
+
+int64_t ArrowMetadataSizeOf(const char* metadata) {
+  if (metadata == NULL) {
+    return 0;
+  }
+
+  struct ArrowMetadataReader reader;
+  struct ArrowStringView key;
+  struct ArrowStringView value;
+  ArrowMetadataReaderInit(&reader, metadata);
+
+  int64_t size = sizeof(int32_t);
+  while (ArrowMetadataReaderRead(&reader, &key, &value) == NANOARROW_OK) {
+    size += sizeof(int32_t) + key.n_bytes + sizeof(int32_t) + value.n_bytes;
+  }
+
+  return size;
+}
+
+ArrowErrorCode ArrowMetadataGetValue(const char* metadata, const char* key,
+                                     const char* default_value,
+                                     struct ArrowStringView* value_out) {
+  struct ArrowStringView target_key_view = {key, strlen(key)};
+  value_out->data = default_value;
+  if (default_value != NULL) {
+    value_out->n_bytes = strlen(default_value);
+  } else {
+    value_out->n_bytes = 0;
+  }
+
+  struct ArrowMetadataReader reader;
+  struct ArrowStringView key_view;
+  struct ArrowStringView value;
+  ArrowMetadataReaderInit(&reader, metadata);
+
+  int64_t size = sizeof(int32_t);
+  while (ArrowMetadataReaderRead(&reader, &key_view, &value) == NANOARROW_OK) {
+    int key_equal = target_key_view.n_bytes == key_view.n_bytes &&
+                    strncmp(target_key_view.data, key_view.data, key_view.n_bytes) == 0;
+    if (key_equal) {
+      value_out->data = value.data;
+      value_out->n_bytes = value.n_bytes;
+      break;
+    }
+  }
+
+  return NANOARROW_OK;
+}
+
+char ArrowMetadataHasKey(const char* metadata, const char* key) {
+  struct ArrowStringView value;
+  ArrowMetadataGetValue(metadata, key, NULL, &value);
+  return value.data != NULL;
+}
diff --git a/src/nanoarrow/metadata_test.cc b/src/nanoarrow/metadata_test.cc
new file mode 100644
index 0000000..5ac959c
--- /dev/null
+++ b/src/nanoarrow/metadata_test.cc
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <arrow/c/bridge.h>
+#include <arrow/testing/gtest_util.h>
+#include <arrow/util/key_value_metadata.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+using namespace arrow;
+
+TEST(SchemaTest, Metadata) {
+  // (test will only work on little endian)
+  char simple_metadata[] = {'\1', '\0', '\0', '\0', '\3', '\0', '\0', '\0', 'k', 'e',
+                            'y',  '\5', '\0', '\0', '\0', 'v',  'a',  'l',  'u', 'e'};
+
+  EXPECT_EQ(ArrowMetadataSizeOf(nullptr), 0);
+  EXPECT_EQ(ArrowMetadataSizeOf(simple_metadata), sizeof(simple_metadata));
+
+  EXPECT_EQ(ArrowMetadataHasKey(simple_metadata, "key"), 1);
+  EXPECT_EQ(ArrowMetadataHasKey(simple_metadata, "not_a_key"), 0);
+
+  struct ArrowStringView value;
+  EXPECT_EQ(ArrowMetadataGetValue(simple_metadata, "key", "default_val", &value),
+            NANOARROW_OK);
+  EXPECT_EQ(std::string(value.data, value.n_bytes), "value");
+  EXPECT_EQ(ArrowMetadataGetValue(simple_metadata, "not_a_key", "default_val", &value),
+            NANOARROW_OK);
+  EXPECT_EQ(std::string(value.data, value.n_bytes), "default_val");
+}
diff --git a/src/nanoarrow/nanoarrow.c b/src/nanoarrow/nanoarrow.c
new file mode 100644
index 0000000..90823aa
--- /dev/null
+++ b/src/nanoarrow/nanoarrow.c
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "allocator.c"
+#include "buffer.c"
+#include "error.c"
+#include "metadata.c"
+#include "schema.c"
+#include "schema_view.c"
diff --git a/src/nanoarrow/nanoarrow.h b/src/nanoarrow/nanoarrow.h
new file mode 100644
index 0000000..e1b8bd2
--- /dev/null
+++ b/src/nanoarrow/nanoarrow.h
@@ -0,0 +1,566 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#ifndef NANOARROW_H_INCLUDED
+#define NANOARROW_H_INCLUDED
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Extra guard for versions of Arrow without the canonical guard
+#ifndef ARROW_FLAG_DICTIONARY_ORDERED
+
+#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
+
+#ifndef ARROW_C_STREAM_INTERFACE
+#define ARROW_C_STREAM_INTERFACE
+
+struct ArrowArrayStream {
+  // Callback to get the stream type
+  // (will be the same for all arrays in the stream).
+  //
+  // Return value: 0 if successful, an `errno`-compatible error code otherwise.
+  //
+  // If successful, the ArrowSchema must be released independently from the stream.
+  int (*get_schema)(struct ArrowArrayStream*, struct ArrowSchema* out);
+
+  // Callback to get the next array
+  // (if no error and the array is released, the stream has ended)
+  //
+  // Return value: 0 if successful, an `errno`-compatible error code otherwise.
+  //
+  // If successful, the ArrowArray must be released independently from the stream.
+  int (*get_next)(struct ArrowArrayStream*, struct ArrowArray* out);
+
+  // Callback to get optional detailed error information.
+  // This must only be called if the last stream operation failed
+  // with a non-0 return code.
+  //
+  // Return value: pointer to a null-terminated character array describing
+  // the last error, or NULL if no description is available.
+  //
+  // The returned pointer is only valid until the next operation on this stream
+  // (including release).
+  const char* (*get_last_error)(struct ArrowArrayStream*);
+
+  // Release callback: release the stream's own resources.
+  // Note that arrays returned by `get_next` must be individually released.
+  void (*release)(struct ArrowArrayStream*);
+
+  // Opaque producer-specific data
+  void* private_data;
+};
+
+#endif  // ARROW_C_STREAM_INTERFACE
+#endif  // ARROW_FLAG_DICTIONARY_ORDERED
+
+/// \file Arrow C Implementation
+///
+/// EXPERIMENTAL. Interface subject to change.
+
+/// \page object-model Object Model
+///
+/// Except where noted, objects are not thread-safe and clients should
+/// take care to serialize accesses to methods.
+///
+/// Because this library is intended to be vendored, it provides full type
+/// definitions and encourages clients to stack or statically allocate
+/// where convenient.
+
+/// \defgroup nanoarrow-malloc Memory management
+///
+/// Non-buffer members of a struct ArrowSchema and struct ArrowArray
+/// must be allocated using ArrowMalloc() or ArrowRealloc() and freed
+/// using ArrowFree for schemas and arrays allocated here. Buffer members
+/// are allocated using an ArrowBufferAllocator.
+
+/// \brief Allocate like malloc()
+void* ArrowMalloc(int64_t size);
+
+/// \brief Reallocate like realloc()
+void* ArrowRealloc(void* ptr, int64_t size);
+
+/// \brief Free a pointer allocated using ArrowMalloc() or ArrowRealloc().
+void ArrowFree(void* ptr);
+
+/// \brief Array buffer allocation and deallocation
+///
+/// Container for allocate, reallocate, and free methods that can be used
+/// to customize allocation and deallocation of buffers when constructing
+/// an ArrowArray.
+struct ArrowBufferAllocator {
+  /// \brief Allocate a buffer or return NULL if it cannot be allocated
+  uint8_t* (*allocate)(struct ArrowBufferAllocator* allocator, int64_t size);
+
+  /// \brief Reallocate a buffer or return NULL if it cannot be reallocated
+  uint8_t* (*reallocate)(struct ArrowBufferAllocator* allocator, uint8_t* ptr,
+                         int64_t old_size, int64_t new_size);
+
+  /// \brief Deallocate a buffer allocated by this allocator
+  void (*free)(struct ArrowBufferAllocator* allocator, uint8_t* ptr, int64_t size);
+
+  /// \brief Opaque data specific to the allocator
+  void* private_data;
+};
+
+/// \brief Return the default allocator
+///
+/// The default allocator uses ArrowMalloc(), ArrowRealloc(), and
+/// ArrowFree().
+struct ArrowBufferAllocator* ArrowBufferAllocatorDefault();
+
+/// }@
+
+/// \defgroup nanoarrow-errors Error handling primitives
+/// Functions generally return an errno-compatible error code; functions that
+/// need to communicate more verbose error information accept a pointer
+/// to an ArrowError. This can be stack or statically allocated. The
+/// content of the message is undefined unless an error code has been
+/// returned.
+
+/// \brief Error type containing a UTF-8 encoded message.
+struct ArrowError {
+  char message[1024];
+};
+
+/// \brief Return code for success.
+#define NANOARROW_OK 0
+
+/// \brief Represents an errno-compatible error code
+typedef int ArrowErrorCode;
+
+/// \brief Set the contents of an error using printf syntax
+ArrowErrorCode ArrowErrorSet(struct ArrowError* error, const char* fmt, ...);
+
+/// \brief Get the contents of an error
+const char* ArrowErrorMessage(struct ArrowError* error);
+
+/// }@
+
+/// \defgroup nanoarrow-utils Utility data structures
+
+/// \brief An non-owning view of a string
+struct ArrowStringView {
+  /// \brief A pointer to the start of the string
+  ///
+  /// If n_bytes is 0, this value may be NULL.
+  const char* data;
+
+  /// \brief The size of the string in bytes,
+  ///
+  /// (Not including the null terminator.)
+  int64_t n_bytes;
+};
+
+/// \brief Arrow type enumerator
+///
+/// These names are intended to map to the corresponding arrow::Type::type
+/// enumerator; however, the numeric values are specifically not equal
+/// (i.e., do not rely on numeric comparison).
+enum ArrowType {
+  NANOARROW_TYPE_UNINITIALIZED = 0,
+  NANOARROW_TYPE_NA = 1,
+  NANOARROW_TYPE_BOOL,
+  NANOARROW_TYPE_UINT8,
+  NANOARROW_TYPE_INT8,
+  NANOARROW_TYPE_UINT16,
+  NANOARROW_TYPE_INT16,
+  NANOARROW_TYPE_UINT32,
+  NANOARROW_TYPE_INT32,
+  NANOARROW_TYPE_UINT64,
+  NANOARROW_TYPE_INT64,
+  NANOARROW_TYPE_HALF_FLOAT,
+  NANOARROW_TYPE_FLOAT,
+  NANOARROW_TYPE_DOUBLE,
+  NANOARROW_TYPE_STRING,
+  NANOARROW_TYPE_BINARY,
+  NANOARROW_TYPE_FIXED_SIZE_BINARY,
+  NANOARROW_TYPE_DATE32,
+  NANOARROW_TYPE_DATE64,
+  NANOARROW_TYPE_TIMESTAMP,
+  NANOARROW_TYPE_TIME32,
+  NANOARROW_TYPE_TIME64,
+  NANOARROW_TYPE_INTERVAL_MONTHS,
+  NANOARROW_TYPE_INTERVAL_DAY_TIME,
+  NANOARROW_TYPE_DECIMAL128,
+  NANOARROW_TYPE_DECIMAL256,
+  NANOARROW_TYPE_LIST,
+  NANOARROW_TYPE_STRUCT,
+  NANOARROW_TYPE_SPARSE_UNION,
+  NANOARROW_TYPE_DENSE_UNION,
+  NANOARROW_TYPE_DICTIONARY,
+  NANOARROW_TYPE_MAP,
+  NANOARROW_TYPE_EXTENSION,
+  NANOARROW_TYPE_FIXED_SIZE_LIST,
+  NANOARROW_TYPE_DURATION,
+  NANOARROW_TYPE_LARGE_STRING,
+  NANOARROW_TYPE_LARGE_BINARY,
+  NANOARROW_TYPE_LARGE_LIST,
+  NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO
+};
+
+/// \brief Arrow time unit enumerator
+///
+/// These names and values map to the corresponding arrow::TimeUnit::type
+/// enumerator.
+enum ArrowTimeUnit {
+  NANOARROW_TIME_UNIT_SECOND = 0,
+  NANOARROW_TIME_UNIT_MILLI = 1,
+  NANOARROW_TIME_UNIT_MICRO = 2,
+  NANOARROW_TIME_UNIT_NANO = 3
+};
+
+/// }@
+
+/// \defgroup nanoarrow-schema Schema producer helpers
+/// These functions allocate, copy, and destroy ArrowSchema structures
+
+/// \brief Initialize the fields of a schema
+///
+/// Initializes the fields and release callback of schema_out. Caller
+/// is responsible for calling the schema->release callback if
+/// NANOARROW_OK is returned.
+ArrowErrorCode ArrowSchemaInit(struct ArrowSchema* schema, enum ArrowType type);
+
+/// \brief Initialize the fields of a fixed-size schema
+///
+/// Returns EINVAL for fixed_size <= 0 or for data_type that is not
+/// NANOARROW_TYPE_FIXED_SIZE_BINARY or NANOARROW_TYPE_FIXED_SIZE_LIST.
+ArrowErrorCode ArrowSchemaInitFixedSize(struct ArrowSchema* schema,
+                                        enum ArrowType data_type, int32_t fixed_size);
+
+/// \brief Initialize the fields of a decimal schema
+///
+/// Returns EINVAL for scale <= 0 or for data_type that is not
+/// NANOARROW_TYPE_DECIMAL128 or NANOARROW_TYPE_DECIMAL256.
+ArrowErrorCode ArrowSchemaInitDecimal(struct ArrowSchema* schema,
+                                      enum ArrowType data_type, int32_t decimal_precision,
+                                      int32_t decimal_scale);
+
+/// \brief Initialize the fields of a time, timestamp, or duration schema
+///
+/// Returns EINVAL for data_type that is not
+/// NANOARROW_TYPE_TIME32, NANOARROW_TYPE_TIME64,
+/// NANOARROW_TYPE_TIMESTAMP, or NANOARROW_TYPE_DURATION. The
+/// timezone parameter must be NULL for a non-timestamp data_type.
+ArrowErrorCode ArrowSchemaInitDateTime(struct ArrowSchema* schema,
+                                       enum ArrowType data_type,
+                                       enum ArrowTimeUnit time_unit,
+                                       const char* timezone);
+
+/// \brief Make a (recursive) copy of a schema
+///
+/// Allocates and copies fields of schema into schema_out.
+ArrowErrorCode ArrowSchemaDeepCopy(struct ArrowSchema* schema,
+                                   struct ArrowSchema* schema_out);
+
+/// \brief Copy format into schema->format
+///
+/// schema must have been allocated using ArrowSchemaInit or
+/// ArrowSchemaDeepCopy.
+ArrowErrorCode ArrowSchemaSetFormat(struct ArrowSchema* schema, const char* format);
+
+/// \brief Copy name into schema->name
+///
+/// schema must have been allocated using ArrowSchemaInit or
+/// ArrowSchemaDeepCopy.
+ArrowErrorCode ArrowSchemaSetName(struct ArrowSchema* schema, const char* name);
+
+/// \brief Copy metadata into schema->metadata
+///
+/// schema must have been allocated using ArrowSchemaInit or
+/// ArrowSchemaDeepCopy.
+ArrowErrorCode ArrowSchemaSetMetadata(struct ArrowSchema* schema, const char* metadata);
+
+/// \brief Allocate the schema->children array
+///
+/// Includes the memory for each child struct ArrowSchema.
+/// schema must have been allocated using ArrowSchemaInit or
+/// ArrowSchemaDeepCopy.
+ArrowErrorCode ArrowSchemaAllocateChildren(struct ArrowSchema* schema,
+                                           int64_t n_children);
+
+/// \brief Allocate the schema->dictionary member
+///
+/// schema must have been allocated using ArrowSchemaInit or
+/// ArrowSchemaDeepCopy.
+ArrowErrorCode ArrowSchemaAllocateDictionary(struct ArrowSchema* schema);
+
+/// \brief Reader for key/value pairs in schema metadata
+struct ArrowMetadataReader {
+  const char* metadata;
+  int64_t offset;
+  int32_t remaining_keys;
+};
+
+/// \brief Initialize an ArrowMetadataReader
+ArrowErrorCode ArrowMetadataReaderInit(struct ArrowMetadataReader* reader,
+                                       const char* metadata);
+
+/// \brief Read the next key/value pair from an ArrowMetadataReader
+ArrowErrorCode ArrowMetadataReaderRead(struct ArrowMetadataReader* reader,
+                                       struct ArrowStringView* key_out,
+                                       struct ArrowStringView* value_out);
+
+/// \brief The number of bytes in in a key/value metadata string
+int64_t ArrowMetadataSizeOf(const char* metadata);
+
+/// \brief Check for a key in schema metadata
+char ArrowMetadataHasKey(const char* metadata, const char* key);
+
+/// \brief Extract a value from schema metadata
+ArrowErrorCode ArrowMetadataGetValue(const char* metadata, const char* key,
+                                     const char* default_value,
+                                     struct ArrowStringView* value_out);
+
+/// }@
+
+/// \defgroup nanoarrow-schema-view Schema consumer helpers
+
+/// \brief A non-owning view of a parsed ArrowSchema
+///
+/// Contains more readily extractable values than a raw ArrowSchema.
+/// Clients can stack or statically allocate this structure but are
+/// encouraged to use the provided getters to ensure forward
+/// compatiblity.
+struct ArrowSchemaView {
+  /// \brief A pointer to the schema represented by this view
+  struct ArrowSchema* schema;
+
+  /// \brief The data type represented by the schema
+  ///
+  /// This value may be NANOARROW_TYPE_DICTIONARY if the schema has a
+  /// non-null dictionary member; datetime types are valid values.
+  /// This value will never be NANOARROW_TYPE_EXTENSION (see
+  /// extension_name and/or extension_metadata to check for
+  /// an extension type).
+  enum ArrowType data_type;
+
+  /// \brief The storage data type represented by the schema
+  ///
+  /// This value will never be NANOARROW_TYPE_DICTIONARY, NANOARROW_TYPE_EXTENSION
+  /// or any datetime type. This value represents only the type required to
+  /// interpret the buffers in the array.
+  enum ArrowType storage_data_type;
+
+  /// \brief The extension type name if it exists
+  ///
+  /// If the ARROW:extension:name key is present in schema.metadata,
+  /// extension_name.data will be non-NULL.
+  struct ArrowStringView extension_name;
+
+  /// \brief The extension type metadata if it exists
+  ///
+  /// If the ARROW:extension:metadata key is present in schema.metadata,
+  /// extension_metadata.data will be non-NULL.
+  struct ArrowStringView extension_metadata;
+
+  /// \brief The expected number of buffers in a paired ArrowArray
+  int32_t n_buffers;
+
+  /// \brief The index of the validity buffer or -1 if one does not exist
+  int32_t validity_buffer_id;
+
+  /// \brief The index of the offset buffer or -1 if one does not exist
+  int32_t offset_buffer_id;
+
+  /// \brief The index of the data buffer or -1 if one does not exist
+  int32_t data_buffer_id;
+
+  /// \brief The index of the type_ids buffer or -1 if one does not exist
+  int32_t type_id_buffer_id;
+
+  /// \brief Format fixed size parameter
+  ///
+  /// This value is set when parsing a fixed-size binary or fixed-size
+  /// list schema; this value is undefined for other types. For a
+  /// fixed-size binary schema this value is in bytes; for a fixed-size
+  /// list schema this value refers to the number of child elements for
+  /// each element of the parent.
+  int32_t fixed_size;
+
+  /// \brief Decimal bitwidth
+  ///
+  /// This value is set when parsing a decimal type schema;
+  /// this value is undefined for other types.
+  int32_t decimal_bitwidth;
+
+  /// \brief Decimal precision
+  ///
+  /// This value is set when parsing a decimal type schema;
+  /// this value is undefined for other types.
+  int32_t decimal_precision;
+
+  /// \brief Decimal scale
+  ///
+  /// This value is set when parsing a decimal type schema;
+  /// this value is undefined for other types.
+  int32_t decimal_scale;
+
+  /// \brief Format time unit parameter
+  ///
+  /// This value is set when parsing a date/time type. The value is
+  /// undefined for other types.
+  enum ArrowTimeUnit time_unit;
+
+  /// \brief Format timezone parameter
+  ///
+  /// This value is set when parsing a timestamp type and represents
+  /// the timezone format parameter. The ArrowStrintgView points to
+  /// data within the schema and the value is undefined for other types.
+  struct ArrowStringView timezone;
+
+  /// \brief Union type ids parameter
+  ///
+  /// This value is set when parsing a union type and represents
+  /// type ids parameter. The ArrowStringView points to
+  /// data within the schema and the value is undefined for other types.
+  struct ArrowStringView union_type_ids;
+};
+
+/// \brief Initialize an ArrowSchemaView
+ArrowErrorCode ArrowSchemaViewInit(struct ArrowSchemaView* schema_view,
+                                   struct ArrowSchema* schema, struct ArrowError* error);
+
+/// }@
+
+/// \defgroup nanoarrow-buffer-builder Growable buffer builders
+
+/// \brief An owning mutable view of a buffer
+struct ArrowBuffer {
+  /// \brief A pointer to the start of the buffer
+  ///
+  /// If capacity_bytes is 0, this value may be NULL.
+  uint8_t* data;
+
+  /// \brief The size of the buffer in bytes
+  int64_t size_bytes;
+
+  /// \brief The capacity of the buffer in bytes
+  int64_t capacity_bytes;
+
+  /// \brief The allocator that will be used to reallocate and/or free the buffer
+  struct ArrowBufferAllocator* allocator;
+};
+
+/// \brief Initialize an ArrowBuffer
+///
+/// Initialize a buffer with a NULL, zero-size buffer using the default
+/// buffer allocator.
+void ArrowBufferInit(struct ArrowBuffer* buffer);
+
+/// \brief Set a newly-initialized buffer's allocator
+///
+/// Returns EINVAL if the buffer has already been allocated.
+ArrowErrorCode ArrowBufferSetAllocator(struct ArrowBuffer* buffer,
+                                       struct ArrowBufferAllocator* allocator);
+
+/// \brief Reset an ArrowBuffer
+///
+/// Releases the buffer using the allocator's free method if
+/// the buffer's data member is non-null, sets the data member
+/// to NULL, and sets the buffer's size and capacity to 0.
+void ArrowBufferReset(struct ArrowBuffer* buffer);
+
+/// \brief Move an ArrowBuffer
+///
+/// Transfers the buffer data and lifecycle management to another
+/// address and resets buffer.
+void ArrowBufferMove(struct ArrowBuffer* buffer, struct ArrowBuffer* buffer_out);
+
+/// \brief Grow or shrink a buffer to a given capacity
+///
+/// When shrinking the capacity of the buffer, the buffer is only reallocated
+/// if shrink_to_fit is non-zero. Calling ArrowBufferResize() does not
+/// adjust the buffer's size member except to ensure that the invariant
+/// capacity >= size remains true.
+ArrowErrorCode ArrowBufferResize(struct ArrowBuffer* buffer, int64_t new_capacity_bytes,
+                                 char shrink_to_fit);
+
+/// \brief Ensure a buffer has at least a given additional capacity
+///
+/// Ensures that the buffer has space to append at least
+/// additional_size_bytes, overallocating when required.
+ArrowErrorCode ArrowBufferReserve(struct ArrowBuffer* buffer,
+                                  int64_t additional_size_bytes);
+
+/// \brief Write data to buffer and increment the buffer size
+///
+/// This function does not check that buffer has the required capacity
+void ArrowBufferAppendUnsafe(struct ArrowBuffer* buffer, const void* data,
+                             int64_t size_bytes);
+
+/// \brief Write data to buffer and increment the buffer size
+///
+/// This function writes and ensures that the buffer has the required capacity,
+/// possibly by reallocating the buffer. Like ArrowBufferReserve, this will
+/// overallocate when reallocation is required.
+ArrowErrorCode ArrowBufferAppend(struct ArrowBuffer* buffer, const void* data,
+                                 int64_t size_bytes);
+
+/// }@
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/nanoarrow/schema.c b/src/nanoarrow/schema.c
new file mode 100644
index 0000000..c4220d9
--- /dev/null
+++ b/src/nanoarrow/schema.c
@@ -0,0 +1,475 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "nanoarrow.h"
+
+void ArrowSchemaRelease(struct ArrowSchema* schema) {
+  if (schema->format != NULL) ArrowFree((void*)schema->format);
+  if (schema->name != NULL) ArrowFree((void*)schema->name);
+  if (schema->metadata != NULL) ArrowFree((void*)schema->metadata);
+
+  // This object owns the memory for all the children, but those
+  // children may have been generated elsewhere and might have
+  // their own release() callback.
+  if (schema->children != NULL) {
+    for (int64_t i = 0; i < schema->n_children; i++) {
+      if (schema->children[i] != NULL) {
+        if (schema->children[i]->release != NULL) {
+          schema->children[i]->release(schema->children[i]);
+        }
+
+        ArrowFree(schema->children[i]);
+      }
+    }
+
+    ArrowFree(schema->children);
+  }
+
+  // This object owns the memory for the dictionary but it
+  // may have been generated somewhere else and have its own
+  // release() callback.
+  if (schema->dictionary != NULL) {
+    if (schema->dictionary->release != NULL) {
+      schema->dictionary->release(schema->dictionary);
+    }
+
+    ArrowFree(schema->dictionary);
+  }
+
+  // private data not currently used
+  if (schema->private_data != NULL) {
+    ArrowFree(schema->private_data);
+  }
+
+  schema->release = NULL;
+}
+
+const char* ArrowSchemaFormatTemplate(enum ArrowType data_type) {
+  switch (data_type) {
+    case NANOARROW_TYPE_UNINITIALIZED:
+      return NULL;
+    case NANOARROW_TYPE_NA:
+      return "n";
+    case NANOARROW_TYPE_BOOL:
+      return "b";
+
+    case NANOARROW_TYPE_UINT8:
+      return "C";
+    case NANOARROW_TYPE_INT8:
+      return "c";
+    case NANOARROW_TYPE_UINT16:
+      return "S";
+    case NANOARROW_TYPE_INT16:
+      return "s";
+    case NANOARROW_TYPE_UINT32:
+      return "I";
+    case NANOARROW_TYPE_INT32:
+      return "i";
+    case NANOARROW_TYPE_UINT64:
+      return "L";
+    case NANOARROW_TYPE_INT64:
+      return "l";
+
+    case NANOARROW_TYPE_HALF_FLOAT:
+      return "e";
+    case NANOARROW_TYPE_FLOAT:
+      return "f";
+    case NANOARROW_TYPE_DOUBLE:
+      return "g";
+
+    case NANOARROW_TYPE_STRING:
+      return "u";
+    case NANOARROW_TYPE_LARGE_STRING:
+      return "U";
+    case NANOARROW_TYPE_BINARY:
+      return "z";
+    case NANOARROW_TYPE_LARGE_BINARY:
+      return "Z";
+
+    case NANOARROW_TYPE_DATE32:
+      return "tdD";
+    case NANOARROW_TYPE_DATE64:
+      return "tdm";
+    case NANOARROW_TYPE_INTERVAL_MONTHS:
+      return "tiM";
+    case NANOARROW_TYPE_INTERVAL_DAY_TIME:
+      return "tiD";
+    case NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO:
+      return "tin";
+
+    case NANOARROW_TYPE_LIST:
+      return "+l";
+    case NANOARROW_TYPE_LARGE_LIST:
+      return "+L";
+    case NANOARROW_TYPE_STRUCT:
+      return "+s";
+    case NANOARROW_TYPE_MAP:
+      return "+m";
+
+    default:
+      return NULL;
+  }
+}
+
+ArrowErrorCode ArrowSchemaInit(struct ArrowSchema* schema, enum ArrowType data_type) {
+  schema->format = NULL;
+  schema->name = NULL;
+  schema->metadata = NULL;
+  schema->flags = ARROW_FLAG_NULLABLE;
+  schema->n_children = 0;
+  schema->children = NULL;
+  schema->dictionary = NULL;
+  schema->private_data = NULL;
+  schema->release = &ArrowSchemaRelease;
+
+  // We don't allocate the dictionary because it has to be nullptr
+  // for non-dictionary-encoded arrays.
+
+  // Set the format to a valid format string for data_type
+  const char* template_format = ArrowSchemaFormatTemplate(data_type);
+
+  // If data_type isn't recognized and not explicitly unset
+  if (template_format == NULL && data_type != NANOARROW_TYPE_UNINITIALIZED) {
+    schema->release(schema);
+    return EINVAL;
+  }
+
+  int result = ArrowSchemaSetFormat(schema, template_format);
+  if (result != NANOARROW_OK) {
+    schema->release(schema);
+    return result;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaInitFixedSize(struct ArrowSchema* schema,
+                                        enum ArrowType data_type, int32_t fixed_size) {
+  int result = ArrowSchemaInit(schema, NANOARROW_TYPE_UNINITIALIZED);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  if (fixed_size <= 0) {
+    schema->release(schema);
+    return EINVAL;
+  }
+
+  char buffer[64];
+  int n_chars;
+  switch (data_type) {
+    case NANOARROW_TYPE_FIXED_SIZE_BINARY:
+      n_chars = snprintf(buffer, sizeof(buffer), "w:%d", (int)fixed_size);
+      break;
+    case NANOARROW_TYPE_FIXED_SIZE_LIST:
+      n_chars = snprintf(buffer, sizeof(buffer), "+w:%d", (int)fixed_size);
+      break;
+    default:
+      schema->release(schema);
+      return EINVAL;
+  }
+
+  buffer[n_chars] = '\0';
+  result = ArrowSchemaSetFormat(schema, buffer);
+  if (result != NANOARROW_OK) {
+    schema->release(schema);
+  }
+
+  return result;
+}
+
+ArrowErrorCode ArrowSchemaInitDecimal(struct ArrowSchema* schema,
+                                      enum ArrowType data_type, int32_t decimal_precision,
+                                      int32_t decimal_scale) {
+  int result = ArrowSchemaInit(schema, NANOARROW_TYPE_UNINITIALIZED);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  if (decimal_precision <= 0) {
+    schema->release(schema);
+    return EINVAL;
+  }
+
+  char buffer[64];
+  int n_chars;
+  switch (data_type) {
+    case NANOARROW_TYPE_DECIMAL128:
+      n_chars =
+          snprintf(buffer, sizeof(buffer), "d:%d,%d", decimal_precision, decimal_scale);
+      break;
+    case NANOARROW_TYPE_DECIMAL256:
+      n_chars = snprintf(buffer, sizeof(buffer), "d:%d,%d,256", decimal_precision,
+                         decimal_scale);
+      break;
+    default:
+      schema->release(schema);
+      return EINVAL;
+  }
+
+  buffer[n_chars] = '\0';
+
+  result = ArrowSchemaSetFormat(schema, buffer);
+  if (result != NANOARROW_OK) {
+    schema->release(schema);
+    return result;
+  }
+
+  return NANOARROW_OK;
+}
+
+static const char* ArrowTimeUnitString(enum ArrowTimeUnit time_unit) {
+  switch (time_unit) {
+    case NANOARROW_TIME_UNIT_SECOND:
+      return "s";
+    case NANOARROW_TIME_UNIT_MILLI:
+      return "m";
+    case NANOARROW_TIME_UNIT_MICRO:
+      return "u";
+    case NANOARROW_TIME_UNIT_NANO:
+      return "n";
+    default:
+      return NULL;
+  }
+}
+
+ArrowErrorCode ArrowSchemaInitDateTime(struct ArrowSchema* schema,
+                                       enum ArrowType data_type,
+                                       enum ArrowTimeUnit time_unit,
+                                       const char* timezone) {
+  int result = ArrowSchemaInit(schema, NANOARROW_TYPE_UNINITIALIZED);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  const char* time_unit_str = ArrowTimeUnitString(time_unit);
+  if (time_unit_str == NULL) {
+    schema->release(schema);
+    return EINVAL;
+  }
+
+  char buffer[128];
+  int n_chars;
+  switch (data_type) {
+    case NANOARROW_TYPE_TIME32:
+    case NANOARROW_TYPE_TIME64:
+      if (timezone != NULL) {
+        schema->release(schema);
+        return EINVAL;
+      }
+      n_chars = snprintf(buffer, sizeof(buffer), "tt%s", time_unit_str);
+      break;
+    case NANOARROW_TYPE_TIMESTAMP:
+      if (timezone == NULL) {
+        timezone = "";
+      }
+      n_chars = snprintf(buffer, sizeof(buffer), "ts%s:%s", time_unit_str, timezone);
+      break;
+    case NANOARROW_TYPE_DURATION:
+      if (timezone != NULL) {
+        schema->release(schema);
+        return EINVAL;
+      }
+      n_chars = snprintf(buffer, sizeof(buffer), "tD%s", time_unit_str);
+      break;
+    default:
+      schema->release(schema);
+      return EINVAL;
+  }
+
+  if (n_chars >= sizeof(buffer)) {
+    schema->release(schema);
+    return ERANGE;
+  }
+
+  buffer[n_chars] = '\0';
+
+  result = ArrowSchemaSetFormat(schema, buffer);
+  if (result != NANOARROW_OK) {
+    schema->release(schema);
+    return result;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaSetFormat(struct ArrowSchema* schema, const char* format) {
+  if (schema->format != NULL) {
+    ArrowFree((void*)schema->format);
+  }
+
+  if (format != NULL) {
+    size_t format_size = strlen(format) + 1;
+    schema->format = (const char*)ArrowMalloc(format_size);
+    if (schema->format == NULL) {
+      return ENOMEM;
+    }
+
+    memcpy((void*)schema->format, format, format_size);
+  } else {
+    schema->format = NULL;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaSetName(struct ArrowSchema* schema, const char* name) {
+  if (schema->name != NULL) {
+    ArrowFree((void*)schema->name);
+  }
+
+  if (name != NULL) {
+    size_t name_size = strlen(name) + 1;
+    schema->name = (const char*)ArrowMalloc(name_size);
+    if (schema->name == NULL) {
+      return ENOMEM;
+    }
+
+    memcpy((void*)schema->name, name, name_size);
+  } else {
+    schema->name = NULL;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaSetMetadata(struct ArrowSchema* schema, const char* metadata) {
+  if (schema->metadata != NULL) {
+    ArrowFree((void*)schema->metadata);
+  }
+
+  if (metadata != NULL) {
+    size_t metadata_size = ArrowMetadataSizeOf(metadata);
+    schema->metadata = (const char*)ArrowMalloc(metadata_size);
+    if (schema->metadata == NULL) {
+      return ENOMEM;
+    }
+
+    memcpy((void*)schema->metadata, metadata, metadata_size);
+  } else {
+    schema->metadata = NULL;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaAllocateChildren(struct ArrowSchema* schema,
+                                           int64_t n_children) {
+  if (schema->children != NULL) {
+    return EEXIST;
+  }
+
+  if (n_children > 0) {
+    schema->children =
+        (struct ArrowSchema**)ArrowMalloc(n_children * sizeof(struct ArrowSchema*));
+
+    if (schema->children == NULL) {
+      return ENOMEM;
+    }
+
+    schema->n_children = n_children;
+
+    memset(schema->children, 0, n_children * sizeof(struct ArrowSchema*));
+
+    for (int64_t i = 0; i < n_children; i++) {
+      schema->children[i] = (struct ArrowSchema*)ArrowMalloc(sizeof(struct ArrowSchema));
+
+      if (schema->children[i] == NULL) {
+        return ENOMEM;
+      }
+
+      schema->children[i]->release = NULL;
+    }
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaAllocateDictionary(struct ArrowSchema* schema) {
+  if (schema->dictionary != NULL) {
+    return EEXIST;
+  }
+
+  schema->dictionary = (struct ArrowSchema*)ArrowMalloc(sizeof(struct ArrowSchema));
+  if (schema->dictionary == NULL) {
+    return ENOMEM;
+  }
+
+  schema->dictionary->release = NULL;
+  return NANOARROW_OK;
+}
+
+int ArrowSchemaDeepCopy(struct ArrowSchema* schema, struct ArrowSchema* schema_out) {
+  int result;
+  result = ArrowSchemaInit(schema_out, NANOARROW_TYPE_NA);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  result = ArrowSchemaSetFormat(schema_out, schema->format);
+  if (result != NANOARROW_OK) {
+    schema_out->release(schema_out);
+    return result;
+  }
+
+  result = ArrowSchemaSetName(schema_out, schema->name);
+  if (result != NANOARROW_OK) {
+    schema_out->release(schema_out);
+    return result;
+  }
+
+  result = ArrowSchemaSetMetadata(schema_out, schema->metadata);
+  if (result != NANOARROW_OK) {
+    schema_out->release(schema_out);
+    return result;
+  }
+
+  result = ArrowSchemaAllocateChildren(schema_out, schema->n_children);
+  if (result != NANOARROW_OK) {
+    schema_out->release(schema_out);
+    return result;
+  }
+
+  for (int64_t i = 0; i < schema->n_children; i++) {
+    result = ArrowSchemaDeepCopy(schema->children[i], schema_out->children[i]);
+    if (result != NANOARROW_OK) {
+      schema_out->release(schema_out);
+      return result;
+    }
+  }
+
+  if (schema->dictionary != NULL) {
+    result = ArrowSchemaAllocateDictionary(schema_out);
+    if (result != NANOARROW_OK) {
+      schema_out->release(schema_out);
+      return result;
+    }
+
+    result = ArrowSchemaDeepCopy(schema->dictionary, schema_out->dictionary);
+    if (result != NANOARROW_OK) {
+      schema_out->release(schema_out);
+      return result;
+    }
+  }
+
+  return NANOARROW_OK;
+}
diff --git a/src/nanoarrow/schema_test.cc b/src/nanoarrow/schema_test.cc
new file mode 100644
index 0000000..e925638
--- /dev/null
+++ b/src/nanoarrow/schema_test.cc
@@ -0,0 +1,417 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <arrow/c/bridge.h>
+#include <arrow/testing/gtest_util.h>
+#include <arrow/util/key_value_metadata.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+using namespace arrow;
+
+TEST(SchemaTest, SchemaInit) {
+  struct ArrowSchema schema;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 2), NANOARROW_OK);
+
+  ASSERT_NE(schema.release, nullptr);
+  EXPECT_EQ(schema.format, nullptr);
+  EXPECT_EQ(schema.name, nullptr);
+  EXPECT_EQ(schema.metadata, nullptr);
+  EXPECT_EQ(schema.n_children, 2);
+  EXPECT_EQ(schema.children[0]->release, nullptr);
+  EXPECT_EQ(schema.children[1]->release, nullptr);
+
+  schema.release(&schema);
+  EXPECT_EQ(schema.release, nullptr);
+}
+
+static void ExpectSchemaInitOk(enum ArrowType data_type,
+                               std::shared_ptr<DataType> expected_arrow_type) {
+  struct ArrowSchema schema;
+  EXPECT_EQ(ArrowSchemaInit(&schema, data_type), NANOARROW_OK);
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(expected_arrow_type));
+}
+
+TEST(SchemaTest, SchemaInitSimple) {
+  ExpectSchemaInitOk(NANOARROW_TYPE_NA, null());
+  ExpectSchemaInitOk(NANOARROW_TYPE_BOOL, boolean());
+  ExpectSchemaInitOk(NANOARROW_TYPE_UINT8, uint8());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INT8, int8());
+  ExpectSchemaInitOk(NANOARROW_TYPE_UINT16, uint16());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INT16, int16());
+  ExpectSchemaInitOk(NANOARROW_TYPE_UINT32, uint32());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INT32, int32());
+  ExpectSchemaInitOk(NANOARROW_TYPE_UINT64, uint64());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INT64, int64());
+  ExpectSchemaInitOk(NANOARROW_TYPE_HALF_FLOAT, float16());
+  ExpectSchemaInitOk(NANOARROW_TYPE_FLOAT, float32());
+  ExpectSchemaInitOk(NANOARROW_TYPE_DOUBLE, float64());
+  ExpectSchemaInitOk(NANOARROW_TYPE_STRING, utf8());
+  ExpectSchemaInitOk(NANOARROW_TYPE_LARGE_STRING, large_utf8());
+  ExpectSchemaInitOk(NANOARROW_TYPE_BINARY, binary());
+  ExpectSchemaInitOk(NANOARROW_TYPE_LARGE_BINARY, large_binary());
+  ExpectSchemaInitOk(NANOARROW_TYPE_DATE32, date32());
+  ExpectSchemaInitOk(NANOARROW_TYPE_DATE64, date64());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INTERVAL_MONTHS, month_interval());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INTERVAL_DAY_TIME, day_time_interval());
+  ExpectSchemaInitOk(NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO, month_day_nano_interval());
+}
+
+TEST(SchemaTest, SchemaInitSimpleError) {
+  struct ArrowSchema schema;
+  EXPECT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_DECIMAL128), EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+}
+
+TEST(SchemaTest, SchemaTestInitNestedList) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_LIST), NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "+l");
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_INT32), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0], "item"), NANOARROW_OK);
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(list(int32())));
+
+  EXPECT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_LARGE_LIST), NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "+L");
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_INT32), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0], "item"), NANOARROW_OK);
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(large_list(int32())));
+}
+
+TEST(SchemaTest, SchemaTestInitNestedStruct) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_STRUCT), NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "+s");
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_INT32), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0], "item"), NANOARROW_OK);
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(struct_({field("item", int32())})));
+}
+
+TEST(SchemaTest, SchemaTestInitNestedMap) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_MAP), NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "+m");
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_STRUCT), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0], "entries"), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(schema.children[0], 2), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0]->children[0], NANOARROW_TYPE_INT32),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0]->children[0], "key"), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0]->children[1], NANOARROW_TYPE_STRING),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0]->children[1], "value"), NANOARROW_OK);
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(map(int32(), utf8())));
+}
+
+TEST(SchemaTest, SchemaInitFixedSize) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInitFixedSize(&schema, NANOARROW_TYPE_DOUBLE, 1), EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+  EXPECT_EQ(ArrowSchemaInitFixedSize(&schema, NANOARROW_TYPE_FIXED_SIZE_BINARY, 0),
+            EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitFixedSize(&schema, NANOARROW_TYPE_FIXED_SIZE_BINARY, 45),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "w:45");
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(fixed_size_binary(45)));
+
+  EXPECT_EQ(ArrowSchemaInitFixedSize(&schema, NANOARROW_TYPE_FIXED_SIZE_LIST, 12),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "+w:12");
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_INT32), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetName(schema.children[0], "item"), NANOARROW_OK);
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(fixed_size_list(int32(), 12)));
+}
+
+TEST(SchemaTest, SchemaInitDecimal) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInitDecimal(&schema, NANOARROW_TYPE_DECIMAL128, -1, 1), EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+  EXPECT_EQ(ArrowSchemaInitDecimal(&schema, NANOARROW_TYPE_DOUBLE, 1, 2), EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitDecimal(&schema, NANOARROW_TYPE_DECIMAL128, 1, 2),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "d:1,2");
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(decimal128(1, 2)));
+
+  EXPECT_EQ(ArrowSchemaInitDecimal(&schema, NANOARROW_TYPE_DECIMAL256, 3, 4),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "d:3,4,256");
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(decimal256(3, 4)));
+}
+
+TEST(SchemaTest, SchemaInitDateTime) {
+  struct ArrowSchema schema;
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_DOUBLE,
+                                    NANOARROW_TIME_UNIT_SECOND, nullptr),
+            EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIME32,
+                                    NANOARROW_TIME_UNIT_SECOND, "non-null timezone"),
+            EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_DURATION,
+                                    NANOARROW_TIME_UNIT_SECOND, "non-null timezone"),
+            EINVAL);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(
+                &schema, NANOARROW_TYPE_TIMESTAMP, NANOARROW_TIME_UNIT_SECOND,
+                "a really really really really really really really really really really "
+                "long timezone that causes a buffer overflow on snprintf"),
+            ERANGE);
+  EXPECT_EQ(schema.release, nullptr);
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIME32,
+                                    NANOARROW_TIME_UNIT_SECOND, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tts");
+
+  auto arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(time32(TimeUnit::SECOND)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIME64,
+                                    NANOARROW_TIME_UNIT_NANO, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "ttn");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(time64(TimeUnit::NANO)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_DURATION,
+                                    NANOARROW_TIME_UNIT_SECOND, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tDs");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(duration(TimeUnit::SECOND)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIMESTAMP,
+                                    NANOARROW_TIME_UNIT_SECOND, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tss:");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(timestamp(TimeUnit::SECOND)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIMESTAMP,
+                                    NANOARROW_TIME_UNIT_MILLI, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tsm:");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(timestamp(TimeUnit::MILLI)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIMESTAMP,
+                                    NANOARROW_TIME_UNIT_MICRO, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tsu:");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(timestamp(TimeUnit::MICRO)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIMESTAMP,
+                                    NANOARROW_TIME_UNIT_NANO, NULL),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tsn:");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(arrow_type.ValueUnsafe()->Equals(timestamp(TimeUnit::NANO)));
+
+  EXPECT_EQ(ArrowSchemaInitDateTime(&schema, NANOARROW_TYPE_TIMESTAMP,
+                                    NANOARROW_TIME_UNIT_SECOND, "America/Halifax"),
+            NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "tss:America/Halifax");
+
+  arrow_type = ImportType(&schema);
+  ARROW_EXPECT_OK(arrow_type);
+  EXPECT_TRUE(
+      arrow_type.ValueUnsafe()->Equals(timestamp(TimeUnit::SECOND, "America/Halifax")));
+}
+
+TEST(SchemaTest, SchemaSetFormat) {
+  struct ArrowSchema schema;
+  ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED);
+
+  EXPECT_EQ(ArrowSchemaSetFormat(&schema, "i"), NANOARROW_OK);
+  EXPECT_STREQ(schema.format, "i");
+
+  EXPECT_EQ(ArrowSchemaSetFormat(&schema, nullptr), NANOARROW_OK);
+  EXPECT_EQ(schema.format, nullptr);
+
+  schema.release(&schema);
+}
+
+TEST(SchemaTest, SchemaSetName) {
+  struct ArrowSchema schema;
+  ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED);
+
+  EXPECT_EQ(ArrowSchemaSetName(&schema, "a_name"), NANOARROW_OK);
+  EXPECT_STREQ(schema.name, "a_name");
+
+  EXPECT_EQ(ArrowSchemaSetName(&schema, nullptr), NANOARROW_OK);
+  EXPECT_EQ(schema.name, nullptr);
+
+  schema.release(&schema);
+}
+
+TEST(SchemaTest, SchemaSetMetadata) {
+  struct ArrowSchema schema;
+  ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED);
+
+  // (test will only work on little endian)
+  char simple_metadata[] = {'\1', '\0', '\0', '\0', '\3', '\0', '\0', '\0', 'k', 'e',
+                            'y',  '\5', '\0', '\0', '\0', 'v',  'a',  'l',  'u', 'e'};
+
+  EXPECT_EQ(ArrowSchemaSetMetadata(&schema, simple_metadata), NANOARROW_OK);
+  EXPECT_EQ(memcmp(schema.metadata, simple_metadata, sizeof(simple_metadata)), 0);
+
+  EXPECT_EQ(ArrowSchemaSetMetadata(&schema, nullptr), NANOARROW_OK);
+  EXPECT_EQ(schema.metadata, nullptr);
+
+  schema.release(&schema);
+}
+
+TEST(SchemaTest, SchemaAllocateDictionary) {
+  struct ArrowSchema schema;
+  ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED);
+
+  EXPECT_EQ(ArrowSchemaAllocateDictionary(&schema), NANOARROW_OK);
+  EXPECT_EQ(schema.dictionary->release, nullptr);
+  EXPECT_EQ(ArrowSchemaAllocateDictionary(&schema), EEXIST);
+  schema.release(&schema);
+}
+
+TEST(SchemaTest, SchemaCopySimpleType) {
+  struct ArrowSchema schema;
+  ARROW_EXPECT_OK(ExportType(*int32(), &schema));
+
+  struct ArrowSchema schema_copy;
+  ArrowSchemaDeepCopy(&schema, &schema_copy);
+
+  ASSERT_NE(schema_copy.release, nullptr);
+  EXPECT_STREQ(schema.format, "i");
+
+  schema.release(&schema);
+  schema_copy.release(&schema_copy);
+}
+
+TEST(SchemaTest, SchemaCopyNestedType) {
+  struct ArrowSchema schema;
+  auto struct_type = struct_({field("col1", int32())});
+  ARROW_EXPECT_OK(ExportType(*struct_type, &schema));
+
+  struct ArrowSchema schema_copy;
+  ArrowSchemaDeepCopy(&schema, &schema_copy);
+
+  ASSERT_NE(schema_copy.release, nullptr);
+  EXPECT_STREQ(schema_copy.format, "+s");
+  EXPECT_EQ(schema_copy.n_children, 1);
+  EXPECT_STREQ(schema_copy.children[0]->format, "i");
+  EXPECT_STREQ(schema_copy.children[0]->name, "col1");
+
+  schema.release(&schema);
+  schema_copy.release(&schema_copy);
+}
+
+TEST(SchemaTest, SchemaCopyDictType) {
+  struct ArrowSchema schema;
+  auto struct_type = dictionary(int32(), int64());
+  ARROW_EXPECT_OK(ExportType(*struct_type, &schema));
+
+  struct ArrowSchema schema_copy;
+  ArrowSchemaDeepCopy(&schema, &schema_copy);
+
+  ASSERT_STREQ(schema_copy.format, "i");
+  ASSERT_NE(schema_copy.dictionary, nullptr);
+  EXPECT_STREQ(schema_copy.dictionary->format, "l");
+
+  schema.release(&schema);
+  schema_copy.release(&schema_copy);
+}
+
+TEST(SchemaTest, SchemaCopyMetadata) {
+  struct ArrowSchema schema;
+  auto arrow_meta = std::make_shared<KeyValueMetadata>();
+  arrow_meta->Append("some_key", "some_value");
+
+  auto int_field = field("field_name", int32(), arrow_meta);
+  ARROW_EXPECT_OK(ExportField(*int_field, &schema));
+
+  struct ArrowSchema schema_copy;
+  ArrowSchemaDeepCopy(&schema, &schema_copy);
+
+  ASSERT_NE(schema_copy.release, nullptr);
+  EXPECT_STREQ(schema_copy.name, "field_name");
+  EXPECT_NE(schema_copy.metadata, nullptr);
+
+  auto int_field_roundtrip = ImportField(&schema_copy).ValueOrDie();
+  EXPECT_EQ(int_field->name(), int_field_roundtrip->name());
+  EXPECT_EQ(int_field_roundtrip->metadata()->Get("some_key").ValueOrDie(), "some_value");
+
+  schema.release(&schema);
+}
diff --git a/src/nanoarrow/schema_view.c b/src/nanoarrow/schema_view.c
new file mode 100644
index 0000000..54d586a
--- /dev/null
+++ b/src/nanoarrow/schema_view.c
@@ -0,0 +1,677 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <errno.h>
+#include <string.h>
+
+#include "nanoarrow.h"
+
+static void ArrowSchemaViewSetPrimitive(struct ArrowSchemaView* schema_view,
+                                        enum ArrowType data_type) {
+  schema_view->data_type = data_type;
+  schema_view->storage_data_type = data_type;
+  schema_view->n_buffers = 2;
+  schema_view->validity_buffer_id = 0;
+  schema_view->data_buffer_id = 1;
+}
+
+static ArrowErrorCode ArrowSchemaViewParse(struct ArrowSchemaView* schema_view,
+                                           const char* format,
+                                           const char** format_end_out,
+                                           struct ArrowError* error) {
+  schema_view->validity_buffer_id = -1;
+  schema_view->offset_buffer_id = -1;
+  schema_view->offset_buffer_id = -1;
+  schema_view->data_buffer_id = -1;
+  schema_view->type_id_buffer_id = -1;
+  *format_end_out = format;
+
+  // needed for decimal parsing
+  const char* parse_start;
+  char* parse_end;
+
+  switch (format[0]) {
+    case 'n':
+      schema_view->data_type = NANOARROW_TYPE_NA;
+      schema_view->storage_data_type = NANOARROW_TYPE_NA;
+      schema_view->n_buffers = 0;
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'b':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_BOOL);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'c':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT8);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'C':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_UINT8);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 's':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT16);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'S':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_UINT16);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'i':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'I':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_UINT32);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'l':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'L':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_UINT64);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'e':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_HALF_FLOAT);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'f':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_FLOAT);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'g':
+      ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_DOUBLE);
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+
+    // decimal
+    case 'd':
+      if (format[1] != ':' || format[2] == '\0') {
+        ArrowErrorSet(error, "Expected ':precision,scale[,bitwidth]' following 'd'",
+                      format + 3);
+        return EINVAL;
+      }
+
+      parse_start = format + 2;
+      schema_view->decimal_precision = strtol(parse_start, &parse_end, 10);
+      if (parse_end == parse_start || parse_end[0] != ',') {
+        ArrowErrorSet(error, "Expected 'precision,scale[,bitwidth]' following 'd:'");
+        return EINVAL;
+      }
+
+      parse_start = parse_end + 1;
+      schema_view->decimal_scale = strtol(parse_start, &parse_end, 10);
+      if (parse_end == parse_start) {
+        ArrowErrorSet(error, "Expected 'scale[,bitwidth]' following 'd:precision,'");
+        return EINVAL;
+      } else if (parse_end[0] != ',') {
+        schema_view->decimal_bitwidth = 128;
+      } else {
+        parse_start = parse_end + 1;
+        schema_view->decimal_bitwidth = strtol(parse_start, &parse_end, 10);
+        if (parse_start == parse_end) {
+          ArrowErrorSet(error, "Expected precision following 'd:precision,scale,'");
+          return EINVAL;
+        }
+      }
+
+      *format_end_out = parse_end;
+
+      switch (schema_view->decimal_bitwidth) {
+        case 128:
+          ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_DECIMAL128);
+          return NANOARROW_OK;
+        case 256:
+          ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_DECIMAL256);
+          return NANOARROW_OK;
+        default:
+          ArrowErrorSet(error, "Expected decimal bitwidth of 128 or 256 but found %d",
+                        (int)schema_view->decimal_bitwidth);
+          return EINVAL;
+      }
+
+    // validity + data
+    case 'w':
+      schema_view->data_type = NANOARROW_TYPE_FIXED_SIZE_BINARY;
+      schema_view->storage_data_type = NANOARROW_TYPE_FIXED_SIZE_BINARY;
+      if (format[1] != ':' || format[2] == '\0') {
+        ArrowErrorSet(error, "Expected ':<width>' following 'w'");
+        return EINVAL;
+      }
+
+      schema_view->n_buffers = 2;
+      schema_view->validity_buffer_id = 0;
+      schema_view->data_buffer_id = 1;
+      schema_view->fixed_size = strtol(format + 2, (char**)format_end_out, 10);
+      return NANOARROW_OK;
+
+    // validity + offset + data
+    case 'z':
+      schema_view->data_type = NANOARROW_TYPE_BINARY;
+      schema_view->storage_data_type = NANOARROW_TYPE_BINARY;
+      schema_view->n_buffers = 3;
+      schema_view->validity_buffer_id = 0;
+      schema_view->offset_buffer_id = 1;
+      schema_view->data_buffer_id = 2;
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'u':
+      schema_view->data_type = NANOARROW_TYPE_STRING;
+      schema_view->storage_data_type = NANOARROW_TYPE_STRING;
+      schema_view->n_buffers = 3;
+      schema_view->validity_buffer_id = 0;
+      schema_view->offset_buffer_id = 1;
+      schema_view->data_buffer_id = 2;
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+
+    // validity + large_offset + data
+    case 'Z':
+      schema_view->data_type = NANOARROW_TYPE_LARGE_BINARY;
+      schema_view->storage_data_type = NANOARROW_TYPE_LARGE_BINARY;
+      schema_view->n_buffers = 3;
+      schema_view->validity_buffer_id = 0;
+      schema_view->offset_buffer_id = 1;
+      schema_view->data_buffer_id = 2;
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+    case 'U':
+      schema_view->data_type = NANOARROW_TYPE_LARGE_STRING;
+      schema_view->storage_data_type = NANOARROW_TYPE_LARGE_STRING;
+      schema_view->n_buffers = 3;
+      schema_view->validity_buffer_id = 0;
+      schema_view->offset_buffer_id = 1;
+      schema_view->data_buffer_id = 2;
+      *format_end_out = format + 1;
+      return NANOARROW_OK;
+
+    // nested types
+    case '+':
+      switch (format[1]) {
+        // list has validity + offset or offset
+        case 'l':
+          schema_view->storage_data_type = NANOARROW_TYPE_LIST;
+          schema_view->data_type = NANOARROW_TYPE_LIST;
+          schema_view->n_buffers = 2;
+          schema_view->validity_buffer_id = 0;
+          schema_view->offset_buffer_id = 1;
+          *format_end_out = format + 2;
+          return NANOARROW_OK;
+
+        // large list has validity + large_offset or large_offset
+        case 'L':
+          schema_view->storage_data_type = NANOARROW_TYPE_LARGE_LIST;
+          schema_view->data_type = NANOARROW_TYPE_LARGE_LIST;
+          schema_view->n_buffers = 2;
+          schema_view->validity_buffer_id = 0;
+          schema_view->offset_buffer_id = 1;
+          *format_end_out = format + 2;
+          return NANOARROW_OK;
+
+        // just validity buffer
+        case 'w':
+          if (format[2] != ':' || format[3] == '\0') {
+            ArrowErrorSet(error, "Expected ':<width>' following '+w'");
+            return EINVAL;
+          }
+
+          schema_view->storage_data_type = NANOARROW_TYPE_FIXED_SIZE_LIST;
+          schema_view->data_type = NANOARROW_TYPE_FIXED_SIZE_LIST;
+          schema_view->n_buffers = 1;
+          schema_view->validity_buffer_id = 0;
+          schema_view->fixed_size = strtol(format + 3, (char**)format_end_out, 10);
+          return NANOARROW_OK;
+        case 's':
+          schema_view->storage_data_type = NANOARROW_TYPE_STRUCT;
+          schema_view->data_type = NANOARROW_TYPE_STRUCT;
+          schema_view->n_buffers = 1;
+          schema_view->validity_buffer_id = 0;
+          *format_end_out = format + 2;
+          return NANOARROW_OK;
+        case 'm':
+          schema_view->storage_data_type = NANOARROW_TYPE_MAP;
+          schema_view->data_type = NANOARROW_TYPE_MAP;
+          schema_view->n_buffers = 1;
+          schema_view->validity_buffer_id = 0;
+          *format_end_out = format + 2;
+          return NANOARROW_OK;
+
+        // unions
+        case 'u':
+          switch (format[2]) {
+            case 'd':
+              schema_view->storage_data_type = NANOARROW_TYPE_DENSE_UNION;
+              schema_view->data_type = NANOARROW_TYPE_DENSE_UNION;
+              schema_view->n_buffers = 2;
+              schema_view->type_id_buffer_id = 0;
+              schema_view->offset_buffer_id = 1;
+              break;
+            case 's':
+              schema_view->storage_data_type = NANOARROW_TYPE_SPARSE_UNION;
+              schema_view->data_type = NANOARROW_TYPE_SPARSE_UNION;
+              schema_view->n_buffers = 1;
+              schema_view->type_id_buffer_id = 0;
+              break;
+            default:
+              ArrowErrorSet(error,
+                            "Expected union format string +us:<type_ids> or "
+                            "+ud:<type_ids> but found '%s'",
+                            format);
+              return EINVAL;
+          }
+
+          if (format[3] == ':') {
+            schema_view->union_type_ids.data = format + 4;
+            schema_view->union_type_ids.n_bytes = strlen(format + 4);
+            *format_end_out = format + strlen(format);
+            return NANOARROW_OK;
+          } else {
+            ArrowErrorSet(error,
+                          "Expected union format string +us:<type_ids> or +ud:<type_ids> "
+                          "but found '%s'",
+                          format);
+            return EINVAL;
+          }
+      }
+
+    // date/time types
+    case 't':
+      switch (format[1]) {
+        // date
+        case 'd':
+          switch (format[2]) {
+            case 'D':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_DATE32;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'm':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_DATE64;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            default:
+              ArrowErrorSet(error, "Expected 'D' or 'm' following 'td' but found '%s'",
+                            format + 2);
+              return EINVAL;
+          }
+
+        // time of day
+        case 't':
+          switch (format[2]) {
+            case 's':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_TIME32;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_SECOND;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'm':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_TIME32;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MILLI;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'u':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_TIME64;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MICRO;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'n':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_TIME64;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_NANO;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            default:
+              ArrowErrorSet(
+                  error, "Expected 's', 'm', 'u', or 'n' following 'tt' but found '%s'",
+                  format + 2);
+              return EINVAL;
+          }
+
+        // timestamp
+        case 's':
+          switch (format[2]) {
+            case 's':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_TIMESTAMP;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_SECOND;
+              break;
+            case 'm':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_TIMESTAMP;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MILLI;
+              break;
+            case 'u':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_TIMESTAMP;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MICRO;
+              break;
+            case 'n':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_TIMESTAMP;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_NANO;
+              break;
+            default:
+              ArrowErrorSet(
+                  error, "Expected 's', 'm', 'u', or 'n' following 'ts' but found '%s'",
+                  format + 2);
+              return EINVAL;
+          }
+
+          if (format[3] != ':') {
+            ArrowErrorSet(error, "Expected ':' following '%.3s' but found '%s'", format,
+                          format + 3);
+            return EINVAL;
+          }
+
+          schema_view->timezone.data = format + 4;
+          schema_view->timezone.n_bytes = strlen(format + 4);
+          *format_end_out = format + strlen(format);
+          return NANOARROW_OK;
+
+        // duration
+        case 'D':
+          switch (format[2]) {
+            case 's':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_DURATION;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_SECOND;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'm':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT32);
+              schema_view->data_type = NANOARROW_TYPE_DURATION;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MILLI;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'u':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_DURATION;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_MICRO;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'n':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INT64);
+              schema_view->data_type = NANOARROW_TYPE_DURATION;
+              schema_view->time_unit = NANOARROW_TIME_UNIT_NANO;
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            default:
+              ArrowErrorSet(error,
+                            "Expected 's', 'm', u', or 'n' following 'tD' but found '%s'",
+                            format + 2);
+              return EINVAL;
+          }
+
+        // interval
+        case 'i':
+          switch (format[2]) {
+            case 'M':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INTERVAL_MONTHS);
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'D':
+              ArrowSchemaViewSetPrimitive(schema_view, NANOARROW_TYPE_INTERVAL_DAY_TIME);
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            case 'n':
+              ArrowSchemaViewSetPrimitive(schema_view,
+                                          NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO);
+              *format_end_out = format + 3;
+              return NANOARROW_OK;
+            default:
+              ArrowErrorSet(error,
+                            "Expected 'M', 'D', or 'n' following 'ti' but found '%s'",
+                            format + 2);
+              return EINVAL;
+          }
+
+        default:
+          ArrowErrorSet(
+              error, "Expected 'd', 't', 's', 'D', or 'i' following 't' but found '%s'",
+              format + 1);
+          return EINVAL;
+      }
+
+    default:
+      ArrowErrorSet(error, "Unknown format: '%s'", format);
+      return EINVAL;
+  }
+}
+
+static ArrowErrorCode ArrowSchemaViewValidateNChildren(
+    struct ArrowSchemaView* schema_view, int64_t n_children, struct ArrowError* error) {
+  if (n_children != -1 && schema_view->schema->n_children != n_children) {
+    ArrowErrorSet(error, "Expected schema with %d children but found %d children",
+                  (int)n_children, (int)schema_view->schema->n_children);
+    return EINVAL;
+  }
+
+  // Don't do a full validation of children but do check that they won't
+  // segfault if inspected
+  struct ArrowSchema* child;
+  for (int64_t i = 0; i < schema_view->schema->n_children; i++) {
+    child = schema_view->schema->children[i];
+    if (child == NULL) {
+      ArrowErrorSet(error, "Expected valid schema at schema->children[%d] but found NULL",
+                    i);
+      return EINVAL;
+    } else if (child->release == NULL) {
+      ArrowErrorSet(
+          error,
+          "Expected valid schema at schema->children[%d] but found a released schema", i);
+      return EINVAL;
+    }
+  }
+
+  return NANOARROW_OK;
+}
+
+static ArrowErrorCode ArrowSchemaViewValidateUnion(struct ArrowSchemaView* schema_view,
+                                                   struct ArrowError* error) {
+  return ArrowSchemaViewValidateNChildren(schema_view, -1, error);
+}
+
+static ArrowErrorCode ArrowSchemaViewValidateMap(struct ArrowSchemaView* schema_view,
+                                                 struct ArrowError* error) {
+  int result = ArrowSchemaViewValidateNChildren(schema_view, 1, error);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  if (schema_view->schema->children[0]->n_children != 2) {
+    ArrowErrorSet(error, "Expected child of map type to have 2 children but found %d",
+                  (int)schema_view->schema->children[0]->n_children);
+    return EINVAL;
+  }
+
+  if (strcmp(schema_view->schema->children[0]->format, "+s") != 0) {
+    ArrowErrorSet(error, "Expected format of child of map type to be '+s' but found '%s'",
+                  schema_view->schema->children[0]->format);
+    return EINVAL;
+  }
+
+  return NANOARROW_OK;
+}
+
+static ArrowErrorCode ArrowSchemaViewValidateDictionary(
+    struct ArrowSchemaView* schema_view, struct ArrowError* error) {
+  // check for valid index type
+  switch (schema_view->storage_data_type) {
+    case NANOARROW_TYPE_UINT8:
+    case NANOARROW_TYPE_INT8:
+    case NANOARROW_TYPE_UINT16:
+    case NANOARROW_TYPE_INT16:
+    case NANOARROW_TYPE_UINT32:
+    case NANOARROW_TYPE_INT32:
+    case NANOARROW_TYPE_UINT64:
+    case NANOARROW_TYPE_INT64:
+      break;
+    default:
+      ArrowErrorSet(
+          error,
+          "Expected dictionary schema index type to be an integral type but found '%s'",
+          schema_view->schema->format);
+      return EINVAL;
+  }
+
+  struct ArrowSchemaView dictionary_schema_view;
+  return ArrowSchemaViewInit(&dictionary_schema_view, schema_view->schema->dictionary,
+                             error);
+}
+
+static ArrowErrorCode ArrowSchemaViewValidate(struct ArrowSchemaView* schema_view,
+                                              enum ArrowType data_type,
+                                              struct ArrowError* error) {
+  switch (data_type) {
+    case NANOARROW_TYPE_NA:
+    case NANOARROW_TYPE_BOOL:
+    case NANOARROW_TYPE_UINT8:
+    case NANOARROW_TYPE_INT8:
+    case NANOARROW_TYPE_UINT16:
+    case NANOARROW_TYPE_INT16:
+    case NANOARROW_TYPE_UINT32:
+    case NANOARROW_TYPE_INT32:
+    case NANOARROW_TYPE_UINT64:
+    case NANOARROW_TYPE_INT64:
+    case NANOARROW_TYPE_HALF_FLOAT:
+    case NANOARROW_TYPE_FLOAT:
+    case NANOARROW_TYPE_DOUBLE:
+    case NANOARROW_TYPE_DECIMAL128:
+    case NANOARROW_TYPE_DECIMAL256:
+    case NANOARROW_TYPE_STRING:
+    case NANOARROW_TYPE_LARGE_STRING:
+    case NANOARROW_TYPE_BINARY:
+    case NANOARROW_TYPE_LARGE_BINARY:
+    case NANOARROW_TYPE_DATE32:
+    case NANOARROW_TYPE_DATE64:
+    case NANOARROW_TYPE_INTERVAL_MONTHS:
+    case NANOARROW_TYPE_INTERVAL_DAY_TIME:
+    case NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO:
+    case NANOARROW_TYPE_TIMESTAMP:
+    case NANOARROW_TYPE_TIME32:
+    case NANOARROW_TYPE_TIME64:
+    case NANOARROW_TYPE_DURATION:
+      return ArrowSchemaViewValidateNChildren(schema_view, 0, error);
+
+    case NANOARROW_TYPE_FIXED_SIZE_BINARY:
+      if (schema_view->fixed_size <= 0) {
+        ArrowErrorSet(error, "Expected size > 0 for fixed size binary but found size %d",
+                      schema_view->fixed_size);
+        return EINVAL;
+      }
+      return ArrowSchemaViewValidateNChildren(schema_view, 0, error);
+
+    case NANOARROW_TYPE_LIST:
+    case NANOARROW_TYPE_LARGE_LIST:
+    case NANOARROW_TYPE_FIXED_SIZE_LIST:
+      return ArrowSchemaViewValidateNChildren(schema_view, 1, error);
+
+    case NANOARROW_TYPE_STRUCT:
+      return ArrowSchemaViewValidateNChildren(schema_view, -1, error);
+
+    case NANOARROW_TYPE_SPARSE_UNION:
+    case NANOARROW_TYPE_DENSE_UNION:
+      return ArrowSchemaViewValidateUnion(schema_view, error);
+
+    case NANOARROW_TYPE_MAP:
+      return ArrowSchemaViewValidateMap(schema_view, error);
+
+    case NANOARROW_TYPE_DICTIONARY:
+      return ArrowSchemaViewValidateDictionary(schema_view, error);
+
+    default:
+      ArrowErrorSet(error, "Expected a valid enum ArrowType value but found %d",
+                    (int)schema_view->data_type);
+      return EINVAL;
+  }
+
+  return NANOARROW_OK;
+}
+
+ArrowErrorCode ArrowSchemaViewInit(struct ArrowSchemaView* schema_view,
+                                   struct ArrowSchema* schema, struct ArrowError* error) {
+  if (schema == NULL) {
+    ArrowErrorSet(error, "Expected non-NULL schema");
+    return EINVAL;
+  }
+
+  if (schema->release == NULL) {
+    ArrowErrorSet(error, "Expected non-released schema");
+    return EINVAL;
+  }
+
+  schema_view->schema = schema;
+
+  const char* format = schema->format;
+  if (format == NULL) {
+    ArrowErrorSet(
+        error,
+        "Error parsing schema->format: Expected a null-terminated string but found NULL");
+    return EINVAL;
+  }
+
+  int format_len = strlen(format);
+  if (format_len == 0) {
+    ArrowErrorSet(error, "Error parsing schema->format: Expected a string with size > 0");
+    return EINVAL;
+  }
+
+  const char* format_end_out;
+  ArrowErrorCode result =
+      ArrowSchemaViewParse(schema_view, format, &format_end_out, error);
+
+  if (result != NANOARROW_OK) {
+    char child_error[1024];
+    memcpy(child_error, ArrowErrorMessage(error), 1024);
+    ArrowErrorSet(error, "Error parsing schema->format: %s", child_error);
+    return result;
+  }
+
+  if ((format + format_len) != format_end_out) {
+    ArrowErrorSet(error, "Error parsing schema->format '%s': parsed %d/%d characters",
+                  format, (int)(format_end_out - format), (int)(format_len));
+    return EINVAL;
+  }
+
+  if (schema->dictionary != NULL) {
+    schema_view->data_type = NANOARROW_TYPE_DICTIONARY;
+  }
+
+  result = ArrowSchemaViewValidate(schema_view, schema_view->storage_data_type, error);
+  if (result != NANOARROW_OK) {
+    return result;
+  }
+
+  if (schema_view->storage_data_type != schema_view->data_type) {
+    result = ArrowSchemaViewValidate(schema_view, schema_view->data_type, error);
+    if (result != NANOARROW_OK) {
+      return result;
+    }
+  }
+
+  ArrowMetadataGetValue(schema->metadata, "ARROW:extension:name", NULL,
+                        &schema_view->extension_name);
+  ArrowMetadataGetValue(schema->metadata, "ARROW:extension:metadata", NULL,
+                        &schema_view->extension_metadata);
+
+  return NANOARROW_OK;
+}
diff --git a/src/nanoarrow/schema_view_test.cc b/src/nanoarrow/schema_view_test.cc
new file mode 100644
index 0000000..bb1fb4e
--- /dev/null
+++ b/src/nanoarrow/schema_view_test.cc
@@ -0,0 +1,782 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include <arrow/c/bridge.h>
+#include <arrow/testing/gtest_util.h>
+#include <arrow/util/key_value_metadata.h>
+
+#include "nanoarrow/nanoarrow.h"
+
+using namespace arrow;
+
+TEST(SchemaViewTest, SchemaViewInitErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, nullptr, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error), "Expected non-NULL schema");
+
+  schema.release = nullptr;
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error), "Expected non-released schema");
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_UNINITIALIZED), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(
+      ArrowErrorMessage(&error),
+      "Error parsing schema->format: Expected a null-terminated string but found NULL");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, ""), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected a string with size > 0");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Unknown format: '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "n*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format 'n*': parsed 1/2 characters");
+
+  schema.release(&schema);
+}
+
+void ExpectSimpleTypeOk(std::shared_ptr<DataType> arrow_t, enum ArrowType nanoarrow_t) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*arrow_t, &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, nanoarrow_t);
+  EXPECT_EQ(schema_view.storage_data_type, nanoarrow_t);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitSimple) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*null(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_NA);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_NA);
+  EXPECT_EQ(schema_view.n_buffers, 0);
+  EXPECT_EQ(schema_view.extension_name.data, nullptr);
+  EXPECT_EQ(schema_view.extension_metadata.data, nullptr);
+  schema.release(&schema);
+
+  ExpectSimpleTypeOk(boolean(), NANOARROW_TYPE_BOOL);
+  ExpectSimpleTypeOk(int8(), NANOARROW_TYPE_INT8);
+  ExpectSimpleTypeOk(uint8(), NANOARROW_TYPE_UINT8);
+  ExpectSimpleTypeOk(int16(), NANOARROW_TYPE_INT16);
+  ExpectSimpleTypeOk(uint16(), NANOARROW_TYPE_UINT16);
+  ExpectSimpleTypeOk(int32(), NANOARROW_TYPE_INT32);
+  ExpectSimpleTypeOk(uint32(), NANOARROW_TYPE_UINT32);
+  ExpectSimpleTypeOk(int64(), NANOARROW_TYPE_INT64);
+  ExpectSimpleTypeOk(uint64(), NANOARROW_TYPE_UINT64);
+  ExpectSimpleTypeOk(float16(), NANOARROW_TYPE_HALF_FLOAT);
+  ExpectSimpleTypeOk(float64(), NANOARROW_TYPE_DOUBLE);
+  ExpectSimpleTypeOk(float32(), NANOARROW_TYPE_FLOAT);
+}
+
+TEST(SchemaViewTest, SchemaViewInitSimpleErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 2), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected schema with 0 children but found 2 children");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitDecimal) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*decimal128(5, 6), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DECIMAL128);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_DECIMAL128);
+  EXPECT_EQ(schema_view.decimal_bitwidth, 128);
+  EXPECT_EQ(schema_view.decimal_precision, 5);
+  EXPECT_EQ(schema_view.decimal_scale, 6);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*decimal256(5, 6), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DECIMAL256);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_DECIMAL256);
+  EXPECT_EQ(schema_view.decimal_bitwidth, 256);
+  EXPECT_EQ(schema_view.decimal_precision, 5);
+  EXPECT_EQ(schema_view.decimal_scale, 6);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitDecimalErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':precision,scale[,bitwidth]' "
+               "following 'd'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d:"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':precision,scale[,bitwidth]' "
+               "following 'd'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d:5"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 'precision,scale[,bitwidth]' "
+               "following 'd:'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d:5,"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 'scale[,bitwidth]' following "
+               "'d:precision,'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d:5,6,"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(
+      ArrowErrorMessage(&error),
+      "Error parsing schema->format: Expected precision following 'd:precision,scale,'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "d:5,6,127"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected decimal bitwidth of 128 or 256 "
+               "but found 127");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitBinaryAndString) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*fixed_size_binary(123), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_FIXED_SIZE_BINARY);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_FIXED_SIZE_BINARY);
+  EXPECT_EQ(schema_view.fixed_size, 123);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*utf8(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 3);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_buffer_id, 2);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_STRING);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_STRING);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*binary(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 3);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_buffer_id, 2);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_BINARY);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_BINARY);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*large_binary(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 3);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_buffer_id, 2);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_LARGE_BINARY);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_LARGE_BINARY);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*large_utf8(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 3);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_buffer_id, 2);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_LARGE_STRING);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_LARGE_STRING);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitBinaryAndStringErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "w"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':<width>' following 'w'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "w:"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':<width>' following 'w'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "w:abc"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format 'w:abc': parsed 2/5 characters");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "w:0"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected size > 0 for fixed size binary but found size 0");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeDate) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*date32(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DATE32);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*date64(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DATE64);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeTime) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*time32(TimeUnit::SECOND), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIME32);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_SECOND);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*time32(TimeUnit::MILLI), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIME32);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MILLI);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*time64(TimeUnit::MICRO), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIME64);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MICRO);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*time64(TimeUnit::NANO), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIME64);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_NANO);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeTimestamp) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*timestamp(TimeUnit::SECOND, "America/Halifax"), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIMESTAMP);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_SECOND);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*timestamp(TimeUnit::MILLI, "America/Halifax"), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIMESTAMP);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MILLI);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*timestamp(TimeUnit::MICRO, "America/Halifax"), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIMESTAMP);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MICRO);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*timestamp(TimeUnit::NANO, "America/Halifax"), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_TIMESTAMP);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_NANO);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeDuration) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*duration(TimeUnit::SECOND), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DURATION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_SECOND);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*duration(TimeUnit::MILLI), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DURATION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MILLI);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*duration(TimeUnit::MICRO), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DURATION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_MICRO);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*duration(TimeUnit::NANO), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DURATION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT64);
+  EXPECT_EQ(schema_view.time_unit, NANOARROW_TIME_UNIT_NANO);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeInterval) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*month_interval(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_INTERVAL_MONTHS);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INTERVAL_MONTHS);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*day_time_interval(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_INTERVAL_DAY_TIME);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INTERVAL_DAY_TIME);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*month_day_nano_interval(), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INTERVAL_MONTH_DAY_NANO);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitTimeErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "t*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 'd', 't', 's', 'D', or 'i' "
+               "following 't' but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "td*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(
+      ArrowErrorMessage(&error),
+      "Error parsing schema->format: Expected 'D' or 'm' following 'td' but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "tt*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 's', 'm', 'u', or 'n' following "
+               "'tt' but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "ts*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 's', 'm', 'u', or 'n' following "
+               "'ts' but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "tD*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 's', 'm', u', or 'n' following "
+               "'tD' but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "ti*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected 'M', 'D', or 'n' following 'ti' "
+               "but found '*'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "tss"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':' following 'tss' but found ''");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedList) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*list(int32()), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_LIST);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_LIST);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*large_list(int32()), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_LARGE_LIST);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_LARGE_LIST);
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*fixed_size_list(int32(), 123), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 1);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_FIXED_SIZE_LIST);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_FIXED_SIZE_LIST);
+  EXPECT_EQ(schema_view.fixed_size, 123);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewNestedListErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "+w"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':<width>' following '+w'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "+w:"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected ':<width>' following '+w'");
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "+w:1"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected schema with 1 children but found 0 children");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedStruct) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*struct_({field("col", int32())}), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 1);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_STRUCT);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_STRUCT);
+
+  // Make sure child validates
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, schema.children[0], &error), NANOARROW_OK);
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedStructErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_STRUCT), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(
+      ArrowErrorMessage(&error),
+      "Expected valid schema at schema->children[0] but found a released schema");
+
+  // Make sure validation passes even with an inspectable but invalid child
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_UNINITIALIZED),
+            NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, schema.children[0], &error), EINVAL);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+
+  ArrowFree(schema.children[0]);
+  schema.children[0] = NULL;
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected valid schema at schema->children[0] but found NULL");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedMap) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*map(int32(), int32()), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 1);
+  EXPECT_EQ(schema_view.validity_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_MAP);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_MAP);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedMapErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_MAP), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 2), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected schema with 1 children but found 2 children");
+  schema.release(&schema);
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_MAP), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_UNINITIALIZED),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetFormat(schema.children[0], "n"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected child of map type to have 2 children but found 0");
+  schema.release(&schema);
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_MAP), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(&schema, 1), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0], NANOARROW_TYPE_UNINITIALIZED),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateChildren(schema.children[0], 2), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaSetFormat(schema.children[0], "+us:0,1"), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0]->children[0], NANOARROW_TYPE_NA),
+            NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.children[0]->children[1], NANOARROW_TYPE_NA),
+            NANOARROW_OK);
+
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Expected format of child of map type to be '+s' but found '+us:0,1'");
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedUnion) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*dense_union({field("col", int32())}), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 2);
+  EXPECT_EQ(schema_view.type_id_buffer_id, 0);
+  EXPECT_EQ(schema_view.offset_buffer_id, 1);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DENSE_UNION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_DENSE_UNION);
+  EXPECT_EQ(
+      std::string(schema_view.union_type_ids.data, schema_view.union_type_ids.n_bytes),
+      std::string("0"));
+  schema.release(&schema);
+
+  ARROW_EXPECT_OK(ExportType(*sparse_union({field("col", int32())}), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.n_buffers, 1);
+  EXPECT_EQ(schema_view.type_id_buffer_id, 0);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_SPARSE_UNION);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_SPARSE_UNION);
+  EXPECT_EQ(
+      std::string(schema_view.union_type_ids.data, schema_view.union_type_ids.n_bytes),
+      std::string("0"));
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitNestedUnionErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_NA), NANOARROW_OK);
+
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "+u*"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected union format string "
+               "+us:<type_ids> or +ud:<type_ids> but found '+u*'");
+
+  // missing colon
+  ASSERT_EQ(ArrowSchemaSetFormat(&schema, "+us"), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error),
+               "Error parsing schema->format: Expected union format string "
+               "+us:<type_ids> or +ud:<type_ids> but found '+us'");
+
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitDictionary) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ARROW_EXPECT_OK(ExportType(*dictionary(int32(), utf8()), &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(schema_view.storage_data_type, NANOARROW_TYPE_INT32);
+  EXPECT_EQ(schema_view.data_type, NANOARROW_TYPE_DICTIONARY);
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitDictionaryErrors) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_INT32), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateDictionary(&schema), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(ArrowErrorMessage(&error), "Expected non-released schema");
+  schema.release(&schema);
+
+  ASSERT_EQ(ArrowSchemaInit(&schema, NANOARROW_TYPE_STRUCT), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaAllocateDictionary(&schema), NANOARROW_OK);
+  ASSERT_EQ(ArrowSchemaInit(schema.dictionary, NANOARROW_TYPE_STRING), NANOARROW_OK);
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), EINVAL);
+  EXPECT_STREQ(
+      ArrowErrorMessage(&error),
+      "Expected dictionary schema index type to be an integral type but found '+s'");
+  schema.release(&schema);
+}
+
+TEST(SchemaViewTest, SchemaViewInitExtension) {
+  struct ArrowSchema schema;
+  struct ArrowSchemaView schema_view;
+  struct ArrowError error;
+
+  auto arrow_meta = std::make_shared<KeyValueMetadata>();
+  arrow_meta->Append("ARROW:extension:name", "arrow.test.ext_name");
+  arrow_meta->Append("ARROW:extension:metadata", "test metadata");
+
+  auto int_field = field("field_name", int32(), arrow_meta);
+  ARROW_EXPECT_OK(ExportField(*int_field, &schema));
+  EXPECT_EQ(ArrowSchemaViewInit(&schema_view, &schema, &error), NANOARROW_OK);
+  EXPECT_EQ(
+      std::string(schema_view.extension_name.data, schema_view.extension_name.n_bytes),
+      "arrow.test.ext_name");
+  EXPECT_EQ(std::string(schema_view.extension_metadata.data,
+                        schema_view.extension_metadata.n_bytes),
+            "test metadata");
+
+  schema.release(&schema);
+}