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

etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 4731c6c  feat: schema and types (#1)
4731c6c is described below

commit 4731c6cd9a2c87533cac6f6fc5ad690a9feed375
Author: Matt Topol <[email protected]>
AuthorDate: Wed Aug 16 01:48:37 2023 -0400

    feat: schema and types (#1)
---
 .asf.yaml                                      |  34 +
 .gitattributes                                 |  23 +
 .github/ISSUE_TEMPLATE/iceberg_bug_report.yml  |  40 ++
 .github/ISSUE_TEMPLATE/iceberg_improvement.yml |  28 +
 .github/ISSUE_TEMPLATE/iceberg_question.yml    |  31 +
 .github/dependabot.yml                         |  24 +
 .github/labeler.yml                            |  25 +
 .github/workflows/go-ci.yml                    |  57 ++
 .github/workflows/labeler.yml                  |  32 +
 .github/workflows/license_check.yml            |  27 +
 .gitignore                                     |  39 ++
 .pre-commit-config.yaml                        |  26 +
 LICENSE                                        | 315 +++++++++
 NOTICE                                         |   8 +
 README.md                                      |  27 +-
 dev/.rat-excludes                              |   5 +
 dev/check-license                              |  83 +++
 errors.go                                      |  27 +
 go.mod                                         |  31 +
 go.sum                                         |  12 +
 schema.go                                      | 860 +++++++++++++++++++++++++
 schema_test.go                                 | 758 ++++++++++++++++++++++
 types.go                                       | 615 ++++++++++++++++++
 types_test.go                                  | 236 +++++++
 utils.go                                       |  54 ++
 25 files changed, 3416 insertions(+), 1 deletion(-)

diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..ffd625d
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# The format of this file is documented at
+# https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features
+
+github:
+  description: "Apache Iceberg - Go"
+  homepage: https://iceberg.apache.org/
+  labels:
+    - iceberg
+    - apache
+    - golang
+  features:
+    issues: true
+
+notifications:
+  commits:      [email protected]
+  issues:       [email protected]
+  pullrequests: [email protected]
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..2ca0612
--- /dev/null
+++ b/.gitattributes
@@ -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.
+
+# Files marked as export-ignore will be ignored from the release's
+# built via `git archive`. This simplifies the release script and 
+# uses an industry standard as opposed to possibly hard to read
+# shell scripts with many flags. Unfortunately, directories themselves
+# won't recursively ignore, so we need the top level directories
+# as well as their files.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/iceberg_bug_report.yml 
b/.github/ISSUE_TEMPLATE/iceberg_bug_report.yml
new file mode 100644
index 0000000..512bebc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/iceberg_bug_report.yml
@@ -0,0 +1,40 @@
+# 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.
+
+---
+name: Iceberg Bug report 🐞
+description: Problems, bugs and issues with Apache Iceberg
+labels: ["kind:bug"]
+body:
+  - type: dropdown
+    attributes:
+      label: Apache Iceberg version
+      description: What Apache Iceberg version are you using?
+      multiple: false
+      options:     
+        - "main (development)"
+    validations:
+      required: false
+  - type: textarea
+    attributes:
+      label: Please describe the bug 🐞
+      description: >
+        Please describe the problem, what to expect, and how to reproduce.
+        Feel free to include stacktraces and the Iceberg catalog configuration.
+        You can include files by dragging and dropping them here.
+    validations:
+      required: true
diff --git a/.github/ISSUE_TEMPLATE/iceberg_improvement.yml 
b/.github/ISSUE_TEMPLATE/iceberg_improvement.yml
new file mode 100644
index 0000000..60eddb5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/iceberg_improvement.yml
@@ -0,0 +1,28 @@
+# 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.
+
+---
+name: Iceberg Improvement / Feature Request
+description: New features with Apache Iceberg
+labels: ["kind:feature request"]
+body:
+  - type: textarea
+    attributes:
+      label: Feature Request / Improvement
+      description: Please describe the feature and elaborate on the use case 
and motivation behind it
+    validations:
+      required: true     
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/iceberg_question.yml 
b/.github/ISSUE_TEMPLATE/iceberg_question.yml
new file mode 100644
index 0000000..a6111bb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/iceberg_question.yml
@@ -0,0 +1,31 @@
+# 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.
+
+---
+name: Iceberg Question
+description: Questions around Apache Iceberg
+labels: ["kind:question"]
+body:
+  - type: markdown
+    attributes:
+      value: "Feel free to ask your question on 
[Slack](https://join.slack.com/t/apache-iceberg/shared_invite/zt-1uva9gyp1-TrLQl7o~nZ5PsTVgl6uoEQ)
 as well."
+  - type: textarea
+    attributes:
+      label: Question
+      description: What is your question?
+    validations:
+      required: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..14f8e04
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+# 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.
+
+version: 2
+updates:
+  - package-ecosystem: gomod
+    directory: /
+    schedule:
+      interval: "weekly"
+      day: "sunday"
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000..4cd7825
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,25 @@
+# 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.
+
+# Pull request labeler Github Action Configuration: 
https://github.com/marketplace/actions/labeler
+INFRA:
+  - .asf.yaml
+  - .gitattributes
+  - .gitignore
+  - .github/**/*
+  - .pre-commit-config.yaml
+  - dev/**/*
diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml
new file mode 100644
index 0000000..5f4c42a
--- /dev/null
+++ b/.github/workflows/go-ci.yml
@@ -0,0 +1,57 @@
+# 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.
+
+name: Go
+
+on:
+  push:
+    branches:
+      - 'master'
+    tags:
+      - 'v**'          
+  pull_request:    
+
+concurrency:
+  group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ 
github.workflow }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+  contents: read
+
+jobs:
+  lint-and-test:
+    name: ${{ matrix.os }} go${{ matrix.go }}
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        go: [ '1.20', '1.21' ]
+        os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest' ]
+    steps:
+    - uses: actions/checkout@v3
+    - name: Install Go
+      uses: actions/setup-go@v4
+      with:
+        go-version: ${{ matrix.go }}
+        cache: true
+        cache-dependency-path: go.sum
+    - name: Install staticcheck
+      run: go install honnef.co/go/tools/cmd/staticcheck@latest
+    - name: Lint
+      run: staticcheck ./...
+    - name: Run tests
+      run: go test -v ./...
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 0000000..4a5b455
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,32 @@
+# 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.
+
+name: "Pull Request Labeler"
+on: pull_request_target
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  triage:
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/labeler@v4
+      with:
+        repo-token: "${{ secrets.GITHUB_TOKEN }}"
+        sync-labels: true
diff --git a/.github/workflows/license_check.yml 
b/.github/workflows/license_check.yml
new file mode 100644
index 0000000..d727084
--- /dev/null
+++ b/.github/workflows/license_check.yml
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: "Run License Check"
+on: pull_request
+
+jobs:
+  rat:
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v3
+    - run: |
+        dev/check-license
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9fe7271
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+# 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.
+
+.DS_Store
+.cache
+tmp/
+
+.vscode
+
+# Binaries from programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# rat check
+build/
+lib/
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool
+*.out
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..d1bf3ab
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,26 @@
+# 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.
+---
+files: ^go/
+
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: golangci-lint
+        entry: bash -c 'cd iceberg && golangci-lint run --fix --timeout 5m'
+        types_or: [go]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..515fd54
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,315 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
+
+--------------------------------------------------------------------------------
+
+This product includes a gradle wrapper.
+
+* gradlew and gradle/wrapper/gradle-wrapper.properties
+
+Copyright: 2010-2019 Gradle Authors.
+Home page: https://github.com/gradle/gradle
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache Avro.
+
+* Conversion in DecimalWriter is based on Avro's Conversions.DecimalConversion.
+
+Copyright: 2014-2017 The Apache Software Foundation.
+Home page: https://avro.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache Parquet.
+
+* DynMethods.java
+* DynConstructors.java
+* AssertHelpers.java
+* IOUtil.java readFully and tests
+* ByteBufferInputStream implementations and tests
+
+Copyright: 2014-2017 The Apache Software Foundation.
+Home page: https://parquet.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Cloudera Kite.
+
+* SchemaVisitor and visit methods
+
+Copyright: 2013-2017 Cloudera Inc.
+Home page: https://kitesdk.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Presto.
+
+* Retry wait and jitter logic in Tasks.java
+* S3FileIO logic derived from PrestoS3FileSystem.java in S3InputStream.java
+  and S3OutputStream.java
+* SQL grammar rules for parsing CALL statements in IcebergSqlExtensions.g4
+* some aspects of handling stored procedures
+
+Copyright: 2016 Facebook and contributors
+Home page: https://prestodb.io/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache iBATIS.
+
+* Hive ScriptRunner.java
+
+Copyright: 2004 Clinton Begin
+Home page: https://ibatis.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache Hive.
+
+* Hive metastore derby schema in hive-schema-3.1.0.derby.sql
+
+Copyright: 2011-2018 The Apache Software Foundation
+Home page: https://hive.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache Spark.
+
+* dev/check-license script
+* vectorized reading of definition levels in 
BaseVectorizedParquetValuesReader.java
+* portions of the extensions parser
+* casting logic in AssignmentAlignmentSupport
+* implementation of SetAccumulator.
+* Connector expressions.
+
+Copyright: 2011-2018 The Apache Software Foundation
+Home page: https://spark.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Delta Lake.
+
+* AssignmentAlignmentSupport is an independent development but 
UpdateExpressionsSupport in Delta was used as a reference.
+
+Copyright: 2020 The Delta Lake Project Authors.
+Home page: https://delta.io/
+License: https://www.apache.org/licenses/LICENSE-2.0
+
+--------------------------------------------------------------------------------
+
+This product includes code from Apache Commons.
+
+* Core ArrayUtil.
+
+Copyright: 2020 The Apache Software Foundation
+Home page: https://commons.apache.org/
+License: https://www.apache.org/licenses/LICENSE-2.0
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..48df82e
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,8 @@
+
+Apache Iceberg
+Copyright 2023 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+--------------------------------------------------------------------------------
diff --git a/README.md b/README.md
index 5f12f79..2ad172e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,26 @@
-# iceberg-go
+<!--
+ - 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.
+ -->
+
+# Iceberg Golang
+
+[![Go 
Reference](https://pkg.go.dev/badge/github.com/apache/iceberg-go.svg)](https://pkg.go.dev/github.com/apache/iceberg-go)
+
+`iceberg` is a Golang implementation of the [Iceberg table 
spec](https://iceberg.apache.org/spec/).
+
+# Get in Touch
+
+- [Iceberg community](https://iceberg.apache.org/community/)
\ No newline at end of file
diff --git a/dev/.rat-excludes b/dev/.rat-excludes
new file mode 100644
index 0000000..13d4c64
--- /dev/null
+++ b/dev/.rat-excludes
@@ -0,0 +1,5 @@
+.gitignore
+.rat-excludes
+LICENSE
+NOTICE
+go.sum
\ No newline at end of file
diff --git a/dev/check-license b/dev/check-license
new file mode 100755
index 0000000..23d22f7
--- /dev/null
+++ b/dev/check-license
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+
+# 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.
+
+acquire_rat_jar () {
+
+  
URL="https://repo.maven.apache.org/maven2/org/apache/rat/apache-rat/${RAT_VERSION}/apache-rat-${RAT_VERSION}.jar";
+
+  JAR="$rat_jar"
+
+  # Download rat launch jar if it hasn't been downloaded yet
+  if [ ! -f "$JAR" ]; then
+    # Download
+    printf "Attempting to fetch rat\n"
+    JAR_DL="${JAR}.part"
+    if [ $(command -v curl) ]; then
+      curl -L --silent "${URL}" > "$JAR_DL" && mv "$JAR_DL" "$JAR"
+    elif [ $(command -v wget) ]; then
+      wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
+    else
+      printf "You do not have curl or wget installed, please install rat 
manually.\n"
+      exit -1
+    fi
+  fi
+
+  unzip -tq "$JAR" &> /dev/null
+  if [ $? -ne 0 ]; then 
+    # We failed to download
+    rm "$JAR"
+    printf "Our attempt to download rat locally to ${JAR} failed. Please 
install rat manually.\n"
+    exit -1
+  fi
+}
+
+# Go to the Spark project root directory
+FWDIR="$(cd "`dirname "$0"`"/..; pwd)"
+cd "$FWDIR"
+
+if test -x "$JAVA_HOME/bin/java"; then
+    declare java_cmd="$JAVA_HOME/bin/java"
+else
+    declare java_cmd=java
+fi
+
+export RAT_VERSION=0.15
+export rat_jar="$FWDIR"/lib/apache-rat-${RAT_VERSION}.jar
+mkdir -p "$FWDIR"/lib
+
+[[ -f "$rat_jar" ]] || acquire_rat_jar || {
+    echo "Download failed. Obtain the rat jar manually and place it at 
$rat_jar"
+    exit 1
+}
+
+mkdir -p build
+$java_cmd -jar "$rat_jar" -E "$FWDIR"/dev/.rat-excludes -d "$FWDIR" > 
build/rat-results.txt
+
+if [ $? -ne 0 ]; then
+   echo "RAT exited abnormally"
+   exit 1
+fi
+
+ERRORS="$(cat build/rat-results.txt | grep -e "??")"
+
+if test ! -z "$ERRORS"; then 
+    echo "Could not find Apache license headers in the following files:"
+    echo "$ERRORS"
+    exit 1
+else 
+    echo -e "RAT checks passed."
+fi
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..801f5c5
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg
+
+import "errors"
+
+var (
+       ErrInvalidTypeString = errors.New("invalid type")
+       ErrNotImplemented    = errors.New("not implemented")
+       ErrInvalidArgument   = errors.New("invalid argument")
+       ErrInvalidSchema     = errors.New("invalid schema")
+)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..030d65e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,31 @@
+// 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.
+
+module github.com/apache/iceberg-go
+
+go 1.20
+
+require (
+       github.com/stretchr/testify v1.8.4
+       golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
+)
+
+require (
+       github.com/davecgh/go-spew v1.1.1 // indirect
+       github.com/pmezard/go-difflib v1.0.0 // indirect
+       gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..172fe17
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,12 @@
+github.com/davecgh/go-spew v1.1.1 
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.8.4 
h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod 
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb 
h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
+golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod 
h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 
h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/schema.go b/schema.go
new file mode 100644
index 0000000..e9e559b
--- /dev/null
+++ b/schema.go
@@ -0,0 +1,860 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg
+
+import (
+       "encoding/json"
+       "fmt"
+       "strings"
+       "sync/atomic"
+
+       "golang.org/x/exp/maps"
+       "golang.org/x/exp/slices"
+)
+
+// Schema is an Iceberg table schema, represented as a struct with
+// multiple fields. The fields are only exported via accessor methods
+// rather than exposing the slice directly in order to ensure a schema
+// as immutable.
+type Schema struct {
+       ID                 int   `json:"schema-id"`
+       IdentifierFieldIDs []int `json:"identifier-field-ids"`
+
+       fields []NestedField
+
+       // the following maps are lazily populated as needed.
+       // rather than have lock contention with a mutex, we can use
+       // atomic pointers to Store/Load the values.
+       idToName      atomic.Pointer[map[int]string]
+       idToField     atomic.Pointer[map[int]NestedField]
+       nameToID      atomic.Pointer[map[string]int]
+       nameToIDLower atomic.Pointer[map[string]int]
+}
+
+// NewSchema constructs a new schema with the provided ID
+// and list of fields.
+func NewSchema(id int, fields ...NestedField) *Schema {
+       return NewSchemaWithIdentifiers(id, []int{}, fields...)
+}
+
+// NewSchemaWithIdentifiers constructs a new schema with the provided ID
+// and fields, along with a slice of field IDs to be listed as identifier
+// fields.
+func NewSchemaWithIdentifiers(id int, identifierIDs []int, fields 
...NestedField) *Schema {
+       return &Schema{ID: id, fields: fields, IdentifierFieldIDs: 
identifierIDs}
+}
+
+func (s *Schema) String() string {
+       var b strings.Builder
+       b.WriteString("table {")
+       for _, f := range s.fields {
+               b.WriteString("\n\t")
+               b.WriteString(f.String())
+       }
+       b.WriteString("\n}")
+       return b.String()
+}
+
+func (s *Schema) lazyNameToID() (map[string]int, error) {
+       index := s.nameToID.Load()
+       if index != nil {
+               return *index, nil
+       }
+
+       idx, err := IndexByName(s)
+       if err != nil {
+               return nil, err
+       }
+
+       s.nameToID.Store(&idx)
+       return idx, nil
+}
+
+func (s *Schema) lazyIDToField() (map[int]NestedField, error) {
+       index := s.idToField.Load()
+       if index != nil {
+               return *index, nil
+       }
+
+       idx, err := IndexByID(s)
+       if err != nil {
+               return nil, err
+       }
+
+       s.idToField.Store(&idx)
+       return idx, nil
+}
+
+func (s *Schema) lazyIDToName() (map[int]string, error) {
+       index := s.idToName.Load()
+       if index != nil {
+               return *index, nil
+       }
+
+       idx, err := IndexNameByID(s)
+       if err != nil {
+               return nil, err
+       }
+
+       s.idToName.Store(&idx)
+       return idx, nil
+}
+
+func (s *Schema) lazyNameToIDLower() (map[string]int, error) {
+       index := s.nameToIDLower.Load()
+       if index != nil {
+               return *index, nil
+       }
+
+       idx, err := s.lazyNameToID()
+       if err != nil {
+               return nil, err
+       }
+
+       out := make(map[string]int)
+       for k, v := range idx {
+               out[strings.ToLower(k)] = v
+       }
+
+       s.nameToIDLower.Store(&out)
+       return out, nil
+}
+
+func (s *Schema) Type() string { return "struct" }
+
+// AsStruct returns a Struct with the same fields as the schema which can
+// then be used as a Type.
+func (s *Schema) AsStruct() StructType    { return StructType{FieldList: 
s.fields} }
+func (s *Schema) NumFields() int          { return len(s.fields) }
+func (s *Schema) Field(i int) NestedField { return s.fields[i] }
+func (s *Schema) Fields() []NestedField   { return slices.Clone(s.fields) }
+
+func (s *Schema) UnmarshalJSON(b []byte) error {
+       type Alias Schema
+       aux := struct {
+               Fields []NestedField `json:"fields"`
+               *Alias
+       }{Alias: (*Alias)(s)}
+
+       if err := json.Unmarshal(b, &aux); err != nil {
+               return err
+       }
+
+       s.fields = aux.Fields
+       if s.IdentifierFieldIDs == nil {
+               s.IdentifierFieldIDs = []int{}
+       }
+       return nil
+}
+
+func (s *Schema) MarshalJSON() ([]byte, error) {
+       if s.IdentifierFieldIDs == nil {
+               s.IdentifierFieldIDs = []int{}
+       }
+
+       type Alias Schema
+       return json.Marshal(struct {
+               Type   string        `json:"type"`
+               Fields []NestedField `json:"fields"`
+               *Alias
+       }{Type: "struct", Fields: s.fields, Alias: (*Alias)(s)})
+}
+
+// FindColumnName returns the name of the column identified by the
+// passed in field id. The second return value reports whether or
+// not the field id was found in the schema.
+func (s *Schema) FindColumnName(fieldID int) (string, bool) {
+       idx, _ := s.lazyIDToName()
+       col, ok := idx[fieldID]
+       return col, ok
+}
+
+// FindFieldByName returns the field identified by the name given,
+// the second return value will be false if no field by this name
+// is found.
+//
+// Note: This search is done in a case sensitive manner. To perform
+// a case insensitive search, use [*Schema.FindFieldByNameCaseInsensitive].
+func (s *Schema) FindFieldByName(name string) (NestedField, bool) {
+       idx, _ := s.lazyNameToID()
+
+       id, ok := idx[name]
+       if !ok {
+               return NestedField{}, false
+       }
+
+       return s.FindFieldByID(id)
+}
+
+// FindFieldByNameCaseInsensitive is like [*Schema.FindFieldByName],
+// but performs a case insensitive search.
+func (s *Schema) FindFieldByNameCaseInsensitive(name string) (NestedField, 
bool) {
+       idx, _ := s.lazyNameToIDLower()
+
+       id, ok := idx[strings.ToLower(name)]
+       if !ok {
+               return NestedField{}, false
+       }
+
+       return s.FindFieldByID(id)
+}
+
+// FindFieldByID is like [*Schema.FindColumnByName], but returns the whole
+// field rather than just the field name.
+func (s *Schema) FindFieldByID(id int) (NestedField, bool) {
+       idx, _ := s.lazyIDToField()
+       f, ok := idx[id]
+       return f, ok
+}
+
+// FindTypeByID is like [*Schema.FindFieldByID], but returns only the data
+// type of the field.
+func (s *Schema) FindTypeByID(id int) (Type, bool) {
+       f, ok := s.FindFieldByID(id)
+       if !ok {
+               return nil, false
+       }
+
+       return f.Type, true
+}
+
+// FindTypeByName is a convenience function for calling 
[*Schema.FindFieldByName],
+// and then returning just the type.
+func (s *Schema) FindTypeByName(name string) (Type, bool) {
+       f, ok := s.FindFieldByName(name)
+       if !ok {
+               return nil, false
+       }
+
+       return f.Type, true
+}
+
+// FindTypeByNameCaseInsensitive is like [*Schema.FindTypeByName] but
+// performs a case insensitive search.
+func (s *Schema) FindTypeByNameCaseInsensitive(name string) (Type, bool) {
+       f, ok := s.FindFieldByNameCaseInsensitive(name)
+       if !ok {
+               return nil, false
+       }
+
+       return f.Type, true
+}
+
+// Equals compares the fields and identifierIDs, but does not compare
+// the schema ID itself.
+func (s *Schema) Equals(other *Schema) bool {
+       if other == nil {
+               return false
+       }
+
+       if s == other {
+               return true
+       }
+
+       if len(s.fields) != len(other.fields) {
+               return false
+       }
+
+       if !slices.Equal(s.IdentifierFieldIDs, other.IdentifierFieldIDs) {
+               return false
+       }
+
+       return slices.EqualFunc(s.fields, other.fields, func(a, b NestedField) 
bool {
+               return a.Equals(b)
+       })
+}
+
+// HighestFieldID returns the value of the numerically highest field ID
+// in this schema.
+func (s *Schema) HighestFieldID() int {
+       id, _ := Visit[int](s, findLastFieldID{})
+       return id
+}
+
+type Void = struct{}
+
+var void = Void{}
+
+// Select creates a new schema with just the fields identified by name
+// passed in the order they are provided. If caseSensitive is false,
+// then fields will be identified by case insensitive search.
+//
+// An error is returned if a requested name cannot be found.
+func (s *Schema) Select(caseSensitive bool, names ...string) (*Schema, error) {
+       ids := make(map[int]Void)
+       if caseSensitive {
+               nameMap, _ := s.lazyNameToID()
+               for _, n := range names {
+                       id, ok := nameMap[n]
+                       if !ok {
+                               return nil, fmt.Errorf("%w: could not find 
column %s", ErrInvalidSchema, n)
+                       }
+                       ids[id] = void
+               }
+       } else {
+               nameMap, _ := s.lazyNameToIDLower()
+               for _, n := range names {
+                       id, ok := nameMap[strings.ToLower(n)]
+                       if !ok {
+                               return nil, fmt.Errorf("%w: could not find 
column %s", ErrInvalidSchema, n)
+                       }
+                       ids[id] = void
+               }
+       }
+
+       return PruneColumns(s, ids, true)
+}
+
+// SchemaVisitor is an interface that can be implemented to allow for
+// easy traversal and processing of a schema.
+//
+// A SchemaVisitor can also optionally implement the Before/After Field,
+// ListElement, MapKey, or MapValue interfaces to allow them to get called
+// at the appropriate points within schema traversal.
+type SchemaVisitor[T any] interface {
+       Schema(schema *Schema, structResult T) T
+       Struct(st StructType, fieldResults []T) T
+       Field(field NestedField, fieldResult T) T
+       List(list ListType, elemResult T) T
+       Map(mapType MapType, keyResult, valueResult T) T
+       Primitive(p PrimitiveType) T
+}
+
+type BeforeFieldVisitor interface {
+       BeforeField(field NestedField)
+}
+
+type AfterFieldVisitor interface {
+       AfterField(field NestedField)
+}
+
+type BeforeListElementVisitor interface {
+       BeforeListElement(elem NestedField)
+}
+
+type AfterListElementVisitor interface {
+       AfterListElement(elem NestedField)
+}
+
+type BeforeMapKeyVisitor interface {
+       BeforeMapKey(key NestedField)
+}
+
+type AfterMapKeyVisitor interface {
+       AfterMapKey(key NestedField)
+}
+
+type BeforeMapValueVisitor interface {
+       BeforeMapValue(value NestedField)
+}
+
+type AfterMapValueVisitor interface {
+       AfterMapValue(value NestedField)
+}
+
+// Visit accepts a visitor and performs a post-order traversal of the given 
schema.
+func Visit[T any](sc *Schema, visitor SchemaVisitor[T]) (res T, err error) {
+       if sc == nil {
+               err = fmt.Errorf("%w: cannot visit nil schema", 
ErrInvalidArgument)
+               return
+       }
+
+       defer func() {
+               if r := recover(); r != nil {
+                       switch e := r.(type) {
+                       case string:
+                               err = fmt.Errorf("error encountered during 
schema visitor: %s", e)
+                       case error:
+                               err = fmt.Errorf("error encountered during 
schema visitor: %w", e)
+                       }
+               }
+       }()
+
+       return visitor.Schema(sc, visitStruct(sc.AsStruct(), visitor)), nil
+}
+
+func visitStruct[T any](obj StructType, visitor SchemaVisitor[T]) T {
+       results := make([]T, len(obj.FieldList))
+
+       bf, _ := visitor.(BeforeFieldVisitor)
+       af, _ := visitor.(AfterFieldVisitor)
+
+       for i, f := range obj.FieldList {
+               if bf != nil {
+                       bf.BeforeField(f)
+               }
+
+               res := visitField(f, visitor)
+
+               if af != nil {
+                       af.AfterField(f)
+               }
+
+               results[i] = visitor.Field(f, res)
+       }
+
+       return visitor.Struct(obj, results)
+}
+
+func visitList[T any](obj ListType, visitor SchemaVisitor[T]) T {
+       elemField := obj.ElementField()
+
+       if bl, ok := visitor.(BeforeListElementVisitor); ok {
+               bl.BeforeListElement(elemField)
+       } else if bf, ok := visitor.(BeforeFieldVisitor); ok {
+               bf.BeforeField(elemField)
+       }
+
+       res := visitField(elemField, visitor)
+
+       if al, ok := visitor.(AfterListElementVisitor); ok {
+               al.AfterListElement(elemField)
+       } else if af, ok := visitor.(AfterFieldVisitor); ok {
+               af.AfterField(elemField)
+       }
+
+       return visitor.List(obj, res)
+}
+
+func visitMap[T any](obj MapType, visitor SchemaVisitor[T]) T {
+       keyField, valueField := obj.KeyField(), obj.ValueField()
+
+       if bmk, ok := visitor.(BeforeMapKeyVisitor); ok {
+               bmk.BeforeMapKey(keyField)
+       } else if bf, ok := visitor.(BeforeFieldVisitor); ok {
+               bf.BeforeField(keyField)
+       }
+
+       keyRes := visitField(keyField, visitor)
+
+       if amk, ok := visitor.(AfterMapKeyVisitor); ok {
+               amk.AfterMapKey(keyField)
+       } else if af, ok := visitor.(AfterFieldVisitor); ok {
+               af.AfterField(keyField)
+       }
+
+       if bmk, ok := visitor.(BeforeMapValueVisitor); ok {
+               bmk.BeforeMapValue(valueField)
+       } else if bf, ok := visitor.(BeforeFieldVisitor); ok {
+               bf.BeforeField(valueField)
+       }
+
+       valueRes := visitField(valueField, visitor)
+
+       if amk, ok := visitor.(AfterMapValueVisitor); ok {
+               amk.AfterMapValue(valueField)
+       } else if af, ok := visitor.(AfterFieldVisitor); ok {
+               af.AfterField(valueField)
+       }
+
+       return visitor.Map(obj, keyRes, valueRes)
+}
+
+func visitField[T any](f NestedField, visitor SchemaVisitor[T]) T {
+       switch typ := f.Type.(type) {
+       case *StructType:
+               return visitStruct(*typ, visitor)
+       case *ListType:
+               return visitList(*typ, visitor)
+       case *MapType:
+               return visitMap(*typ, visitor)
+       default: // primitive
+               return visitor.Primitive(typ.(PrimitiveType))
+       }
+}
+
+// IndexByID performs a post-order traversal of the given schema and
+// returns a mapping from field ID to field.
+func IndexByID(schema *Schema) (map[int]NestedField, error) {
+       return Visit[map[int]NestedField](schema, &indexByID{index: 
make(map[int]NestedField)})
+}
+
+type indexByID struct {
+       index map[int]NestedField
+}
+
+func (i *indexByID) Schema(*Schema, map[int]NestedField) map[int]NestedField {
+       return i.index
+}
+
+func (i *indexByID) Struct(StructType, []map[int]NestedField) 
map[int]NestedField {
+       return i.index
+}
+
+func (i *indexByID) Field(field NestedField, _ map[int]NestedField) 
map[int]NestedField {
+       i.index[field.ID] = field
+       return i.index
+}
+
+func (i *indexByID) List(list ListType, _ map[int]NestedField) 
map[int]NestedField {
+       i.index[list.ElementID] = list.ElementField()
+       return i.index
+}
+
+func (i *indexByID) Map(mapType MapType, _, _ map[int]NestedField) 
map[int]NestedField {
+       i.index[mapType.KeyID] = mapType.KeyField()
+       i.index[mapType.ValueID] = mapType.ValueField()
+       return i.index
+}
+
+func (i *indexByID) Primitive(PrimitiveType) map[int]NestedField {
+       return i.index
+}
+
+// IndexByName performs a post-order traversal of the schema and returns
+// a mapping from field name to field ID.
+func IndexByName(schema *Schema) (map[string]int, error) {
+       if schema == nil {
+               return nil, fmt.Errorf("%w: cannot index nil schema", 
ErrInvalidArgument)
+       }
+
+       if len(schema.fields) > 0 {
+               indexer := &indexByName{
+                       index:           make(map[string]int),
+                       shortNameId:     make(map[string]int),
+                       fieldNames:      make([]string, 0),
+                       shortFieldNames: make([]string, 0),
+               }
+               if _, err := Visit[map[string]int](schema, indexer); err != nil 
{
+                       return nil, err
+               }
+
+               return indexer.ByName(), nil
+       }
+       return map[string]int{}, nil
+}
+
+// IndexNameByID performs a post-order traversal of the schema and returns
+// a mapping from field ID to field name.
+func IndexNameByID(schema *Schema) (map[int]string, error) {
+       indexer := &indexByName{
+               index:           make(map[string]int),
+               shortNameId:     make(map[string]int),
+               fieldNames:      make([]string, 0),
+               shortFieldNames: make([]string, 0),
+       }
+       if _, err := Visit[map[string]int](schema, indexer); err != nil {
+               return nil, err
+       }
+       return indexer.ByID(), nil
+}
+
+type indexByName struct {
+       index           map[string]int
+       shortNameId     map[string]int
+       combinedIndex   map[string]int
+       fieldNames      []string
+       shortFieldNames []string
+}
+
+func (i *indexByName) ByID() map[int]string {
+       idToName := make(map[int]string)
+       for k, v := range i.index {
+               idToName[v] = k
+       }
+       return idToName
+}
+
+func (i *indexByName) ByName() map[string]int {
+       i.combinedIndex = maps.Clone(i.shortNameId)
+       maps.Copy(i.combinedIndex, i.index)
+       return i.combinedIndex
+}
+
+func (i *indexByName) Primitive(PrimitiveType) map[string]int { return i.index 
}
+func (i *indexByName) addField(name string, fieldID int) {
+       fullName := name
+       if len(i.fieldNames) > 0 {
+               fullName = strings.Join(i.fieldNames, ".") + "." + name
+       }
+
+       if _, ok := i.index[fullName]; ok {
+               panic(fmt.Errorf("%w: multiple fields for name %s: %d and %d",
+                       ErrInvalidSchema, fullName, i.index[fullName], fieldID))
+       }
+
+       i.index[fullName] = fieldID
+       if len(i.shortFieldNames) > 0 {
+               shortName := strings.Join(i.shortFieldNames, ".") + "." + name
+               i.shortNameId[shortName] = fieldID
+       }
+}
+
+func (i *indexByName) Schema(*Schema, map[string]int) map[string]int {
+       return i.index
+}
+
+func (i *indexByName) Struct(StructType, []map[string]int) map[string]int {
+       return i.index
+}
+
+func (i *indexByName) Field(field NestedField, _ map[string]int) 
map[string]int {
+       i.addField(field.Name, field.ID)
+       return i.index
+}
+
+func (i *indexByName) List(list ListType, _ map[string]int) map[string]int {
+       i.addField(list.ElementField().Name, list.ElementID)
+       return i.index
+}
+
+func (i *indexByName) Map(mapType MapType, _, _ map[string]int) map[string]int 
{
+       i.addField(mapType.KeyField().Name, mapType.KeyID)
+       i.addField(mapType.ValueField().Name, mapType.ValueID)
+       return i.index
+}
+
+func (i *indexByName) BeforeListElement(elem NestedField) {
+       if _, ok := elem.Type.(*StructType); !ok {
+               i.shortFieldNames = append(i.shortFieldNames, elem.Name)
+       }
+       i.fieldNames = append(i.fieldNames, elem.Name)
+}
+
+func (i *indexByName) AfterListElement(elem NestedField) {
+       if _, ok := elem.Type.(*StructType); !ok {
+               i.shortFieldNames = i.shortFieldNames[:len(i.shortFieldNames)-1]
+       }
+       i.fieldNames = i.fieldNames[:len(i.fieldNames)-1]
+}
+
+func (i *indexByName) BeforeField(field NestedField) {
+       i.fieldNames = append(i.fieldNames, field.Name)
+       i.shortFieldNames = append(i.shortFieldNames, field.Name)
+}
+
+func (i *indexByName) AfterField(field NestedField) {
+       i.fieldNames = i.fieldNames[:len(i.fieldNames)-1]
+       i.shortFieldNames = i.shortFieldNames[:len(i.shortFieldNames)-1]
+}
+
+// PruneColumns visits a schema pruning any columns which do not exist in the
+// provided selected set. Parent fields of a selected child will be retained.
+func PruneColumns(schema *Schema, selected map[int]Void, selectFullTypes bool) 
(*Schema, error) {
+
+       result, err := Visit[Type](schema, &pruneColVisitor{selected: selected,
+               fullTypes: selectFullTypes})
+       if err != nil {
+               return nil, err
+       }
+
+       n, ok := result.(NestedType)
+       if !ok {
+               n = &StructType{}
+       }
+
+       newIdentifierIDs := make([]int, 0, len(schema.IdentifierFieldIDs))
+       for _, id := range schema.IdentifierFieldIDs {
+               if _, ok := selected[id]; ok {
+                       newIdentifierIDs = append(newIdentifierIDs, id)
+               }
+       }
+
+       return &Schema{
+               fields:             n.Fields(),
+               ID:                 schema.ID,
+               IdentifierFieldIDs: newIdentifierIDs,
+       }, nil
+}
+
+type pruneColVisitor struct {
+       selected  map[int]Void
+       fullTypes bool
+}
+
+func (p *pruneColVisitor) Schema(_ *Schema, structResult Type) Type {
+       return structResult
+}
+
+func (p *pruneColVisitor) Struct(st StructType, fieldResults []Type) Type {
+       selected, fields := []NestedField{}, st.FieldList
+       sameType := true
+
+       for i, t := range fieldResults {
+               field := fields[i]
+               if field.Type == t {
+                       selected = append(selected, field)
+               } else if t != nil {
+                       sameType = false
+                       // type has changed, create a new field with the 
projected type
+                       selected = append(selected, NestedField{
+                               ID:       field.ID,
+                               Name:     field.Name,
+                               Type:     t,
+                               Doc:      field.Doc,
+                               Required: field.Required,
+                       })
+               }
+       }
+
+       if len(selected) > 0 {
+               if len(selected) == len(fields) && sameType {
+                       // nothing changed, return the original
+                       return &st
+               } else {
+                       return &StructType{FieldList: selected}
+               }
+       }
+
+       return nil
+}
+
+func (p *pruneColVisitor) Field(field NestedField, fieldResult Type) Type {
+       _, ok := p.selected[field.ID]
+       if !ok {
+               if fieldResult != nil {
+                       return fieldResult
+               }
+
+               return nil
+       }
+
+       if p.fullTypes {
+               return field.Type
+       }
+
+       if _, ok := field.Type.(*StructType); ok {
+               return p.projectSelectedStruct(fieldResult)
+       }
+
+       typ, ok := field.Type.(PrimitiveType)
+       if !ok {
+               panic(fmt.Errorf("%w: cannot explicitly project List or Map 
types, %d:%s of type %s was selected",
+                       ErrInvalidSchema, field.ID, field.Name, field.Type))
+       }
+       return typ
+}
+
+func (p *pruneColVisitor) List(list ListType, elemResult Type) Type {
+       _, ok := p.selected[list.ElementID]
+       if !ok {
+               if elemResult != nil {
+                       return p.projectList(&list, elemResult)
+               }
+
+               return nil
+       }
+
+       if p.fullTypes {
+               return &list
+       }
+
+       _, ok = list.Element.(*StructType)
+       if list.Element != nil && ok {
+               projected := p.projectSelectedStruct(elemResult)
+               return p.projectList(&list, projected)
+       }
+
+       if _, ok = list.Element.(PrimitiveType); !ok {
+               panic(fmt.Errorf("%w: cannot explicitly project List or Map 
types, %d of type %s was selected",
+                       ErrInvalidSchema, list.ElementID, list.Element))
+       }
+
+       return &list
+}
+
+func (p *pruneColVisitor) Map(mapType MapType, keyResult, valueResult Type) 
Type {
+       _, ok := p.selected[mapType.ValueID]
+       if !ok {
+               if valueResult != nil {
+                       return p.projectMap(&mapType, valueResult)
+               }
+
+               if _, ok = p.selected[mapType.KeyID]; ok {
+                       return &mapType
+               }
+
+               return nil
+       }
+
+       if p.fullTypes {
+               return &mapType
+       }
+
+       _, ok = mapType.ValueType.(*StructType)
+       if mapType.ValueType != nil && ok {
+               projected := p.projectSelectedStruct(valueResult)
+               return p.projectMap(&mapType, projected)
+       }
+
+       if _, ok = mapType.ValueType.(PrimitiveType); !ok {
+               panic(fmt.Errorf("%w: cannot explicitly project List or Map 
types, Map value %d of type %s was selected",
+                       ErrInvalidSchema, mapType.ValueID, mapType.ValueType))
+       }
+
+       return &mapType
+}
+
+func (p *pruneColVisitor) Primitive(_ PrimitiveType) Type { return nil }
+
+func (*pruneColVisitor) projectSelectedStruct(projected Type) *StructType {
+       if projected == nil {
+               return &StructType{}
+       }
+
+       if ty, ok := projected.(*StructType); ok {
+               return ty
+       }
+
+       panic("expected a struct")
+}
+
+func (*pruneColVisitor) projectList(listType *ListType, elementResult Type) 
*ListType {
+       if listType.Element.Equals(elementResult) {
+               return listType
+       }
+
+       return &ListType{ElementID: listType.ElementID, Element: elementResult,
+               ElementRequired: listType.ElementRequired}
+}
+
+func (*pruneColVisitor) projectMap(mapType *MapType, valueResult Type) 
*MapType {
+       if mapType.ValueType.Equals(valueResult) {
+               return mapType
+       }
+
+       return &MapType{
+               KeyID:         mapType.KeyID,
+               ValueID:       mapType.ValueID,
+               KeyType:       mapType.KeyType,
+               ValueType:     valueResult,
+               ValueRequired: mapType.ValueRequired,
+       }
+}
+
+type findLastFieldID struct{}
+
+func (findLastFieldID) Schema(_ *Schema, result int) int {
+       return result
+}
+
+func (findLastFieldID) Struct(_ StructType, fieldResults []int) int {
+       return max(fieldResults...)
+}
+
+func (findLastFieldID) Field(field NestedField, fieldResult int) int {
+       return max(field.ID, fieldResult)
+}
+
+func (findLastFieldID) List(_ ListType, elemResult int) int { return 
elemResult }
+
+func (findLastFieldID) Map(_ MapType, keyResult, valueResult int) int {
+       return max(keyResult, valueResult)
+}
+
+func (findLastFieldID) Primitive(PrimitiveType) int { return 0 }
diff --git a/schema_test.go b/schema_test.go
new file mode 100644
index 0000000..9190d8b
--- /dev/null
+++ b/schema_test.go
@@ -0,0 +1,758 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg_test
+
+import (
+       "encoding/json"
+       "strings"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+var (
+       tableSchemaNested = iceberg.NewSchemaWithIdentifiers(1,
+               []int{1},
+               iceberg.NestedField{
+                       ID: 1, Name: "foo", Type: 
iceberg.PrimitiveTypes.String, Required: false},
+               iceberg.NestedField{
+                       ID: 2, Name: "bar", Type: iceberg.PrimitiveTypes.Int32, 
Required: true},
+               iceberg.NestedField{
+                       ID: 3, Name: "baz", Type: iceberg.PrimitiveTypes.Bool, 
Required: false},
+               iceberg.NestedField{
+                       ID: 4, Name: "qux", Required: true, Type: 
&iceberg.ListType{
+                               ElementID: 5, Element: 
iceberg.PrimitiveTypes.String, ElementRequired: true}},
+               iceberg.NestedField{
+                       ID: 6, Name: "quux",
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.String,
+                               ValueID: 8,
+                               ValueType: &iceberg.MapType{
+                                       KeyID:         9,
+                                       KeyType:       
iceberg.PrimitiveTypes.String,
+                                       ValueID:       10,
+                                       ValueType:     
iceberg.PrimitiveTypes.Int32,
+                                       ValueRequired: true,
+                               },
+                               ValueRequired: true,
+                       },
+                       Required: true},
+               iceberg.NestedField{
+                       ID: 11, Name: "location", Type: &iceberg.ListType{
+                               ElementID: 12, Element: &iceberg.StructType{
+                                       FieldList: []iceberg.NestedField{
+                                               {ID: 13, Name: "latitude", 
Type: iceberg.PrimitiveTypes.Float32, Required: false},
+                                               {ID: 14, Name: "longitude", 
Type: iceberg.PrimitiveTypes.Float32, Required: false},
+                                       },
+                               },
+                               ElementRequired: true},
+                       Required: true},
+               iceberg.NestedField{
+                       ID:   15,
+                       Name: "person",
+                       Type: &iceberg.StructType{
+                               FieldList: []iceberg.NestedField{
+                                       {ID: 16, Name: "name", Type: 
iceberg.PrimitiveTypes.String, Required: false},
+                                       {ID: 17, Name: "age", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                               },
+                       },
+                       Required: false,
+               },
+       )
+
+       tableSchemaSimple = iceberg.NewSchemaWithIdentifiers(1,
+               []int{2},
+               iceberg.NestedField{ID: 1, Name: "foo", Type: 
iceberg.PrimitiveTypes.String},
+               iceberg.NestedField{ID: 2, Name: "bar", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+               iceberg.NestedField{ID: 3, Name: "baz", Type: 
iceberg.PrimitiveTypes.Bool},
+       )
+)
+
+func TestSchemaToString(t *testing.T) {
+       assert.Equal(t, 3, tableSchemaSimple.NumFields())
+       assert.Equal(t, `table {
+       1: foo: optional string
+       2: bar: required int
+       3: baz: optional boolean
+}`, tableSchemaSimple.String())
+}
+
+func TestNestedFieldToString(t *testing.T) {
+       tests := []struct {
+               idx      int
+               expected string
+       }{
+               {0, "1: foo: optional string"},
+               {1, "2: bar: required int"},
+               {2, "3: baz: optional boolean"},
+               {3, "4: qux: required list<string>"},
+               {4, "6: quux: required map<string, map<string, int>>"},
+               {5, "11: location: required list<struct<13: latitude: float, 
14: longitude: float>>"},
+               {6, "15: person: optional struct<16: name: string, 17: age: 
required int>"},
+       }
+
+       for _, tt := range tests {
+               assert.Equal(t, tt.expected, 
tableSchemaNested.Field(tt.idx).String())
+       }
+}
+
+func TestSchemaIndexByIDVisitor(t *testing.T) {
+       index, err := iceberg.IndexByID(tableSchemaNested)
+       require.NoError(t, err)
+
+       assert.Equal(t, map[int]iceberg.NestedField{
+               1: tableSchemaNested.Field(0),
+               2: tableSchemaNested.Field(1),
+               3: tableSchemaNested.Field(2),
+               4: tableSchemaNested.Field(3),
+               5: {ID: 5, Name: "element", Type: 
iceberg.PrimitiveTypes.String, Required: true},
+               6: tableSchemaNested.Field(4),
+               7: {ID: 7, Name: "key", Type: iceberg.PrimitiveTypes.String, 
Required: true},
+               8: {ID: 8, Name: "value", Type: &iceberg.MapType{
+                       KeyID:         9,
+                       KeyType:       iceberg.PrimitiveTypes.String,
+                       ValueID:       10,
+                       ValueType:     iceberg.PrimitiveTypes.Int32,
+                       ValueRequired: true,
+               }, Required: true},
+               9:  {ID: 9, Name: "key", Type: iceberg.PrimitiveTypes.String, 
Required: true},
+               10: {ID: 10, Name: "value", Type: iceberg.PrimitiveTypes.Int32, 
Required: true},
+               11: tableSchemaNested.Field(5),
+               12: {ID: 12, Name: "element", Type: &iceberg.StructType{
+                       FieldList: []iceberg.NestedField{
+                               {ID: 13, Name: "latitude", Type: 
iceberg.PrimitiveTypes.Float32, Required: false},
+                               {ID: 14, Name: "longitude", Type: 
iceberg.PrimitiveTypes.Float32, Required: false},
+                       },
+               }, Required: true},
+               13: {ID: 13, Name: "latitude", Type: 
iceberg.PrimitiveTypes.Float32, Required: false},
+               14: {ID: 14, Name: "longitude", Type: 
iceberg.PrimitiveTypes.Float32, Required: false},
+               15: tableSchemaNested.Field(6),
+               16: {ID: 16, Name: "name", Type: iceberg.PrimitiveTypes.String, 
Required: false},
+               17: {ID: 17, Name: "age", Type: iceberg.PrimitiveTypes.Int32, 
Required: true},
+       }, index)
+}
+
+func TestSchemaIndexByName(t *testing.T) {
+       index, err := iceberg.IndexByName(tableSchemaNested)
+       require.NoError(t, err)
+
+       assert.Equal(t, map[string]int{
+               "foo":                        1,
+               "bar":                        2,
+               "baz":                        3,
+               "qux":                        4,
+               "qux.element":                5,
+               "quux":                       6,
+               "quux.key":                   7,
+               "quux.value":                 8,
+               "quux.value.key":             9,
+               "quux.value.value":           10,
+               "location":                   11,
+               "location.element":           12,
+               "location.element.latitude":  13,
+               "location.element.longitude": 14,
+               "location.latitude":          13,
+               "location.longitude":         14,
+               "person":                     15,
+               "person.name":                16,
+               "person.age":                 17,
+       }, index)
+}
+
+func TestSchemaFindColumnName(t *testing.T) {
+       tests := []struct {
+               id   int
+               name string
+       }{
+               {1, "foo"},
+               {2, "bar"},
+               {3, "baz"},
+               {4, "qux"},
+               {5, "qux.element"},
+               {6, "quux"},
+               {7, "quux.key"},
+               {8, "quux.value"},
+               {9, "quux.value.key"},
+               {10, "quux.value.value"},
+               {11, "location"},
+               {12, "location.element"},
+               {13, "location.element.latitude"},
+               {14, "location.element.longitude"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       n, ok := tableSchemaNested.FindColumnName(tt.id)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt.name, n)
+               })
+       }
+}
+
+func TestSchemaFindColumnNameIDNotFound(t *testing.T) {
+       n, ok := tableSchemaNested.FindColumnName(99)
+       assert.False(t, ok)
+       assert.Empty(t, n)
+}
+
+func TestSchemaFindColumnNameByID(t *testing.T) {
+       tests := []struct {
+               id   int
+               name string
+       }{
+               {1, "foo"},
+               {2, "bar"},
+               {3, "baz"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       n, ok := tableSchemaSimple.FindColumnName(tt.id)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt.name, n)
+               })
+       }
+}
+
+func TestSchemaFindFieldByID(t *testing.T) {
+       index, err := iceberg.IndexByID(tableSchemaSimple)
+       require.NoError(t, err)
+
+       col1 := index[1]
+       assert.Equal(t, 1, col1.ID)
+       assert.Equal(t, iceberg.PrimitiveTypes.String, col1.Type)
+       assert.False(t, col1.Required)
+
+       col2 := index[2]
+       assert.Equal(t, 2, col2.ID)
+       assert.Equal(t, iceberg.PrimitiveTypes.Int32, col2.Type)
+       assert.True(t, col2.Required)
+
+       col3 := index[3]
+       assert.Equal(t, 3, col3.ID)
+       assert.Equal(t, iceberg.PrimitiveTypes.Bool, col3.Type)
+       assert.False(t, col3.Required)
+}
+
+func TestFindFieldByIDUnknownField(t *testing.T) {
+       index, err := iceberg.IndexByID(tableSchemaSimple)
+       require.NoError(t, err)
+       _, ok := index[4]
+       assert.False(t, ok)
+}
+
+func TestSchemaFindField(t *testing.T) {
+       tests := []iceberg.NestedField{
+               {ID: 1, Name: "foo", Type: iceberg.PrimitiveTypes.String, 
Required: false},
+               {ID: 2, Name: "bar", Type: iceberg.PrimitiveTypes.Int32, 
Required: true},
+               {ID: 3, Name: "baz", Type: iceberg.PrimitiveTypes.Bool, 
Required: false},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.Name, func(t *testing.T) {
+                       f, ok := tableSchemaSimple.FindFieldByID(tt.ID)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt, f)
+
+                       f, ok = tableSchemaSimple.FindFieldByName(tt.Name)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt, f)
+
+                       f, ok = 
tableSchemaSimple.FindFieldByNameCaseInsensitive(strings.ToUpper(tt.Name))
+                       assert.True(t, ok)
+                       assert.Equal(t, tt, f)
+               })
+       }
+}
+
+func TestSchemaFindType(t *testing.T) {
+       _, ok := tableSchemaSimple.FindTypeByID(0)
+       assert.False(t, ok)
+       _, ok = tableSchemaSimple.FindTypeByName("FOOBAR")
+       assert.False(t, ok)
+       _, ok = tableSchemaSimple.FindTypeByNameCaseInsensitive("FOOBAR")
+       assert.False(t, ok)
+
+       tests := []struct {
+               id   int
+               name string
+               typ  iceberg.Type
+       }{
+               {1, "foo", iceberg.PrimitiveTypes.String},
+               {2, "bar", iceberg.PrimitiveTypes.Int32},
+               {3, "baz", iceberg.PrimitiveTypes.Bool},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       typ, ok := tableSchemaSimple.FindTypeByID(tt.id)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt.typ, typ)
+
+                       typ, ok = tableSchemaSimple.FindTypeByName(tt.name)
+                       assert.True(t, ok)
+                       assert.Equal(t, tt.typ, typ)
+
+                       typ, ok = 
tableSchemaSimple.FindTypeByNameCaseInsensitive(strings.ToUpper(tt.name))
+                       assert.True(t, ok)
+                       assert.Equal(t, tt.typ, typ)
+               })
+       }
+}
+
+func TestSerializeSchema(t *testing.T) {
+       data, err := json.Marshal(tableSchemaSimple)
+       require.NoError(t, err)
+
+       assert.JSONEq(t, `{
+               "type": "struct",
+               "fields": [
+                       {"id": 1, "name": "foo", "type": "string", "required": 
false},
+                       {"id": 2, "name": "bar", "type": "int", "required": 
true},
+                       {"id": 3, "name": "baz", "type": "boolean", "required": 
false}
+               ],
+               "schema-id": 1,
+               "identifier-field-ids": [2]
+       }`, string(data))
+}
+
+func TestUnmarshalSchema(t *testing.T) {
+       var schema iceberg.Schema
+       require.NoError(t, json.Unmarshal([]byte(`{
+               "type": "struct",
+               "fields": [
+                       {"id": 1, "name": "foo", "type": "string", "required": 
false},
+                       {"id": 2, "name": "bar", "type": "int", "required": 
true},
+                       {"id": 3, "name": "baz", "type": "boolean", "required": 
false}
+               ],
+               "schema-id": 1,
+               "identifier-field-ids": [2]
+       }`), &schema))
+
+       assert.True(t, tableSchemaSimple.Equals(&schema))
+}
+
+func TestPruneColumnsString(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{1: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchemaWithIdentifiers(1, []int{1},
+               iceberg.NestedField{ID: 1, Name: "foo", Type: 
iceberg.PrimitiveTypes.String, Required: false})))
+}
+
+func TestPruneColumnsStringFull(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{1: {}}, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchemaWithIdentifiers(1, []int{1},
+               iceberg.NestedField{ID: 1, Name: "foo", Type: 
iceberg.PrimitiveTypes.String, Required: false})))
+}
+
+func TestPruneColumnsList(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{5: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{ID: 4, Name: "qux", Required: true, Type: 
&iceberg.ListType{
+                       ElementID: 5, Element: iceberg.PrimitiveTypes.String, 
ElementRequired: true,
+               }})))
+}
+
+func TestPruneColumnsListItself(t *testing.T) {
+       _, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{4: {}}, false)
+       assert.ErrorIs(t, err, iceberg.ErrInvalidSchema)
+
+       assert.ErrorContains(t, err, "cannot explicitly project List or Map 
types, 4:qux of type list<string> was selected")
+}
+
+func TestPruneColumnsListFull(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{5: {}}, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{ID: 4, Name: "qux", Required: true, Type: 
&iceberg.ListType{
+                       ElementID: 5, Element: iceberg.PrimitiveTypes.String, 
ElementRequired: true,
+               }})))
+}
+
+func TestPruneColumnsMap(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{9: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "quux",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.String,
+                               ValueID: 8,
+                               ValueType: &iceberg.MapType{
+                                       KeyID:         9,
+                                       KeyType:       
iceberg.PrimitiveTypes.String,
+                                       ValueID:       10,
+                                       ValueType:     
iceberg.PrimitiveTypes.Int32,
+                                       ValueRequired: true,
+                               },
+                               ValueRequired: true,
+                       },
+               })))
+}
+
+func TestPruneColumnsMapItself(t *testing.T) {
+       _, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{6: {}}, false)
+       assert.ErrorIs(t, err, iceberg.ErrInvalidSchema)
+       assert.ErrorContains(t, err, "cannot explicitly project List or Map 
types, 6:quux of type map<string, map<string, int>> was selected")
+}
+
+func TestPruneColumnsMapFull(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{9: {}}, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "quux",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.String,
+                               ValueID: 8,
+                               ValueType: &iceberg.MapType{
+                                       KeyID:         9,
+                                       KeyType:       
iceberg.PrimitiveTypes.String,
+                                       ValueID:       10,
+                                       ValueType:     
iceberg.PrimitiveTypes.Int32,
+                                       ValueRequired: true,
+                               },
+                               ValueRequired: true,
+                       },
+               })))
+}
+
+func TestPruneColumnsMapKey(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{10: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "quux",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.String,
+                               ValueID: 8,
+                               ValueType: &iceberg.MapType{
+                                       KeyID:         9,
+                                       KeyType:       
iceberg.PrimitiveTypes.String,
+                                       ValueID:       10,
+                                       ValueType:     
iceberg.PrimitiveTypes.Int32,
+                                       ValueRequired: true,
+                               },
+                               ValueRequired: true,
+                       },
+               })))
+}
+
+func TestPruneColumnsStruct(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{16: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       15,
+                       Name:     "person",
+                       Required: false,
+                       Type: &iceberg.StructType{
+                               FieldList: []iceberg.NestedField{{
+                                       ID: 16, Name: "name", Type: 
iceberg.PrimitiveTypes.String, Required: false,
+                               }},
+                       },
+               })))
+}
+
+func TestPruneColumnsStructFull(t *testing.T) {
+       sc, err := iceberg.PruneColumns(tableSchemaNested, 
map[int]iceberg.Void{16: {}}, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       15,
+                       Name:     "person",
+                       Required: false,
+                       Type: &iceberg.StructType{
+                               FieldList: []iceberg.NestedField{{
+                                       ID: 16, Name: "name", Type: 
iceberg.PrimitiveTypes.String, Required: false,
+                               }},
+                       },
+               })))
+}
+
+func TestPruneColumnsEmptyStruct(t *testing.T) {
+       schemaEmptyStruct := iceberg.NewSchema(0, iceberg.NestedField{
+               ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: 
false,
+       })
+
+       sc, err := iceberg.PruneColumns(schemaEmptyStruct, 
map[int]iceberg.Void{15: {}}, false)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(0,
+               iceberg.NestedField{
+                       ID: 15, Name: "person", Type: &iceberg.StructType{}, 
Required: false})))
+}
+
+func TestPruneColumnsEmptyStructFull(t *testing.T) {
+       schemaEmptyStruct := iceberg.NewSchema(0, iceberg.NestedField{
+               ID: 15, Name: "person", Type: &iceberg.StructType{}, Required: 
false,
+       })
+
+       sc, err := iceberg.PruneColumns(schemaEmptyStruct, 
map[int]iceberg.Void{15: {}}, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(iceberg.NewSchema(0,
+               iceberg.NestedField{
+                       ID: 15, Name: "person", Type: &iceberg.StructType{}, 
Required: false})))
+}
+
+func TestPruneColumnsStructInMap(t *testing.T) {
+       nestedSchema := iceberg.NewSchemaWithIdentifiers(1, []int{1},
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "id_to_person",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.Int32,
+                               ValueID: 8,
+                               ValueType: &iceberg.StructType{
+                                       FieldList: []iceberg.NestedField{
+                                               {ID: 10, Name: "name", Type: 
iceberg.PrimitiveTypes.String},
+                                               {ID: 11, Name: "age", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                                       },
+                               },
+                               ValueRequired: true,
+                       },
+               })
+
+       sc, err := iceberg.PruneColumns(nestedSchema, map[int]iceberg.Void{11: 
{}}, false)
+       require.NoError(t, err)
+
+       expected := iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "id_to_person",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.Int32,
+                               ValueID: 8,
+                               ValueType: &iceberg.StructType{
+                                       FieldList: []iceberg.NestedField{
+                                               {ID: 11, Name: "age", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                                       },
+                               },
+                               ValueRequired: true,
+                       },
+               })
+
+       assert.Truef(t, sc.Equals(expected), "expected: %s\ngot: %s", expected, 
sc)
+}
+
+func TestPruneColumnsStructInMapFull(t *testing.T) {
+       nestedSchema := iceberg.NewSchemaWithIdentifiers(1, []int{1},
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "id_to_person",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.Int32,
+                               ValueID: 8,
+                               ValueType: &iceberg.StructType{
+                                       FieldList: []iceberg.NestedField{
+                                               {ID: 10, Name: "name", Type: 
iceberg.PrimitiveTypes.String},
+                                               {ID: 11, Name: "age", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                                       },
+                               },
+                               ValueRequired: true,
+                       },
+               })
+
+       sc, err := iceberg.PruneColumns(nestedSchema, map[int]iceberg.Void{11: 
{}}, true)
+       require.NoError(t, err)
+
+       expected := iceberg.NewSchema(1,
+               iceberg.NestedField{
+                       ID:       6,
+                       Name:     "id_to_person",
+                       Required: true,
+                       Type: &iceberg.MapType{
+                               KeyID:   7,
+                               KeyType: iceberg.PrimitiveTypes.Int32,
+                               ValueID: 8,
+                               ValueType: &iceberg.StructType{
+                                       FieldList: []iceberg.NestedField{
+                                               {ID: 11, Name: "age", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                                       },
+                               },
+                               ValueRequired: true,
+                       },
+               })
+
+       assert.Truef(t, sc.Equals(expected), "expected: %s\ngot: %s", expected, 
sc)
+}
+
+func TestPruneColumnsSelectOriginalSchema(t *testing.T) {
+       id := tableSchemaNested.HighestFieldID()
+       selected := make(map[int]iceberg.Void)
+       for i := 0; i < id; i++ {
+               selected[i] = iceberg.Void{}
+       }
+
+       sc, err := iceberg.PruneColumns(tableSchemaNested, selected, true)
+       require.NoError(t, err)
+
+       assert.True(t, sc.Equals(tableSchemaNested))
+}
+
+func TestPruneNilSchema(t *testing.T) {
+       _, err := iceberg.PruneColumns(nil, nil, true)
+       assert.ErrorIs(t, err, iceberg.ErrInvalidArgument)
+}
+
+func TestSchemaRoundTrip(t *testing.T) {
+       data, err := json.Marshal(tableSchemaNested)
+       require.NoError(t, err)
+
+       assert.JSONEq(t, `{
+               "type": "struct",
+               "schema-id": 1,
+               "identifier-field-ids": [1],
+               "fields": [
+                       {
+                               "type": "string",
+                               "id": 1,
+                               "name": "foo",
+                               "required": false
+                       },
+                       {
+                               "type": "int",
+                               "id": 2,
+                               "name": "bar",
+                               "required": true
+                       },
+                       {
+                               "type": "boolean",
+                               "id": 3,
+                               "name": "baz",
+                               "required": false
+                       },
+                       {
+                               "id": 4,
+                               "name": "qux",
+                               "required": true,
+                               "type": {
+                                       "type": "list",
+                                       "element-id": 5,
+                                       "element-required": true,
+                                       "element": "string"
+                               }
+                       },
+                       {
+                               "id": 6,
+                               "name": "quux",
+                               "required": true,
+                               "type": {
+                                       "type": "map",
+                                       "key-id": 7,
+                                       "key": "string",
+                                       "value-id": 8,
+                                       "value": {
+                                               "type": "map",
+                                               "key-id": 9,
+                                               "key": "string",
+                                               "value-id": 10,
+                                               "value": "int",
+                                               "value-required": true
+                                       },
+                                       "value-required": true
+                               }
+                       },
+                       {
+                               "id": 11,
+                               "name": "location",
+                               "required": true,
+                               "type": {
+                                       "type": "list",
+                                       "element-id": 12,
+                                       "element-required": true,
+                                       "element": {
+                                               "type": "struct",
+                                               "fields": [
+                                                       {
+                                                               "id": 13,
+                                                               "name": 
"latitude",
+                                                               "type": "float",
+                                                               "required": 
false
+                                                       },
+                                                       {
+                                                               "id": 14,
+                                                               "name": 
"longitude",
+                                                               "type": "float",
+                                                               "required": 
false
+                                                       }
+                                               ]
+                                       }
+                               }
+                       },
+                       {
+                               "id": 15,
+                               "name": "person",
+                               "required": false,
+                               "type": {
+                                       "type": "struct",
+                                       "fields": [
+                                               {
+                                                       "id": 16,
+                                                       "name": "name",
+                                                       "type": "string",
+                                                       "required": false
+                                               },
+                                               {
+                                                       "id": 17,
+                                                       "name": "age",
+                                                       "type": "int",
+                                                       "required": true
+                                               }
+                                       ]
+                               }
+                       }
+               ]
+       }`, string(data))
+
+       var sc iceberg.Schema
+       require.NoError(t, json.Unmarshal(data, &sc))
+
+       assert.Truef(t, tableSchemaNested.Equals(&sc), "expected: %s\ngot: %s", 
tableSchemaNested, &sc)
+}
diff --git a/types.go b/types.go
new file mode 100644
index 0000000..8ca0ad7
--- /dev/null
+++ b/types.go
@@ -0,0 +1,615 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg
+
+import (
+       "encoding/json"
+       "fmt"
+       "regexp"
+       "strconv"
+       "strings"
+
+       "golang.org/x/exp/slices"
+)
+
+var (
+       regexFromBrackets = regexp.MustCompile(`^\w+\[(\d+)\]$`)
+       decimalRegex      = 
regexp.MustCompile(`decimal\(\s*(\d+)\s*,\s*(\d+)\s*\)`)
+)
+
+type Properties map[string]string
+
+// Type is an interface representing any of the available iceberg types,
+// such as primitives (int32/int64/etc.) or nested types (list/struct/map).
+type Type interface {
+       fmt.Stringer
+       Type() string
+       Equals(Type) bool
+}
+
+// NestedType is an interface that allows access to the child fields of
+// a nested type such as a list/struct/map type.
+type NestedType interface {
+       Type
+       Fields() []NestedField
+}
+
+type typeIFace struct {
+       Type
+}
+
+func (t *typeIFace) MarshalJSON() ([]byte, error) {
+       if nested, ok := t.Type.(NestedType); ok {
+               return json.Marshal(nested)
+       }
+       return []byte(`"` + t.Type.Type() + `"`), nil
+}
+
+func (t *typeIFace) UnmarshalJSON(b []byte) error {
+       var typename string
+       err := json.Unmarshal(b, &typename)
+       if err == nil {
+               switch typename {
+               case "boolean":
+                       t.Type = BooleanType{}
+               case "int":
+                       t.Type = Int32Type{}
+               case "long":
+                       t.Type = Int64Type{}
+               case "float":
+                       t.Type = Float32Type{}
+               case "double":
+                       t.Type = Float64Type{}
+               case "date":
+                       t.Type = DateType{}
+               case "time":
+                       t.Type = TimeType{}
+               case "timestamp":
+                       t.Type = TimestampType{}
+               case "timestamptz":
+                       t.Type = TimestampTzType{}
+               case "string":
+                       t.Type = StringType{}
+               case "uuid":
+                       t.Type = UUIDType{}
+               case "binary":
+                       t.Type = BinaryType{}
+               default:
+                       switch {
+                       case strings.HasPrefix(typename, "fixed"):
+                               matches := 
regexFromBrackets.FindStringSubmatch(typename)
+                               if len(matches) != 2 {
+                                       return fmt.Errorf("%w: %s", 
ErrInvalidTypeString, typename)
+                               }
+
+                               n, _ := strconv.Atoi(matches[1])
+                               t.Type = FixedType{len: n}
+                       case strings.HasPrefix(typename, "decimal"):
+                               matches := 
decimalRegex.FindStringSubmatch(typename)
+                               if len(matches) != 3 {
+                                       return fmt.Errorf("%w: %s", 
ErrInvalidTypeString, typename)
+                               }
+
+                               prec, _ := strconv.Atoi(matches[1])
+                               scale, _ := strconv.Atoi(matches[2])
+                               t.Type = DecimalType{precision: prec, scale: 
scale}
+                       default:
+                               return fmt.Errorf("%w: unrecognized field 
type", ErrInvalidSchema)
+                       }
+               }
+               return nil
+       }
+
+       aux := struct {
+               TypeName string `json:"type"`
+       }{}
+       if err = json.Unmarshal(b, &aux); err != nil {
+               return err
+       }
+
+       switch aux.TypeName {
+       case "list":
+               t.Type = &ListType{}
+       case "map":
+               t.Type = &MapType{}
+       case "struct":
+               t.Type = &StructType{}
+       default:
+               return fmt.Errorf("%w: %s", ErrInvalidTypeString, aux.TypeName)
+       }
+
+       return json.Unmarshal(b, t.Type)
+}
+
+type NestedField struct {
+       Type `json:"-"`
+
+       ID             int    `json:"id"`
+       Name           string `json:"name"`
+       Required       bool   `json:"required"`
+       Doc            string `json:"doc,omitempty"`
+       InitialDefault any    `json:"initial-default,omitempty"`
+       WriteDefault   any    `json:"write-default,omitempty"`
+}
+
+func optOrReq(required bool) string {
+       if required {
+               return "required"
+       }
+       return "optional"
+}
+
+func (n NestedField) String() string {
+       doc := n.Doc
+       if doc != "" {
+               doc = " (" + doc + ")"
+       }
+
+       return fmt.Sprintf("%d: %s: %s %s%s",
+               n.ID, n.Name, optOrReq(n.Required), n.Type, doc)
+}
+
+func (n *NestedField) Equals(other NestedField) bool {
+       return n.ID == other.ID &&
+               n.Name == other.Name &&
+               n.Required == other.Required &&
+               n.Doc == other.Doc &&
+               n.InitialDefault == other.InitialDefault &&
+               n.WriteDefault == other.WriteDefault &&
+               n.Type.Equals(other.Type)
+}
+
+func (n NestedField) MarshalJSON() ([]byte, error) {
+       type Alias NestedField
+       return json.Marshal(struct {
+               Type *typeIFace `json:"type"`
+               *Alias
+       }{Type: &typeIFace{n.Type}, Alias: (*Alias)(&n)})
+}
+
+func (n *NestedField) UnmarshalJSON(b []byte) error {
+       type Alias NestedField
+       aux := struct {
+               Type typeIFace `json:"type"`
+               *Alias
+       }{
+               Alias: (*Alias)(n),
+       }
+
+       if err := json.Unmarshal(b, &aux); err != nil {
+               return err
+       }
+
+       n.Type = aux.Type.Type
+
+       return nil
+}
+
+type StructType struct {
+       FieldList []NestedField `json:"fields"`
+}
+
+func (s *StructType) Equals(other Type) bool {
+       st, ok := other.(*StructType)
+       if !ok {
+               return false
+       }
+
+       return slices.EqualFunc(s.FieldList, st.FieldList, func(a, b 
NestedField) bool {
+               return a.Equals(b)
+       })
+}
+
+func (s *StructType) Fields() []NestedField { return s.FieldList }
+
+func (s *StructType) MarshalJSON() ([]byte, error) {
+       type Alias StructType
+       return json.Marshal(struct {
+               Type string `json:"type"`
+               *Alias
+       }{Type: s.Type(), Alias: (*Alias)(s)})
+}
+
+func (*StructType) Type() string { return "struct" }
+func (s *StructType) String() string {
+       var b strings.Builder
+       b.WriteString("struct<")
+       for i, f := range s.FieldList {
+               if i != 0 {
+                       b.WriteString(", ")
+               }
+               fmt.Fprintf(&b, "%d: %s: ",
+                       f.ID, f.Name)
+               if f.Required {
+                       b.WriteString("required ")
+               }
+               b.WriteString(f.Type.String())
+               if f.Doc != "" {
+                       b.WriteString(" (")
+                       b.WriteString(f.Doc)
+                       b.WriteByte(')')
+               }
+       }
+       b.WriteString(">")
+
+       return b.String()
+}
+
+type ListType struct {
+       ElementID       int  `json:"element-id"`
+       Element         Type `json:"-"`
+       ElementRequired bool `json:"element-required"`
+}
+
+func (l *ListType) MarshalJSON() ([]byte, error) {
+       type Alias ListType
+       return json.Marshal(struct {
+               Type string `json:"type"`
+               *Alias
+               Element *typeIFace `json:"element"`
+       }{Type: l.Type(), Alias: (*Alias)(l), Element: &typeIFace{l.Element}})
+}
+
+func (l *ListType) Equals(other Type) bool {
+       rhs, ok := other.(*ListType)
+       if !ok {
+               return false
+       }
+
+       return l.ElementID == rhs.ElementID &&
+               l.Element.Equals(rhs.Element) &&
+               l.ElementRequired == rhs.ElementRequired
+}
+
+func (l *ListType) Fields() []NestedField {
+       return []NestedField{l.ElementField()}
+}
+
+func (l *ListType) ElementField() NestedField {
+       return NestedField{
+               ID:       l.ElementID,
+               Name:     "element",
+               Type:     l.Element,
+               Required: l.ElementRequired,
+       }
+}
+
+func (*ListType) Type() string     { return "list" }
+func (l *ListType) String() string { return fmt.Sprintf("list<%s>", l.Element) 
}
+
+func (l *ListType) UnmarshalJSON(b []byte) error {
+       aux := struct {
+               ID   int       `json:"element-id"`
+               Elem typeIFace `json:"element"`
+               Req  bool      `json:"element-required"`
+       }{}
+       if err := json.Unmarshal(b, &aux); err != nil {
+               return err
+       }
+
+       l.ElementID = aux.ID
+       l.Element = aux.Elem.Type
+       l.ElementRequired = aux.Req
+       return nil
+}
+
+type MapType struct {
+       KeyID         int  `json:"key-id"`
+       KeyType       Type `json:"-"`
+       ValueID       int  `json:"value-id"`
+       ValueType     Type `json:"-"`
+       ValueRequired bool `json:"value-required"`
+}
+
+func (m *MapType) MarshalJSON() ([]byte, error) {
+       type Alias MapType
+       return json.Marshal(struct {
+               Type string `json:"type"`
+               *Alias
+               KeyType   *typeIFace `json:"key"`
+               ValueType *typeIFace `json:"value"`
+       }{Type: m.Type(), Alias: (*Alias)(m),
+               KeyType:   &typeIFace{m.KeyType},
+               ValueType: &typeIFace{m.ValueType}})
+}
+
+func (m *MapType) Equals(other Type) bool {
+       rhs, ok := other.(*MapType)
+       if !ok {
+               return false
+       }
+
+       return m.KeyID == rhs.KeyID &&
+               m.KeyType.Equals(rhs.KeyType) &&
+               m.ValueID == rhs.ValueID &&
+               m.ValueType.Equals(rhs.ValueType) &&
+               m.ValueRequired == rhs.ValueRequired
+}
+
+func (m *MapType) Fields() []NestedField {
+       return []NestedField{m.KeyField(), m.ValueField()}
+}
+
+func (m *MapType) KeyField() NestedField {
+       return NestedField{
+               Name:     "key",
+               ID:       m.KeyID,
+               Type:     m.KeyType,
+               Required: true,
+       }
+}
+
+func (m *MapType) ValueField() NestedField {
+       return NestedField{
+               Name:     "value",
+               ID:       m.ValueID,
+               Type:     m.ValueType,
+               Required: m.ValueRequired,
+       }
+}
+
+func (*MapType) Type() string { return "map" }
+func (m *MapType) String() string {
+       return fmt.Sprintf("map<%s, %s>", m.KeyType, m.ValueType)
+}
+
+func (m *MapType) UnmarshalJSON(b []byte) error {
+       aux := struct {
+               KeyID    int       `json:"key-id"`
+               Key      typeIFace `json:"key"`
+               ValueID  int       `json:"value-id"`
+               Value    typeIFace `json:"value"`
+               ValueReq *bool     `json:"value-required"`
+       }{}
+       if err := json.Unmarshal(b, &aux); err != nil {
+               return err
+       }
+
+       m.KeyID, m.KeyType = aux.KeyID, aux.Key.Type
+       m.ValueID, m.ValueType = aux.ValueID, aux.Value.Type
+       if aux.ValueReq == nil {
+               m.ValueRequired = true
+       } else {
+               m.ValueRequired = *aux.ValueReq
+       }
+       return nil
+}
+
+func FixedTypeOf(n int) FixedType { return FixedType{len: n} }
+
+type FixedType struct {
+       len int
+}
+
+func (f FixedType) Equals(other Type) bool {
+       rhs, ok := other.(FixedType)
+       if !ok {
+               return false
+       }
+
+       return f.len == rhs.len
+}
+func (f FixedType) Len() int       { return f.len }
+func (f FixedType) Type() string   { return fmt.Sprintf("fixed[%d]", f.len) }
+func (f FixedType) String() string { return fmt.Sprintf("fixed[%d]", f.len) }
+
+func DecimalTypeOf(prec, scale int) DecimalType {
+       return DecimalType{precision: prec, scale: scale}
+}
+
+type DecimalType struct {
+       precision, scale int
+}
+
+func (d DecimalType) Equals(other Type) bool {
+       rhs, ok := other.(DecimalType)
+       if !ok {
+               return false
+       }
+
+       return d.precision == rhs.precision &&
+               d.scale == rhs.scale
+}
+
+func (d DecimalType) Type() string   { return fmt.Sprintf("decimal(%d, %d)", 
d.precision, d.scale) }
+func (d DecimalType) String() string { return fmt.Sprintf("decimal(%d, %d)", 
d.precision, d.scale) }
+func (d DecimalType) Precision() int { return d.precision }
+func (d DecimalType) Scale() int     { return d.scale }
+
+type PrimitiveType interface {
+       Type
+       primitive()
+}
+
+type BooleanType struct{}
+
+func (BooleanType) Equals(other Type) bool {
+       _, ok := other.(BooleanType)
+       return ok
+}
+
+func (BooleanType) primitive()     {}
+func (BooleanType) Type() string   { return "boolean" }
+func (BooleanType) String() string { return "boolean" }
+
+// Int32Type is the "int"/"integer" type of the iceberg spec.
+type Int32Type struct{}
+
+func (Int32Type) Equals(other Type) bool {
+       _, ok := other.(Int32Type)
+       return ok
+}
+
+func (Int32Type) primitive()     {}
+func (Int32Type) Type() string   { return "int" }
+func (Int32Type) String() string { return "int" }
+
+// Int64Type is the "long" type of the iceberg spec.
+type Int64Type struct{}
+
+func (Int64Type) Equals(other Type) bool {
+       _, ok := other.(Int64Type)
+       return ok
+}
+
+func (Int64Type) primitive()     {}
+func (Int64Type) Type() string   { return "long" }
+func (Int64Type) String() string { return "long" }
+
+// Float32Type is the "float" type in the iceberg spec.
+type Float32Type struct{}
+
+func (Float32Type) Equals(other Type) bool {
+       _, ok := other.(Float32Type)
+       return ok
+}
+
+func (Float32Type) primitive()     {}
+func (Float32Type) Type() string   { return "float" }
+func (Float32Type) String() string { return "float" }
+
+// Float64Type represents the "double" type of the iceberg spec.
+type Float64Type struct{}
+
+func (Float64Type) Equals(other Type) bool {
+       _, ok := other.(Float64Type)
+       return ok
+}
+
+func (Float64Type) primitive()     {}
+func (Float64Type) Type() string   { return "double" }
+func (Float64Type) String() string { return "double" }
+
+type Date int32
+
+// DateType represents a calendar date without a timezone or time,
+// represented as a 32-bit integer denoting the number of days since
+// the unix epoch.
+type DateType struct{}
+
+func (DateType) Equals(other Type) bool {
+       _, ok := other.(DateType)
+       return ok
+}
+
+func (DateType) primitive()     {}
+func (DateType) Type() string   { return "date" }
+func (DateType) String() string { return "date" }
+
+type Time int64
+
+// TimeType represents a number of microseconds since midnight.
+type TimeType struct{}
+
+func (TimeType) Equals(other Type) bool {
+       _, ok := other.(TimeType)
+       return ok
+}
+
+func (TimeType) primitive()     {}
+func (TimeType) Type() string   { return "time" }
+func (TimeType) String() string { return "time" }
+
+type Timestamp int64
+
+// TimestampType represents a number of microseconds since the unix epoch
+// without regard for timezone.
+type TimestampType struct{}
+
+func (TimestampType) Equals(other Type) bool {
+       _, ok := other.(TimestampType)
+       return ok
+}
+
+func (TimestampType) primitive()     {}
+func (TimestampType) Type() string   { return "timestamp" }
+func (TimestampType) String() string { return "timestamp" }
+
+// TimestampTzType represents a timestamp stored as UTC representing the
+// number of microseconds since the unix epoch.
+type TimestampTzType struct{}
+
+func (TimestampTzType) Equals(other Type) bool {
+       _, ok := other.(TimestampTzType)
+       return ok
+}
+
+func (TimestampTzType) primitive()     {}
+func (TimestampTzType) Type() string   { return "timestamptz" }
+func (TimestampTzType) String() string { return "timestamptz" }
+
+type StringType struct{}
+
+func (StringType) Equals(other Type) bool {
+       _, ok := other.(StringType)
+       return ok
+}
+
+func (StringType) primitive()     {}
+func (StringType) Type() string   { return "string" }
+func (StringType) String() string { return "string" }
+
+type UUIDType struct{}
+
+func (UUIDType) Equals(other Type) bool {
+       _, ok := other.(UUIDType)
+       return ok
+}
+
+func (UUIDType) primitive()     {}
+func (UUIDType) Type() string   { return "uuid" }
+func (UUIDType) String() string { return "uuid" }
+
+type BinaryType struct{}
+
+func (BinaryType) Equals(other Type) bool {
+       _, ok := other.(BinaryType)
+       return ok
+}
+
+func (BinaryType) primitive()     {}
+func (BinaryType) Type() string   { return "binary" }
+func (BinaryType) String() string { return "binary" }
+
+var PrimitiveTypes = struct {
+       Bool        PrimitiveType
+       Int32       PrimitiveType
+       Int64       PrimitiveType
+       Float32     PrimitiveType
+       Float64     PrimitiveType
+       Date        PrimitiveType
+       Time        PrimitiveType
+       Timestamp   PrimitiveType
+       TimestampTz PrimitiveType
+       String      PrimitiveType
+       Binary      PrimitiveType
+       UUID        PrimitiveType
+}{
+       Bool:        BooleanType{},
+       Int32:       Int32Type{},
+       Int64:       Int64Type{},
+       Float32:     Float32Type{},
+       Float64:     Float64Type{},
+       Date:        DateType{},
+       Time:        TimeType{},
+       Timestamp:   TimestampType{},
+       TimestampTz: TimestampTzType{},
+       String:      StringType{},
+       Binary:      BinaryType{},
+       UUID:        UUIDType{},
+}
diff --git a/types_test.go b/types_test.go
new file mode 100644
index 0000000..abe233f
--- /dev/null
+++ b/types_test.go
@@ -0,0 +1,236 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg_test
+
+import (
+       "encoding/json"
+       "testing"
+
+       "github.com/apache/iceberg-go"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestTypesBasic(t *testing.T) {
+       tests := []struct {
+               expected string
+               typ      iceberg.Type
+       }{
+               {"boolean", iceberg.PrimitiveTypes.Bool},
+               {"int", iceberg.PrimitiveTypes.Int32},
+               {"long", iceberg.PrimitiveTypes.Int64},
+               {"float", iceberg.PrimitiveTypes.Float32},
+               {"double", iceberg.PrimitiveTypes.Float64},
+               {"date", iceberg.PrimitiveTypes.Date},
+               {"time", iceberg.PrimitiveTypes.Time},
+               {"timestamp", iceberg.PrimitiveTypes.Timestamp},
+               {"timestamptz", iceberg.PrimitiveTypes.TimestampTz},
+               {"uuid", iceberg.PrimitiveTypes.UUID},
+               {"binary", iceberg.PrimitiveTypes.Binary},
+               {"fixed[5]", iceberg.FixedTypeOf(5)},
+               {"decimal(9, 4)", iceberg.DecimalTypeOf(9, 4)},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.expected, func(t *testing.T) {
+                       var data = `{
+                               "id": 1,
+                               "name": "test",
+                               "type": "` + tt.expected + `",
+                               "required": false
+                       }`
+
+                       var n iceberg.NestedField
+                       require.NoError(t, json.Unmarshal([]byte(data), &n))
+                       assert.Truef(t, n.Type.Equals(tt.typ), "expected: 
%s\ngot: %s", tt.typ, n.Type)
+
+                       out, err := json.Marshal(n)
+                       require.NoError(t, err)
+                       assert.JSONEq(t, data, string(out))
+               })
+       }
+}
+
+func TestFixedType(t *testing.T) {
+       typ := iceberg.FixedTypeOf(5)
+       assert.Equal(t, 5, typ.Len())
+       assert.Equal(t, "fixed[5]", typ.String())
+       assert.True(t, typ.Equals(iceberg.FixedTypeOf(5)))
+       assert.False(t, typ.Equals(iceberg.FixedTypeOf(6)))
+}
+
+func TestDecimalType(t *testing.T) {
+       typ := iceberg.DecimalTypeOf(9, 2)
+       assert.Equal(t, 9, typ.Precision())
+       assert.Equal(t, 2, typ.Scale())
+       assert.Equal(t, "decimal(9, 2)", typ.String())
+       assert.True(t, typ.Equals(iceberg.DecimalTypeOf(9, 2)))
+       assert.False(t, typ.Equals(iceberg.DecimalTypeOf(9, 3)))
+}
+
+func TestStructType(t *testing.T) {
+       typ := &iceberg.StructType{
+               FieldList: []iceberg.NestedField{
+                       {ID: 1, Name: "required_field", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                       {ID: 2, Name: "optional_field", Type: 
iceberg.FixedTypeOf(5), Required: false},
+                       {ID: 3, Name: "required_field", Type: 
&iceberg.StructType{
+                               FieldList: []iceberg.NestedField{
+                                       {ID: 4, Name: "optional_field", Type: 
iceberg.DecimalTypeOf(8, 2), Required: false},
+                                       {ID: 5, Name: "required_field", Type: 
iceberg.PrimitiveTypes.Int64, Required: false},
+                               },
+                       }, Required: false},
+               },
+       }
+
+       assert.Len(t, typ.FieldList, 3)
+       assert.False(t, typ.Equals(&iceberg.StructType{FieldList: 
[]iceberg.NestedField{{ID: 1, Name: "optional_field", Type: 
iceberg.PrimitiveTypes.Int32, Required: true}}}))
+       out, err := json.Marshal(typ)
+       require.NoError(t, err)
+
+       var actual iceberg.StructType
+       require.NoError(t, json.Unmarshal(out, &actual))
+       assert.True(t, typ.Equals(&actual))
+}
+
+func TestListType(t *testing.T) {
+       typ := &iceberg.ListType{
+               ElementID:       1,
+               ElementRequired: false,
+               Element: &iceberg.StructType{
+                       FieldList: []iceberg.NestedField{
+                               {ID: 2, Name: "required_field", Type: 
iceberg.DecimalTypeOf(8, 2), Required: true},
+                               {ID: 3, Name: "optional_field", Type: 
iceberg.PrimitiveTypes.Int64, Required: false},
+                       },
+               },
+       }
+
+       assert.IsType(t, (*iceberg.StructType)(nil), typ.ElementField().Type)
+       assert.Len(t, typ.ElementField().Type.(iceberg.NestedType).Fields(), 2)
+       assert.Equal(t, 1, typ.ElementField().ID)
+       assert.False(t, typ.Equals(&iceberg.ListType{
+               ElementID:       1,
+               ElementRequired: true,
+               Element: &iceberg.StructType{
+                       FieldList: []iceberg.NestedField{
+                               {ID: 2, Name: "required_field", Type: 
iceberg.DecimalTypeOf(8, 2), Required: true},
+                       },
+               },
+       }))
+
+       out, err := json.Marshal(typ)
+       require.NoError(t, err)
+
+       var actual iceberg.ListType
+       require.NoError(t, json.Unmarshal(out, &actual))
+       assert.True(t, typ.Equals(&actual))
+}
+
+func TestMapType(t *testing.T) {
+       typ := &iceberg.MapType{
+               KeyID:         1,
+               KeyType:       iceberg.PrimitiveTypes.Float64,
+               ValueID:       2,
+               ValueType:     iceberg.PrimitiveTypes.UUID,
+               ValueRequired: false,
+       }
+
+       assert.IsType(t, iceberg.PrimitiveTypes.Float64, typ.KeyField().Type)
+       assert.Equal(t, 1, typ.KeyField().ID)
+       assert.IsType(t, iceberg.PrimitiveTypes.UUID, typ.ValueField().Type)
+       assert.Equal(t, 2, typ.ValueField().ID)
+       assert.False(t, typ.Equals(&iceberg.MapType{
+               KeyID: 1, KeyType: iceberg.PrimitiveTypes.Int64,
+               ValueID: 2, ValueType: iceberg.PrimitiveTypes.UUID, 
ValueRequired: false,
+       }))
+       assert.False(t, typ.Equals(&iceberg.MapType{
+               KeyID: 1, KeyType: iceberg.PrimitiveTypes.Float64,
+               ValueID: 2, ValueType: iceberg.PrimitiveTypes.String, 
ValueRequired: true,
+       }))
+
+       out, err := json.Marshal(typ)
+       require.NoError(t, err)
+
+       var actual iceberg.MapType
+       require.NoError(t, json.Unmarshal(out, &actual))
+       assert.True(t, typ.Equals(&actual))
+}
+
+var (
+       NonParameterizedTypes = []iceberg.Type{
+               iceberg.PrimitiveTypes.Bool,
+               iceberg.PrimitiveTypes.Int32,
+               iceberg.PrimitiveTypes.Int64,
+               iceberg.PrimitiveTypes.Float32,
+               iceberg.PrimitiveTypes.Float64,
+               iceberg.PrimitiveTypes.Date,
+               iceberg.PrimitiveTypes.Time,
+               iceberg.PrimitiveTypes.Timestamp,
+               iceberg.PrimitiveTypes.TimestampTz,
+               iceberg.PrimitiveTypes.String,
+               iceberg.PrimitiveTypes.Binary,
+               iceberg.PrimitiveTypes.UUID,
+       }
+)
+
+func TestNonParameterizedTypeEquality(t *testing.T) {
+       for i, in := range NonParameterizedTypes {
+               for j, check := range NonParameterizedTypes {
+                       if i == j {
+                               assert.Truef(t, in.Equals(check), "expected %s 
== %s", in, check)
+                       } else {
+                               assert.Falsef(t, in.Equals(check), "expected %s 
!= %s", in, check)
+                       }
+               }
+       }
+}
+
+func TestTypeStrings(t *testing.T) {
+       tests := []struct {
+               typ iceberg.Type
+               str string
+       }{
+               {iceberg.PrimitiveTypes.Bool, "boolean"},
+               {iceberg.PrimitiveTypes.Int32, "int"},
+               {iceberg.PrimitiveTypes.Int64, "long"},
+               {iceberg.PrimitiveTypes.Float32, "float"},
+               {iceberg.PrimitiveTypes.Float64, "double"},
+               {iceberg.PrimitiveTypes.Date, "date"},
+               {iceberg.PrimitiveTypes.Time, "time"},
+               {iceberg.PrimitiveTypes.Timestamp, "timestamp"},
+               {iceberg.PrimitiveTypes.TimestampTz, "timestamptz"},
+               {iceberg.PrimitiveTypes.String, "string"},
+               {iceberg.PrimitiveTypes.UUID, "uuid"},
+               {iceberg.PrimitiveTypes.Binary, "binary"},
+               {iceberg.FixedTypeOf(22), "fixed[22]"},
+               {iceberg.DecimalTypeOf(19, 25), "decimal(19, 25)"},
+               {&iceberg.StructType{
+                       FieldList: []iceberg.NestedField{
+                               {ID: 1, Name: "required_field", Type: 
iceberg.PrimitiveTypes.String, Required: true, Doc: "this is a doc"},
+                               {ID: 2, Name: "optional_field", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+                       },
+               }, "struct<1: required_field: required string (this is a doc), 
2: optional_field: required int>"},
+               {&iceberg.ListType{
+                       ElementID: 22, Element: iceberg.PrimitiveTypes.String}, 
"list<string>"},
+               {&iceberg.MapType{KeyID: 19, KeyType: 
iceberg.PrimitiveTypes.String, ValueID: 25, ValueType: 
iceberg.PrimitiveTypes.Float64},
+                       "map<string, double>"},
+       }
+
+       for _, tt := range tests {
+               assert.Equal(t, tt.str, tt.typ.String())
+       }
+}
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..ccb6bbc
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package iceberg
+
+import (
+       "runtime/debug"
+
+       "golang.org/x/exp/constraints"
+)
+
+var version string
+
+func init() {
+       version = "(unknown version)"
+       if info, ok := debug.ReadBuildInfo(); ok {
+               for _, dep := range info.Deps {
+                       switch {
+                       case dep.Path == "github.com/apache/iceberg/go/iceberg":
+                               version = dep.Version
+                       }
+               }
+       }
+}
+
+func Version() string { return version }
+
+func max[T constraints.Ordered](vals ...T) T {
+       if len(vals) == 0 {
+               panic("can't call max with no arguments")
+       }
+
+       out := vals[0]
+       for _, v := range vals[1:] {
+               if v > out {
+                       out = v
+               }
+       }
+       return out
+}

Reply via email to