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
+
+[](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
+}