iceberg-ops 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iceberg_ops-0.1.0/.gitignore +70 -0
- iceberg_ops-0.1.0/CHANGELOG.md +502 -0
- iceberg_ops-0.1.0/LICENSE +21 -0
- iceberg_ops-0.1.0/PKG-INFO +425 -0
- iceberg_ops-0.1.0/README.md +358 -0
- iceberg_ops-0.1.0/docs/api/autogen.rst +6 -0
- iceberg_ops-0.1.0/docs/api/catalog.rst +6 -0
- iceberg_ops-0.1.0/docs/api/cli.rst +9 -0
- iceberg_ops-0.1.0/docs/api/data_migration.rst +46 -0
- iceberg_ops-0.1.0/docs/api/history.rst +6 -0
- iceberg_ops-0.1.0/docs/api/index.rst +53 -0
- iceberg_ops-0.1.0/docs/api/maintenance.rst +54 -0
- iceberg_ops-0.1.0/docs/api/migration.rst +6 -0
- iceberg_ops-0.1.0/docs/api/polaris.rst +48 -0
- iceberg_ops-0.1.0/docs/api/runner.rst +6 -0
- iceberg_ops-0.1.0/docs/api/scaffolding.rst +5 -0
- iceberg_ops-0.1.0/docs/api/schema.rst +6 -0
- iceberg_ops-0.1.0/docs/api/testing.rst +10 -0
- iceberg_ops-0.1.0/docs/api/types.rst +5 -0
- iceberg_ops-0.1.0/docs/api/utils.rst +5 -0
- iceberg_ops-0.1.0/docs/architecture.rst +189 -0
- iceberg_ops-0.1.0/docs/autogenerate.rst +281 -0
- iceberg_ops-0.1.0/docs/changelog.rst +7 -0
- iceberg_ops-0.1.0/docs/cli.rst +143 -0
- iceberg_ops-0.1.0/docs/compatibility.rst +298 -0
- iceberg_ops-0.1.0/docs/conf.py +214 -0
- iceberg_ops-0.1.0/docs/data_migration.rst +360 -0
- iceberg_ops-0.1.0/docs/index.rst +145 -0
- iceberg_ops-0.1.0/docs/maintenance.rst +262 -0
- iceberg_ops-0.1.0/docs/polaris.rst +293 -0
- iceberg_ops-0.1.0/docs/quickstart.rst +239 -0
- iceberg_ops-0.1.0/docs/schema_migrations.rst +565 -0
- iceberg_ops-0.1.0/iceberg_ops/__init__.py +12 -0
- iceberg_ops-0.1.0/iceberg_ops/autogen.py +1462 -0
- iceberg_ops-0.1.0/iceberg_ops/cli.py +1883 -0
- iceberg_ops-0.1.0/iceberg_ops/core/__init__.py +23 -0
- iceberg_ops-0.1.0/iceberg_ops/core/catalog.py +508 -0
- iceberg_ops-0.1.0/iceberg_ops/core/schema.py +1254 -0
- iceberg_ops-0.1.0/iceberg_ops/core/types.py +214 -0
- iceberg_ops-0.1.0/iceberg_ops/core/utils.py +561 -0
- iceberg_ops-0.1.0/iceberg_ops/data_migration/__init__.py +94 -0
- iceberg_ops-0.1.0/iceberg_ops/data_migration/checkpoint.py +454 -0
- iceberg_ops-0.1.0/iceberg_ops/data_migration/migrator.py +1564 -0
- iceberg_ops-0.1.0/iceberg_ops/data_migration/reconciler.py +594 -0
- iceberg_ops-0.1.0/iceberg_ops/history.py +409 -0
- iceberg_ops-0.1.0/iceberg_ops/maintenance/__init__.py +72 -0
- iceberg_ops-0.1.0/iceberg_ops/maintenance/advisor.py +194 -0
- iceberg_ops-0.1.0/iceberg_ops/maintenance/compaction.py +647 -0
- iceberg_ops-0.1.0/iceberg_ops/maintenance/orphans.py +455 -0
- iceberg_ops-0.1.0/iceberg_ops/maintenance/snapshots.py +242 -0
- iceberg_ops-0.1.0/iceberg_ops/migration.py +950 -0
- iceberg_ops-0.1.0/iceberg_ops/polaris/__init__.py +112 -0
- iceberg_ops-0.1.0/iceberg_ops/polaris/client.py +532 -0
- iceberg_ops-0.1.0/iceberg_ops/polaris/dispatcher.py +467 -0
- iceberg_ops-0.1.0/iceberg_ops/polaris/policies.py +216 -0
- iceberg_ops-0.1.0/iceberg_ops/py.typed +0 -0
- iceberg_ops-0.1.0/iceberg_ops/runner.py +827 -0
- iceberg_ops-0.1.0/iceberg_ops/scaffolding.py +303 -0
- iceberg_ops-0.1.0/iceberg_ops/testing.py +224 -0
- iceberg_ops-0.1.0/pyproject.toml +181 -0
- iceberg_ops-0.1.0/tests/conftest.py +103 -0
- iceberg_ops-0.1.0/tests/migrations/0001_create_products.py +26 -0
- iceberg_ops-0.1.0/tests/migrations/0002_add_sku.py +23 -0
- iceberg_ops-0.1.0/tests/migrations/__init__.py +0 -0
- iceberg_ops-0.1.0/tests/test_autogen.py +1927 -0
- iceberg_ops-0.1.0/tests/test_catalog.py +598 -0
- iceberg_ops-0.1.0/tests/test_cli.py +2115 -0
- iceberg_ops-0.1.0/tests/test_cli_catalogs.py +638 -0
- iceberg_ops-0.1.0/tests/test_cli_revision.py +421 -0
- iceberg_ops-0.1.0/tests/test_data_migration.py +3114 -0
- iceberg_ops-0.1.0/tests/test_docs_snippets.py +793 -0
- iceberg_ops-0.1.0/tests/test_history.py +357 -0
- iceberg_ops-0.1.0/tests/test_hygiene.py +137 -0
- iceberg_ops-0.1.0/tests/test_maintenance.py +1647 -0
- iceberg_ops-0.1.0/tests/test_migration.py +583 -0
- iceberg_ops-0.1.0/tests/test_packaging.py +368 -0
- iceberg_ops-0.1.0/tests/test_polaris_client.py +577 -0
- iceberg_ops-0.1.0/tests/test_polaris_dispatcher.py +611 -0
- iceberg_ops-0.1.0/tests/test_polaris_policies.py +175 -0
- iceberg_ops-0.1.0/tests/test_runner.py +852 -0
- iceberg_ops-0.1.0/tests/test_runner_concurrency.py +188 -0
- iceberg_ops-0.1.0/tests/test_scaffolding.py +196 -0
- iceberg_ops-0.1.0/tests/test_schema.py +1071 -0
- iceberg_ops-0.1.0/tests/test_smoke.py +18 -0
- iceberg_ops-0.1.0/tests/test_testing.py +127 -0
- iceberg_ops-0.1.0/tests/test_types.py +182 -0
- iceberg_ops-0.1.0/tests/test_utils.py +482 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
share/python-wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
MANIFEST
|
|
24
|
+
|
|
25
|
+
# Virtual environments
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
env/
|
|
29
|
+
ENV/
|
|
30
|
+
.env
|
|
31
|
+
|
|
32
|
+
# Testing / coverage
|
|
33
|
+
.pytest_cache/
|
|
34
|
+
.coverage
|
|
35
|
+
.coverage.*
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.cache/
|
|
40
|
+
coverage.xml
|
|
41
|
+
*.cover
|
|
42
|
+
.hypothesis/
|
|
43
|
+
|
|
44
|
+
# Type / lint caches
|
|
45
|
+
.mypy_cache/
|
|
46
|
+
.dmypy.json
|
|
47
|
+
dmypy.json
|
|
48
|
+
.pyre/
|
|
49
|
+
.pytype/
|
|
50
|
+
.ruff_cache/
|
|
51
|
+
|
|
52
|
+
# IDE
|
|
53
|
+
.idea/
|
|
54
|
+
.vscode/
|
|
55
|
+
*.swp
|
|
56
|
+
*.swo
|
|
57
|
+
*~
|
|
58
|
+
|
|
59
|
+
# OS
|
|
60
|
+
.DS_Store
|
|
61
|
+
Thumbs.db
|
|
62
|
+
|
|
63
|
+
# Sphinx
|
|
64
|
+
docs/_build/
|
|
65
|
+
|
|
66
|
+
# Jupyter
|
|
67
|
+
.ipynb_checkpoints/
|
|
68
|
+
|
|
69
|
+
# iceberg-ops runtime data
|
|
70
|
+
local_data/
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-08
|
|
11
|
+
|
|
12
|
+
First public release of `iceberg-ops`: Alembic-style schema migrations
|
|
13
|
+
for Apache Iceberg, plus a cross-table data mover, basic maintenance
|
|
14
|
+
helpers and an experimental Apache Polaris policy executor.
|
|
15
|
+
|
|
16
|
+
Development builds from May 2026 were never published. The Changed and
|
|
17
|
+
Fixed lists below describe how this release differs from them, for
|
|
18
|
+
anyone who ran those builds from source.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
Schema migrations:
|
|
23
|
+
|
|
24
|
+
- `BaseMigration` and `MigrationContext` for `migrations/NNNN_*.py`
|
|
25
|
+
files with `upgrade()` / `downgrade()` and the `pre_upgrade`,
|
|
26
|
+
`post_upgrade`, `pre_downgrade` and `post_downgrade` hooks. Context
|
|
27
|
+
helpers: `create_table_if_not_exists` (with `partition_spec`,
|
|
28
|
+
`sort_order`, `properties` and `location`, applied on create),
|
|
29
|
+
`drop_table_if_exists`, `load_table`, `add_column`, `enforce_column`
|
|
30
|
+
(with opt-in `legacy_archival`), `enforce_rename`, `delete_column`,
|
|
31
|
+
`update_partition_spec`, `replace_sort_order`, `set_clustering_keys`,
|
|
32
|
+
`set_table_properties`, `unset_table_properties`,
|
|
33
|
+
`set_snapshot_retention` and `set_compaction_target_size`. Helpers
|
|
34
|
+
take bare table names; the runner adds the namespace.
|
|
35
|
+
- `MigrationRunner` with `migrate`, `rollback`, `stamp`, `plan`,
|
|
36
|
+
`status` and `history` for one namespace at a time. Migration history
|
|
37
|
+
is an Iceberg table, `_iceberg_ops.migration_history`, in the managed
|
|
38
|
+
catalog; it keeps the latest status of each version per namespace.
|
|
39
|
+
- `iceberg_ops.runner.normalize_version`: `"2"`, `"0002"` and `2` name
|
|
40
|
+
the same version.
|
|
41
|
+
- The `iceberg-ops` CLI: `init`, `revision`, `migrate`, `rollback`,
|
|
42
|
+
`stamp`, `plan`, `status`, `current`, `history`, `migrate-data` and
|
|
43
|
+
`run-policies`, plus `--version`. Settings come from flags or from
|
|
44
|
+
`iceberg-ops.yaml` (keys `catalog`, `catalog_name`, `namespace`,
|
|
45
|
+
`db_path`, `warehouse`, `migrations_dir`).
|
|
46
|
+
- `--catalog-name NAME` and the config key `catalog_name` open any
|
|
47
|
+
catalog defined in `.pyiceberg.yaml` or `PYICEBERG_CATALOG__<NAME>__*`
|
|
48
|
+
environment variables. `migrate-data` has `--src-catalog-name` /
|
|
49
|
+
`--dst-catalog-name` and `run-policies` has `--catalog-name`.
|
|
50
|
+
- Autogenerate (`iceberg_ops.autogen`): `schema_diff_to_ops`,
|
|
51
|
+
`spec_diff_to_ops`, `sort_order_diff_to_ops`, `properties_diff_to_ops`,
|
|
52
|
+
`render_migration_body`, `render_migration_file` and
|
|
53
|
+
`write_autogenerated_revision`, plus `iceberg-ops revision
|
|
54
|
+
--autogenerate --target-table T` with `--target-schema-yaml FILE` or
|
|
55
|
+
`--target-schema-arrow module:attr` and repeatable
|
|
56
|
+
`--rename-hint old:new`. The CLI diffs columns; the Python API also
|
|
57
|
+
diffs partition spec, sort order and table properties. Rename hints
|
|
58
|
+
are validated, and new columns keep their target doc.
|
|
59
|
+
- YAML schema contracts (`SchemaContract.from_yaml`) accept the Iceberg
|
|
60
|
+
primitive types, including `decimal(P,S)`, `fixed[N]` and
|
|
61
|
+
`timestamp_ns`, and are validated with clear errors.
|
|
62
|
+
- Core helpers (`iceberg_ops.core`): `CatalogFactory.build` for the
|
|
63
|
+
`sqlite`, `memory`, `glue`, `rest`, `hive` and `s3tables` kinds,
|
|
64
|
+
`default_local_paths()`, Arrow-to-Iceberg type translation with
|
|
65
|
+
`is_allowed_promotion(old, new)` (Arrow time, uuid, fixed-size binary,
|
|
66
|
+
string_view, binary_view and dictionary types are mapped), schema
|
|
67
|
+
diff, `SchemaContract`, `SchemaEvolutionHelper`, and snapshot, upsert,
|
|
68
|
+
time-travel and field-history utilities.
|
|
69
|
+
|
|
70
|
+
Companion modules:
|
|
71
|
+
|
|
72
|
+
- Cross-table data migration, experimental (`iceberg_ops.data_migration`,
|
|
73
|
+
`iceberg-ops migrate-data`): `SchemaReconciler`, `CheckpointStore` and
|
|
74
|
+
`DataMigrator` with `append`, `upsert` and `overwrite_partition` write
|
|
75
|
+
modes, checkpoint resume, dry runs and `row_count` /
|
|
76
|
+
`partition_hash` / `sample` verification. `MigrationResult` reports
|
|
77
|
+
`dest_snapshot_before` (a rollback target), `rows_inserted` and
|
|
78
|
+
`rows_updated`; `CheckpointStore.job` exposes the recorded job
|
|
79
|
+
settings. Every snapshot the migrator writes carries
|
|
80
|
+
`iceberg-ops.migration-job` and `iceberg-ops.migration-batch` summary
|
|
81
|
+
properties.
|
|
82
|
+
- Basic maintenance helpers for self-managed tables
|
|
83
|
+
(`iceberg_ops.maintenance`): `compact_small_files_streaming`,
|
|
84
|
+
`expire_snapshots`, `remove_orphan_files` and `table_health_report`.
|
|
85
|
+
`SnapshotExpiryResult.protected_ids` lists snapshots kept because a
|
|
86
|
+
branch or tag points at them; `OrphanCleanupResult.failed_paths` lists
|
|
87
|
+
orphans whose deletion failed.
|
|
88
|
+
- Apache Polaris policy executor, experimental (`iceberg_ops.polaris`,
|
|
89
|
+
`iceberg-ops run-policies`): `PolarisClient`, `Policy`,
|
|
90
|
+
`parse_policy`, `fetch_table_policies`, `dispatch_policy`,
|
|
91
|
+
`dispatch_policies` and a `handlers={...}` hook for custom policy
|
|
92
|
+
types. Only `system.snapshot-expiry` is executed.
|
|
93
|
+
- `iceberg_ops.testing`: `FakeResponse` and `FakeSession` test doubles
|
|
94
|
+
for driving the Polaris client without HTTP. They may change before
|
|
95
|
+
1.0.
|
|
96
|
+
|
|
97
|
+
Packaging and project:
|
|
98
|
+
|
|
99
|
+
- Extras: `sqlite`, `glue`, `hive`, `s3tables`, `transforms` (needed to
|
|
100
|
+
write to tables partitioned by bucket, truncate or time transforms)
|
|
101
|
+
and `polaris`. `rest` and `contracts` are accepted but add nothing to
|
|
102
|
+
the core install.
|
|
103
|
+
- A `py.typed` marker, so type checkers use the package's annotations.
|
|
104
|
+
- Documentation at https://namitkewat.github.io/iceberg-ops/, including
|
|
105
|
+
a compatibility page and plain-text `llms.txt` / `llms-full.txt`
|
|
106
|
+
exports of the docs.
|
|
107
|
+
- `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` (Contributor Covenant 3.0),
|
|
108
|
+
`SECURITY.md` (GitHub private vulnerability reporting), `ROADMAP.md`,
|
|
109
|
+
issue forms and a pull request template.
|
|
110
|
+
- `AGENTS.md`, a guide for coding agents working on the repository, and
|
|
111
|
+
`agent-skills/iceberg-ops-migrations/SKILL.md`, a skill that teaches a
|
|
112
|
+
coding agent to write, review and run iceberg-ops migrations.
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
Packaging and CLI:
|
|
117
|
+
|
|
118
|
+
- The command-line tool is `iceberg-ops` only, and its config file is
|
|
119
|
+
`iceberg-ops.yaml`. The earlier short script name and its config file
|
|
120
|
+
name were dropped: that name belongs to an unrelated project.
|
|
121
|
+
- PyIceberg is pinned to `>=0.12.0,<0.13` (was `>=0.11.0,<0.12`) and
|
|
122
|
+
PyArrow to `>=18`. Python 3.14 is tested in CI. PyYAML is a core
|
|
123
|
+
dependency, and `tzdata` is installed on Windows.
|
|
124
|
+
- The `[maintenance]` extra was removed: it only installed boto3, which
|
|
125
|
+
no module imported. `[s3tables]` installs `pyiceberg[rest-sigv4]`;
|
|
126
|
+
`[rest]` and `[polaris]` no longer request a `pyiceberg[rest]` extra,
|
|
127
|
+
which PyIceberg does not define.
|
|
128
|
+
- Development status is Alpha.
|
|
129
|
+
- Default local state: without `db_path` / `warehouse_dir`,
|
|
130
|
+
`CatalogFactory.build("sqlite")` uses `./local_data/` in the current
|
|
131
|
+
working directory at call time, and the CLI's sqlite catalog uses
|
|
132
|
+
`local_data/` next to the config file (or in the current directory
|
|
133
|
+
when there is none). `CatalogFactory.build("memory")` without
|
|
134
|
+
`warehouse_dir` writes data files to a fresh temporary directory.
|
|
135
|
+
- The `glue`, `rest`, `hive` and `s3tables` kinds are loaded through
|
|
136
|
+
`pyiceberg.catalog.load_catalog`, so `.pyiceberg.yaml` and
|
|
137
|
+
`PYICEBERG_CATALOG__<KIND>__*` variables apply; explicit arguments
|
|
138
|
+
win. `db_path` with a remote kind raises `ValueError`, and the local
|
|
139
|
+
kinds reject `uri` / `warehouse` keyword arguments.
|
|
140
|
+
- The CLI no longer accepts the memory catalog kind, which forgot
|
|
141
|
+
applied migrations after each command. Catalog-kind options are
|
|
142
|
+
case-insensitive, and a typo is a usage error.
|
|
143
|
+
- Relative paths in the config file (`db_path`, the sqlite `warehouse`,
|
|
144
|
+
`migrations_dir`) resolve against the config file's directory, and
|
|
145
|
+
`migrations/` defaults to sitting next to it. Paths given as flags
|
|
146
|
+
stay relative to the current directory.
|
|
147
|
+
- CLI exit codes: 2 for usage errors (bad flags or config, missing
|
|
148
|
+
catalog, namespace or migrations directory, invalid `--target` /
|
|
149
|
+
`--steps`); 1 for runtime failures, with a single `Error:` line. Set
|
|
150
|
+
`ICEBERG_OPS_TRACEBACK=1` to see the traceback.
|
|
151
|
+
- A missing migrations directory is an error (`FileNotFoundError` from
|
|
152
|
+
the runner, a usage error in the CLI) instead of reporting that all
|
|
153
|
+
migrations are up to date. An explicit `--config` path that does not
|
|
154
|
+
exist is a usage error, except for `init`.
|
|
155
|
+
|
|
156
|
+
Migrations:
|
|
157
|
+
|
|
158
|
+
- A migration's `version` must equal its file's `NNNN` prefix, and two
|
|
159
|
+
files with the same version are rejected. `revision` numbers new files
|
|
160
|
+
by the same `NNNN_<description>.py` rule the runner uses.
|
|
161
|
+
- `plan`, `status` and `history` no longer create the `_iceberg_ops`
|
|
162
|
+
namespace or the history table; the first write creates them.
|
|
163
|
+
- `--target` / `target=` is compared as a version number: `2` means
|
|
164
|
+
`0002`, and an unknown or malformed target raises `ValueError` (CLI:
|
|
165
|
+
exit 2) before anything is applied. `rollback(target="0000")` rolls
|
|
166
|
+
back every migration. `rollback --steps` must be 1 or more, defaults
|
|
167
|
+
to 1 and cannot be combined with `--target`.
|
|
168
|
+
- `enforce_column` adds a missing column, does nothing for an equivalent
|
|
169
|
+
type (only a changed doc is written), updates a promotable primitive
|
|
170
|
+
type in place, and otherwise raises
|
|
171
|
+
`pyiceberg.exceptions.ValidationError` unless `legacy_archival=True`.
|
|
172
|
+
Promotions follow Iceberg's rules, so string-to-binary and decimal
|
|
173
|
+
scale changes are rejected.
|
|
174
|
+
- `set_clustering_keys` sets only the Iceberg sort order, in one commit.
|
|
175
|
+
It no longer writes `snowflake.cluster-by` or `delta.clustering` table
|
|
176
|
+
properties.
|
|
177
|
+
- `set_snapshot_retention` logs a warning when the catalog is AWS S3
|
|
178
|
+
Tables, which manages snapshot retention itself.
|
|
179
|
+
- `ConcurrentMigrationError` is defined in `iceberg_ops.history` and is
|
|
180
|
+
still importable from `iceberg_ops.runner`.
|
|
181
|
+
- `revision` never overwrites an existing migration file (exit 1).
|
|
182
|
+
`write_autogenerated_revision` refuses to overwrite one unless
|
|
183
|
+
`overwrite=True`, and always writes LF line endings.
|
|
184
|
+
|
|
185
|
+
Autogenerate:
|
|
186
|
+
|
|
187
|
+
- A bare `--target-table` stays bare in the generated file, so the
|
|
188
|
+
migration applies to whichever `--namespace` it runs against. A dotted
|
|
189
|
+
name pins the file to that table and prints a warning.
|
|
190
|
+
- A generated `upgrade()` or `downgrade()` raises `NotImplementedError`
|
|
191
|
+
before any step runs when one of its steps cannot run or cannot be
|
|
192
|
+
undone (a column drop, a type widening, a partition-field removal, a
|
|
193
|
+
sort-order replacement, a property removal with an unknown old
|
|
194
|
+
value). The generated calls are kept as comments.
|
|
195
|
+
- Type-change downgrades are no longer generated as narrowing calls,
|
|
196
|
+
since Iceberg only widens types; they raise `NotImplementedError`.
|
|
197
|
+
- A non-strict `SchemaContract` (the default) keeps the table columns
|
|
198
|
+
it does not list, and the CLI reports them. Set `strict: true` to drop
|
|
199
|
+
them. Nullability and doc differences on existing columns are
|
|
200
|
+
reported, not emitted.
|
|
201
|
+
|
|
202
|
+
Data migration and maintenance:
|
|
203
|
+
|
|
204
|
+
- `write_mode="overwrite_partition"` needs a destination partitioned by
|
|
205
|
+
identity transforms only, and the migration must fit in one commit (at
|
|
206
|
+
most `batch_rows` source rows after the row filter). Anything else is
|
|
207
|
+
rejected before any write.
|
|
208
|
+
- Checkpoints record the job's settings (source and destination table
|
|
209
|
+
UUIDs, row filter, `batch_rows`, write mode, upsert keys and
|
|
210
|
+
reconciler). Resuming with different settings, a recreated
|
|
211
|
+
destination or a checkpoint without these settings raises
|
|
212
|
+
`CheckpointError`, which is now a `RuntimeError`. A completed
|
|
213
|
+
checkpoint is terminal: call `CheckpointStore.reset()` to run again.
|
|
214
|
+
- `MigrationResult.new_snapshots` lists every destination snapshot the
|
|
215
|
+
run created (up to three per upsert batch, or per 1,000-key sub-batch
|
|
216
|
+
for composite keys), and
|
|
217
|
+
`CheckpointStore.mark_committed` accepts a list of snapshot IDs.
|
|
218
|
+
- `DataMigrator` rejects blank or repeated `upsert_keys`, `upsert_keys`
|
|
219
|
+
with a write mode other than upsert, and upsert keys missing from the
|
|
220
|
+
projected destination schema. An upsert with
|
|
221
|
+
`on_missing_in_source="skip"` that would drop destination columns is
|
|
222
|
+
rejected; with `"null"`, the plan warns that matched keys get those
|
|
223
|
+
values overwritten with null. `SchemaReconciler.compute_plan` reports
|
|
224
|
+
a `column_mapping` key that is not a source column.
|
|
225
|
+
- `migrate-data --batch-rows` and `--sample-size` must be at least 1,
|
|
226
|
+
`--upsert-keys` ignores empty entries, and a missing destination table
|
|
227
|
+
explains that `migrate-data` does not create it.
|
|
228
|
+
- `remove_orphan_files` defaults to `dry_run=True`; pass
|
|
229
|
+
`dry_run=False` to delete. It refuses tables with `gc.enabled=false`
|
|
230
|
+
(in a dry run too), rejects a negative `older_than_hours` and `sweep`
|
|
231
|
+
entries that are absolute or escape the table location, and warns when
|
|
232
|
+
`older_than_hours` is below 24. `deleted` is True only when at least
|
|
233
|
+
one file was removed.
|
|
234
|
+
- All `expire_snapshots` arguments after `table` are keyword-only, and
|
|
235
|
+
`max_age_hours` accepts fractional hours. Omitted arguments fall back
|
|
236
|
+
to the table's `history.expire.max-snapshot-age-ms` /
|
|
237
|
+
`history.expire.min-snapshots-to-keep` properties (as written by
|
|
238
|
+
`set_snapshot_retention`), then to the Iceberg defaults of 5 days and
|
|
239
|
+
1 snapshot. Invalid values raise `ValueError`. The fallback to
|
|
240
|
+
PyIceberg's internal `ExpireSnapshots` class for releases older than
|
|
241
|
+
0.11 was removed.
|
|
242
|
+
- `compact_small_files_streaming` lost its unused `batch_rows`
|
|
243
|
+
parameter.
|
|
244
|
+
|
|
245
|
+
Polaris (experimental):
|
|
246
|
+
|
|
247
|
+
- Only `system.snapshot-expiry` is executed, and only with
|
|
248
|
+
`"enable": true` (a JSON boolean). `system.metadata-compaction` no
|
|
249
|
+
longer triggers a data-file rewrite; it, `system.data-compaction` and
|
|
250
|
+
`system.orphan-file-removal` are reported as skipped.
|
|
251
|
+
- `system.snapshot-expiry` reads the Polaris schema
|
|
252
|
+
(`config.max_snapshot_age_days`, `config.min_snapshot_to_keep`;
|
|
253
|
+
`max_ref_age_days` is ignored with a note). Legacy top-level `*_ms`
|
|
254
|
+
keys, unknown top-level keys, unknown schema versions and invalid
|
|
255
|
+
values fail the dispatch instead of running on defaults.
|
|
256
|
+
- A custom handler returning `None` yields empty details; any other
|
|
257
|
+
non-dict return marks the dispatch as failed.
|
|
258
|
+
- `run-policies` is marked `[experimental]` and prints a warning when
|
|
259
|
+
run, because its requests do not yet match the released Polaris
|
|
260
|
+
policy API.
|
|
261
|
+
|
|
262
|
+
### Fixed
|
|
263
|
+
|
|
264
|
+
Migrations and CLI:
|
|
265
|
+
|
|
266
|
+
- Two concurrent `migrate` runs for the same namespace no longer leave
|
|
267
|
+
duplicate history rows on PyIceberg 0.12
|
|
268
|
+
(apache/iceberg-python#4194). The run that records second raises
|
|
269
|
+
`ConcurrentMigrationError`, and history keeps one row per version and
|
|
270
|
+
namespace. Existing duplicate rows are read as their latest status and
|
|
271
|
+
removed by the next write for that version.
|
|
272
|
+
- When a migration's own commit conflicts with a concurrent commit, it
|
|
273
|
+
raises `ConcurrentMigrationError` and records nothing, instead of
|
|
274
|
+
overwriting the other run's history with `failed`.
|
|
275
|
+
- `migrate --target 2` no longer applies every migration, `stamp
|
|
276
|
+
--target 2` no longer stamps all of them, `rollback --target 1` is no
|
|
277
|
+
longer a no-op, and `"2"` no longer also selects `0010`.
|
|
278
|
+
- `rollback(steps=0)` and a negative `steps` raise `ValueError` instead
|
|
279
|
+
of rolling back an unexpected number of migrations.
|
|
280
|
+
- `rollback` checks that every migration file it needs exists before
|
|
281
|
+
running any `downgrade()`. `status` lists history entries whose
|
|
282
|
+
migration file is missing.
|
|
283
|
+
- `enforce_column` is idempotent for struct, list and map columns.
|
|
284
|
+
- `enforce_column(..., legacy_archival=True)` archives only on a real
|
|
285
|
+
type incompatibility. Commit errors and failing contracts propagate
|
|
286
|
+
instead of archiving the column and adding an empty replacement.
|
|
287
|
+
- Archival, in `enforce_column` and in the `enforce_rename` collision
|
|
288
|
+
path, refuses columns used by the partition spec, sort order or
|
|
289
|
+
identifier fields. An archived required column becomes optional, so
|
|
290
|
+
later appends no longer fail. If archival or the rename fails part
|
|
291
|
+
way, the column is restored with its original doc and required flag;
|
|
292
|
+
if that restore also fails, a `RuntimeError` is raised.
|
|
293
|
+
- `enforce_rename(name, name)` is a no-op, and renaming across
|
|
294
|
+
different parent structs raises `ValueError`.
|
|
295
|
+
- `enforce_column` and `add_column` accept primitive PyArrow types such
|
|
296
|
+
as `pa.float64()` and `pa.string()`.
|
|
297
|
+
- Migration files that define a `@dataclass` (with `from __future__
|
|
298
|
+
import annotations`) load. Same-named files in different namespace
|
|
299
|
+
directories no longer collide. A broken migration file raises an error
|
|
300
|
+
that names the file, and the CLI prints it as one `Error:` line.
|
|
301
|
+
- `create_table_if_not_exists` works for nested namespaces such as
|
|
302
|
+
`sales.eu`, and returns the existing table if another process creates
|
|
303
|
+
it first.
|
|
304
|
+
- `set_table_properties` accepts a property key named `properties`.
|
|
305
|
+
- `revision --autogenerate` honours `--catalog-name`, the config's
|
|
306
|
+
`catalog_name` and the config-relative sqlite defaults.
|
|
307
|
+
- Missing extras, missing tables, missing catalog properties,
|
|
308
|
+
connection failures, permission or server errors, commit conflicts and
|
|
309
|
+
corrupt checkpoints print a single `Error:` line and exit 1 instead of
|
|
310
|
+
a traceback.
|
|
311
|
+
- Config file problems (invalid YAML, a non-mapping top level,
|
|
312
|
+
non-string values such as `namespace: 123`) are usage errors. Unknown
|
|
313
|
+
keys print a warning.
|
|
314
|
+
- Config and generated files are read and written as UTF-8 (a BOM is
|
|
315
|
+
tolerated). `init` appends its block to `.gitignore` without rewriting
|
|
316
|
+
the existing content, and honours `--config` and `--migrations-dir`.
|
|
317
|
+
- Using the `s3tables` kind without boto3 says to install
|
|
318
|
+
`iceberg-ops[s3tables]`.
|
|
319
|
+
- `CatalogFactory.build(..., reset=True)` refuses to delete a
|
|
320
|
+
filesystem root, the home directory, the working directory or one of
|
|
321
|
+
its parents, or a database file that is not SQLite.
|
|
322
|
+
- The `sqlite` and `memory` kinds reject a warehouse that looks like a
|
|
323
|
+
URI and local paths containing `#` or `?`, which PyIceberg would
|
|
324
|
+
misread, before anything is written.
|
|
325
|
+
- The history table is created with `commit.retry.num-retries=20`, so
|
|
326
|
+
runners for different namespaces sharing it no longer lose history
|
|
327
|
+
writes to commit contention.
|
|
328
|
+
- When recording a failure in history itself fails, the migration's
|
|
329
|
+
own error is still the one raised; the history error is logged.
|
|
330
|
+
- `history` sizes its columns to their contents, so full ISO-8601
|
|
331
|
+
timestamps no longer push DESCRIPTION out of line, and a multi-line
|
|
332
|
+
error is printed on one line.
|
|
333
|
+
- `create_table_if_not_exists` raises `ValueError` before creating
|
|
334
|
+
anything when a `pa.Schema` is combined with a non-empty
|
|
335
|
+
`partition_spec` or `sort_order`.
|
|
336
|
+
- `SchemaContract.validate` compares struct, list and map types without
|
|
337
|
+
regard to field IDs, and `enforce_rename` checks the contract when
|
|
338
|
+
neither column exists.
|
|
339
|
+
|
|
340
|
+
Autogenerate:
|
|
341
|
+
|
|
342
|
+
- `revision --autogenerate` passes the table's partition spec, sort
|
|
343
|
+
order and format version to the diff. Dropping a column that the
|
|
344
|
+
spec, the sort order or the identifier fields use makes the generated
|
|
345
|
+
`upgrade()` raise before any step runs, and a type the table's format
|
|
346
|
+
version cannot hold is a usage error. Column names containing `.`,
|
|
347
|
+
decimals with a scale outside `0..precision` or more than 38 digits,
|
|
348
|
+
and `fixed[0]` are rejected too. Existing nested columns of a
|
|
349
|
+
`--target-schema-arrow` target that were not compared are named in a
|
|
350
|
+
warning instead of reporting no differences.
|
|
351
|
+
- Partition and sort fields sourced from a nested column render as the
|
|
352
|
+
full dotted path and bind to the nested field.
|
|
353
|
+
- Autogenerate no longer emits type changes that Iceberg does not
|
|
354
|
+
allow, such as a decimal scale change, string to long, or binary to
|
|
355
|
+
string; such a step makes `upgrade()` raise before anything runs.
|
|
356
|
+
- A generated `downgrade()` with an irreversible step no longer rolls
|
|
357
|
+
back part of the migration and then fails.
|
|
358
|
+
- Nested type changes (struct, list, map) in a `pyiceberg.schema.Schema`
|
|
359
|
+
target (Python API) produce a file that compiles, with the step
|
|
360
|
+
blocked for manual completion, instead of a syntax error.
|
|
361
|
+
- Invalid YAML contracts, unknown types and bad rename hints make
|
|
362
|
+
`revision --autogenerate` exit with a usage error.
|
|
363
|
+
- Partition fields with non-identity transforms (day, bucket, ...)
|
|
364
|
+
import their transform and run. Sort-order diffs compare transform
|
|
365
|
+
and null order, and reject orders the generated call cannot express.
|
|
366
|
+
- `--target-schema-arrow` modules are importable from the current
|
|
367
|
+
directory when the CLI runs from the installed console script.
|
|
368
|
+
|
|
369
|
+
Data migration:
|
|
370
|
+
|
|
371
|
+
- `overwrite_partition` could delete rows written by an earlier batch
|
|
372
|
+
of the same migration; it is now limited to one commit (see Changed).
|
|
373
|
+
- A resume reads the source snapshot recorded in the checkpoint, so rows
|
|
374
|
+
added to the source after a crash are neither skipped nor duplicated.
|
|
375
|
+
`verify()` after a run compares against the snapshot that was
|
|
376
|
+
migrated.
|
|
377
|
+
- Re-running with a checkpoint that records a completed migration raises
|
|
378
|
+
`CheckpointError` instead of duplicating and dropping rows.
|
|
379
|
+
- A batch committed just before a crash, but not yet checkpointed, is
|
|
380
|
+
not written twice on resume.
|
|
381
|
+
- Pre-flight runs every check before it changes the destination, so
|
|
382
|
+
auto-added columns are no longer committed when a later check fails.
|
|
383
|
+
- Writing to a destination partitioned by bucket, truncate, year, month,
|
|
384
|
+
day or hour without `pyiceberg-core` fails before any write with an
|
|
385
|
+
`ImportError` naming `pip install 'iceberg-ops[transforms]'`.
|
|
386
|
+
- The first upsert after a schema-only change (for example an
|
|
387
|
+
auto-added column) no longer fails; an empty snapshot is committed
|
|
388
|
+
first, as a workaround for apache/iceberg-python#2467.
|
|
389
|
+
- Composite-key upserts with more than a few thousand keys per batch no
|
|
390
|
+
longer crash the interpreter (apache/iceberg-python#3508). Large
|
|
391
|
+
batches are split into sub-batches of 1,000 keys within one commit.
|
|
392
|
+
- When the source schema changed after the pinned snapshot, the plan
|
|
393
|
+
uses the schema at that snapshot.
|
|
394
|
+
- `verify(method="partition_hash")` works when a partition field's name
|
|
395
|
+
differs from its source column, and raises `NotImplementedError` for
|
|
396
|
+
non-identity partitions instead of reporting a mismatch (the CLI
|
|
397
|
+
prints `Verify error:` and exits 1).
|
|
398
|
+
- A resume raises `CheckpointError` when the destination was rolled
|
|
399
|
+
back past a checkpointed batch, instead of skipping that batch's
|
|
400
|
+
rows. Every refusal that follows committed batches names the
|
|
401
|
+
snapshot to roll back to before `reset()`.
|
|
402
|
+
- Upsert keys that the plan would fill with null are rejected before
|
|
403
|
+
any write.
|
|
404
|
+
- `DataMigrator` and `migrate-data` refuse a source and destination
|
|
405
|
+
that are the same table.
|
|
406
|
+
- Checkpoint writes are fsync-ed before the atomic rename.
|
|
407
|
+
|
|
408
|
+
Maintenance:
|
|
409
|
+
|
|
410
|
+
- `remove_orphan_files` no longer deletes live position-delete,
|
|
411
|
+
equality-delete and deletion-vector files, or registered table and
|
|
412
|
+
partition statistics files.
|
|
413
|
+
- `compact_small_files_streaming` no longer deletes its staged files
|
|
414
|
+
when the commit outcome is unknown (for example
|
|
415
|
+
`CommitStateUnknownException`), which could make a committed table
|
|
416
|
+
unreadable.
|
|
417
|
+
- `expire_snapshots` skips branch and tag heads (including the current
|
|
418
|
+
snapshot after a rollback) instead of aborting, and dry runs select
|
|
419
|
+
the same snapshots as real runs. A max age older than the epoch
|
|
420
|
+
expires nothing instead of overflowing.
|
|
421
|
+
- Compaction and orphan removal accept Windows drive-letter table
|
|
422
|
+
locations.
|
|
423
|
+
|
|
424
|
+
### Security
|
|
425
|
+
|
|
426
|
+
- Generated migration files cannot run injected code: the `-m`
|
|
427
|
+
message and column, partition and property names and values are
|
|
428
|
+
written as Python literals, and docstring text is escaped.
|
|
429
|
+
- Polaris OAuth2 token-endpoint errors no longer echo the response
|
|
430
|
+
payload, which could leak tokens to logs or stderr. The client strips
|
|
431
|
+
surrounding whitespace from a token, rejects an empty token or one
|
|
432
|
+
with inner whitespace or control characters (`run-policies` exits
|
|
433
|
+
2), and keeps the token and client secret out of connection-error
|
|
434
|
+
messages.
|
|
435
|
+
- A failed migration's history row holds only `"<Type>: <message>"`;
|
|
436
|
+
the traceback is logged locally at `DEBUG` level, since everyone with
|
|
437
|
+
catalog access can read the history table.
|
|
438
|
+
- `remove_orphan_files` matches file names after Unicode NFC
|
|
439
|
+
normalisation and case folding, does not follow directory symlinks
|
|
440
|
+
or Windows junctions, and never deletes a file whose real path is
|
|
441
|
+
outside the table root.
|
|
442
|
+
- Compaction escapes identity-partition values the way PyIceberg does
|
|
443
|
+
and refuses to stage files outside `<table>/data`, so a partition
|
|
444
|
+
value such as `../../x` cannot write elsewhere.
|
|
445
|
+
- `run-policies` reads `ICEBERG_OPS_POLARIS_TOKEN`,
|
|
446
|
+
`ICEBERG_OPS_POLARIS_CLIENT_ID` and `ICEBERG_OPS_POLARIS_CLIENT_SECRET`
|
|
447
|
+
(preferred over flags, which show up in process listings) and warns
|
|
448
|
+
when `--polaris-url` uses plain http to a non-loopback host.
|
|
449
|
+
|
|
450
|
+
### Known limitations
|
|
451
|
+
|
|
452
|
+
- Concurrent runs for one namespace are not guarded, only detected: a
|
|
453
|
+
second run can execute `upgrade()` before it fails to record history.
|
|
454
|
+
Run one migration job per namespace at a time and keep `upgrade()`
|
|
455
|
+
idempotent.
|
|
456
|
+
- The history table keeps the latest status of each version per
|
|
457
|
+
namespace, not one row per attempt. Earlier states are readable by
|
|
458
|
+
time travel on the history table until its snapshots expire.
|
|
459
|
+
- `status`, `current`, `plan` and `history` print only and exit 0 even
|
|
460
|
+
when a migration is recorded as failed; there is no `check` command
|
|
461
|
+
yet. The runner methods print their reports and return `None`.
|
|
462
|
+
- CLI autogenerate diffs columns only. Autogenerate does not emit
|
|
463
|
+
nullability or doc changes, blocks nested type changes from a
|
|
464
|
+
`pyiceberg.schema.Schema` target, and does not compare existing
|
|
465
|
+
nested columns of a `pyarrow.Schema` target (they are treated as
|
|
466
|
+
unchanged; the CLI names them in a warning). Each generated step commits on its own, so a failing step
|
|
467
|
+
leaves earlier steps applied.
|
|
468
|
+
- Only SQLite and in-memory catalogs are integration-tested. Glue, REST,
|
|
469
|
+
Hive and S3 Tables catalogs are reached through PyIceberg but have not
|
|
470
|
+
been tested against live services.
|
|
471
|
+
- Data migration: `overwrite_partition` is limited to one commit and
|
|
472
|
+
identity-partitioned destinations; the destination table must already
|
|
473
|
+
exist. `batch_rows` bounds the rows per commit, not peak memory, which
|
|
474
|
+
grows with the number and size of source data files
|
|
475
|
+
(apache/iceberg-python#2407); an upsert also loads the matched
|
|
476
|
+
destination rows. Duplicate keys within one batch raise `ValueError`,
|
|
477
|
+
and across batches the later row wins. `verify="sample"` can report a
|
|
478
|
+
false mismatch on larger tables, and `partition_hash` needs an
|
|
479
|
+
identity-partitioned or unpartitioned destination and reads both
|
|
480
|
+
tables in full.
|
|
481
|
+
- Maintenance: compaction and orphan removal support the local
|
|
482
|
+
filesystem only. Compaction supports identity partitions and
|
|
483
|
+
unpartitioned tables (other partitions are skipped), rewrites
|
|
484
|
+
qualifying partitions whole, writes `2N + 1` snapshots for `N`
|
|
485
|
+
partitions, uses PyArrow's default codec, fails when a concurrent
|
|
486
|
+
append lands in a partition being compacted, and its memory is not bounded by `target_size_mb`
|
|
487
|
+
(apache/iceberg-python#2407). Delete files and deletion vectors are
|
|
488
|
+
out of scope for compaction (there is no guard) and the health
|
|
489
|
+
report. Snapshot expiry is metadata-only.
|
|
490
|
+
- The Polaris executor does not call the Polaris 1.x policy endpoints,
|
|
491
|
+
so fetching policies from a released Polaris server fails. It has not
|
|
492
|
+
been validated against a live server.
|
|
493
|
+
- Python 3.15 is not tested. At release time PyIceberg 0.12.0 published
|
|
494
|
+
no Python 3.15 wheels.
|
|
495
|
+
- PyArrow 26 and later refuse to import next to NumPy 1.x. iceberg-ops
|
|
496
|
+
does not use NumPy; in an environment pinned to NumPy 1.x, also pin
|
|
497
|
+
`pyarrow<26`.
|
|
498
|
+
- On Windows, write a local warehouse in `.pyiceberg.yaml` as
|
|
499
|
+
`file://C:/path`; PyArrow rejects the `file:///C:/path` form.
|
|
500
|
+
|
|
501
|
+
[Unreleased]: https://github.com/namitkewat/iceberg-ops/compare/v0.1.0...HEAD
|
|
502
|
+
[0.1.0]: https://github.com/namitkewat/iceberg-ops/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Namit Kewat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|